1792 lines
49 KiB
JSON
1792 lines
49 KiB
JSON
{
|
|
"schemaVersion": "1.0",
|
|
"contractVersion": "1.9.0",
|
|
"principles": {
|
|
"syntheticFixturesOnly": true,
|
|
"deterministicIdentity": true,
|
|
"fixedReferenceClock": "2026-08-08T09:00:00Z",
|
|
"noProductionData": true,
|
|
"operationIdOnly": true,
|
|
"evidenceRequired": true,
|
|
"defaultIsolation": "ephemeral-postgres"
|
|
},
|
|
"scenarios": [
|
|
{
|
|
"id": "scenario.common.design-system.interactions",
|
|
"title": "KBX 컴포넌트 상태·키보드 상호작용 계약",
|
|
"screenId": "COMMON-DS-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"component-contract",
|
|
"accessibility",
|
|
"keyboard",
|
|
"visual-regression"
|
|
],
|
|
"fixtureSets": [],
|
|
"requiredPermissions": [],
|
|
"apiOperations": [],
|
|
"isolation": "ui-only",
|
|
"steps": [
|
|
{
|
|
"id": "states",
|
|
"action": "open-catalog",
|
|
"density": "compact"
|
|
},
|
|
{
|
|
"id": "tabs",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"Tab",
|
|
"ArrowRight",
|
|
"ArrowLeft",
|
|
"Home",
|
|
"End"
|
|
]
|
|
},
|
|
{
|
|
"id": "state-surfaces",
|
|
"action": "assert-visible",
|
|
"states": [
|
|
"changed",
|
|
"warning",
|
|
"ai-suggested",
|
|
"loading",
|
|
"empty",
|
|
"error"
|
|
]
|
|
}
|
|
],
|
|
"assertions": [
|
|
"input state meaning is visible without color alone",
|
|
"tabs support roving keyboard focus",
|
|
"loading empty error have distinct text and recovery semantics",
|
|
"component catalog renders without raw business-screen overrides"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.common.operations.stale-event",
|
|
"title": "업무 예외 해결 후 오래된 Event가 재오픈하지 않음",
|
|
"screenId": "COMMON-OPS-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"exception",
|
|
"outbox",
|
|
"projection",
|
|
"ordering"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.common.operations"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.operations.read",
|
|
"common.operations.resolve"
|
|
],
|
|
"apiOperations": [
|
|
"common.operations.search"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "open-v10",
|
|
"action": "projection-event",
|
|
"fixtureRef": "event.stock-shortage.v10"
|
|
},
|
|
{
|
|
"id": "resolve-v11",
|
|
"action": "projection-event",
|
|
"fixtureRef": "event.stock-recovered.v11"
|
|
},
|
|
{
|
|
"id": "late-v10",
|
|
"action": "projection-event",
|
|
"fixtureRef": "event.stock-shortage.v10"
|
|
},
|
|
{
|
|
"id": "search",
|
|
"action": "api-query",
|
|
"operationId": "common.operations.search"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"work item remains resolved",
|
|
"sourceVersion=11",
|
|
"late sourceVersion=10 is ignored",
|
|
"audit retains resolution"
|
|
],
|
|
"evidence": [
|
|
"db-assertions",
|
|
"projection-event-log"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.common.reconcile.grace-period",
|
|
"title": "OMS-WMS 대사 Grace Period 내 Pending 이후 Mismatch",
|
|
"screenId": "COMMON-REC-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"reconcile",
|
|
"eventual-consistency"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.common.reconcile"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.reconcile.read"
|
|
],
|
|
"apiOperations": [
|
|
"common.reconcile.search"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "within-grace",
|
|
"action": "evaluate-reconcile",
|
|
"fixtureRef": "reconcile.oms-wms.pending"
|
|
},
|
|
{
|
|
"id": "after-grace",
|
|
"action": "advance-reference-clock",
|
|
"duration": "PT5M"
|
|
},
|
|
{
|
|
"id": "reevaluate",
|
|
"action": "evaluate-reconcile",
|
|
"fixtureRef": "reconcile.oms-wms.pending"
|
|
},
|
|
{
|
|
"id": "search",
|
|
"action": "api-query",
|
|
"operationId": "common.reconcile.search"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"within grace status=pending",
|
|
"after grace status=mismatch",
|
|
"difference=-2",
|
|
"latest observation timestamp wins"
|
|
],
|
|
"evidence": [
|
|
"db-assertions",
|
|
"clock-snapshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.erp.inventory-move.workflow",
|
|
"title": "ERP 재고이동 정상 상태전이와 잘못된 창고 차단",
|
|
"screenId": "ERP-INV-MOVE-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"workflow",
|
|
"domain-validation",
|
|
"audit"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.erp.inventory-move"
|
|
],
|
|
"requiredPermissions": [
|
|
"erp.inventory.move.read",
|
|
"erp.inventory.move.write"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "invalid-same-warehouse",
|
|
"action": "expect-db-or-domain-rejection",
|
|
"fixtureRef": "move.invalid.same-warehouse"
|
|
},
|
|
{
|
|
"id": "valid-draft",
|
|
"action": "fixture-insert",
|
|
"fixtureRef": "move.valid.1"
|
|
},
|
|
{
|
|
"id": "transition",
|
|
"action": "workflow-sequence",
|
|
"states": [
|
|
"DRAFT",
|
|
"CONFIRMED",
|
|
"IN_TRANSIT",
|
|
"RECEIVED"
|
|
]
|
|
}
|
|
],
|
|
"assertions": [
|
|
"same source/destination rejected",
|
|
"version monotonically increases",
|
|
"received move is no longer directly editable"
|
|
],
|
|
"evidence": [
|
|
"db-assertions",
|
|
"workflow-log"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.erp.inventory.master-detail-context",
|
|
"title": "ERP 재고 T05 Master/Detail Context·이력·Drill-down",
|
|
"screenId": "ERP-INV-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"master-detail",
|
|
"context-retention",
|
|
"drill-down",
|
|
"history"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.erp.inventory-explorer"
|
|
],
|
|
"requiredPermissions": [
|
|
"erp.inventory.read"
|
|
],
|
|
"apiOperations": [
|
|
"erp.inventory.search",
|
|
"erp.inventory.locations",
|
|
"erp.inventory.history"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "search",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F3"
|
|
],
|
|
"operationId": "erp.inventory.search"
|
|
},
|
|
{
|
|
"id": "select",
|
|
"action": "select-fixture",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "locations",
|
|
"action": "query-detail",
|
|
"operationId": "erp.inventory.locations",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "history",
|
|
"action": "query-history",
|
|
"operationId": "erp.inventory.history",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "drill",
|
|
"action": "grid-drill-down",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "requery",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F3"
|
|
],
|
|
"operationId": "erp.inventory.search"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"selected item drives both location and history queries",
|
|
"master selection is restored after requery when the item remains in result",
|
|
"inventory quantity drill-down opens without losing search context",
|
|
"history is ordered newest-first",
|
|
"location/history errors keep the master context recoverable"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot",
|
|
"api-transcript"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.erp.item-price.fast-entry",
|
|
"title": "ERP 품목 단가 T04 Fast Entry 일괄저장",
|
|
"screenId": "ERP-PRICE-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"fast-entry",
|
|
"lookup",
|
|
"paste",
|
|
"fill-down",
|
|
"idempotency",
|
|
"audit",
|
|
"outbox"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.erp.item-price-fast-entry"
|
|
],
|
|
"requiredPermissions": [
|
|
"erp.item.price.read",
|
|
"erp.item.price.write",
|
|
"erp.item.read"
|
|
],
|
|
"apiOperations": [
|
|
"lookup.items.resolveByCode",
|
|
"erp.itemPrices.bulkSave"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "item",
|
|
"action": "lookup-resolve",
|
|
"operationId": "lookup.items.resolveByCode",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "paste",
|
|
"action": "grid-paste",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "fill-down",
|
|
"action": "grid-fill-down"
|
|
},
|
|
{
|
|
"id": "save",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F8"
|
|
],
|
|
"operationId": "erp.itemPrices.bulkSave",
|
|
"idempotencyKeyRef": "idem.erp.item-price.001"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"F2 resolves a real item identity",
|
|
"multi-cell paste preserves row identity and normalizes typed values",
|
|
"duplicate item/effective-date rows are rejected before mutation",
|
|
"server upsert is idempotent for the same command key",
|
|
"audit records before/after price",
|
|
"outbox is created in the same database transaction"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"problem-json"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.external-data.kis.strict-expiry",
|
|
"title": "KIS 현재가 만료 캐시를 최신값으로 사용하지 않음",
|
|
"screenId": "COMMON-DATA-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"external-data",
|
|
"kis",
|
|
"freshness",
|
|
"strict"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.external-data.kis"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.external-data.read"
|
|
],
|
|
"apiOperations": [
|
|
"common.externalData.status"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "status",
|
|
"action": "api-query",
|
|
"operationId": "common.externalData.status",
|
|
"fixtureRef": "external-data.kis.cached.expired"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"expired KIS cache is never returned as fresh",
|
|
"max stale fallback is zero",
|
|
"provider failure does not silently reuse expired current-price data",
|
|
"provenance keeps last received/fresh-until timestamps"
|
|
],
|
|
"evidence": [
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"external-data-provenance"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.external-data.opendart.stale-revalidate",
|
|
"title": "OPENDART Stale 데이터 명시 표시 후 재수집 예약",
|
|
"screenId": "COMMON-DATA-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"external-data",
|
|
"opendart",
|
|
"stale-while-revalidate",
|
|
"provenance"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.external-data.opendart"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.external-data.read",
|
|
"common.external-data.refresh"
|
|
],
|
|
"apiOperations": [
|
|
"common.externalData.status",
|
|
"common.externalData.refresh"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "status",
|
|
"action": "api-query",
|
|
"operationId": "common.externalData.status",
|
|
"fixtureRef": "external-data.opendart.cached.stale"
|
|
},
|
|
{
|
|
"id": "refresh",
|
|
"action": "api-command",
|
|
"operationId": "common.externalData.refresh",
|
|
"fixtureRef": "external-data.opendart.cached.stale"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"stale state is explicit",
|
|
"last normalized value may remain visible within usable window",
|
|
"refresh is queued once through common scheduler",
|
|
"raw provider payload is not persisted by default",
|
|
"payload hash and normalizer version remain traceable"
|
|
],
|
|
"evidence": [
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"external-data-provenance"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.integration.inbox.duplicate",
|
|
"title": "Inbox 동일 메시지 중복수신 1회 처리",
|
|
"screenId": "COMMON-OPS-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"integration",
|
|
"inbox",
|
|
"idempotency",
|
|
"duplicate"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.integration.inbox"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.operations.read"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "first",
|
|
"action": "deliver-inbox",
|
|
"fixtureRef": "integration.inbox.message.1"
|
|
},
|
|
{
|
|
"id": "duplicate",
|
|
"action": "deliver-inbox",
|
|
"fixtureRef": "integration.inbox.message.1"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"receipt insert succeeds once",
|
|
"domain command is executed once",
|
|
"duplicate returns previous processing result or no-op",
|
|
"audit/outbox side effects exist once"
|
|
],
|
|
"evidence": [
|
|
"db-assertions",
|
|
"integration-receipt-log",
|
|
"correlation-ids"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.integration.manual-retry.idempotent",
|
|
"title": "최종 실패 외부연계 수동 재처리 중복 클릭 방지",
|
|
"screenId": "COMMON-OPS-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"integration",
|
|
"manual-retry",
|
|
"idempotency"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.integration.outbox"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.integration.read",
|
|
"common.integration.retry"
|
|
],
|
|
"apiOperations": [
|
|
"common.integrations.retryAttempt"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "retry",
|
|
"action": "api-command",
|
|
"operationId": "common.integrations.retryAttempt",
|
|
"fixtureRef": "integration.attempt.failed.1",
|
|
"idempotencyKeyRef": "idem.integration.retry.001"
|
|
},
|
|
{
|
|
"id": "replay",
|
|
"action": "api-command",
|
|
"operationId": "common.integrations.retryAttempt",
|
|
"fixtureRef": "integration.attempt.failed.1",
|
|
"idempotencyKeyRef": "idem.integration.retry.001"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"one durable retry is scheduled",
|
|
"manual retry key is unique",
|
|
"repeated user click does not create duplicate delivery"
|
|
],
|
|
"evidence": [
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"correlation-ids"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.integration.outbox.permanent-failure",
|
|
"title": "외부연계 영구 실패가 업무 예외로 승격",
|
|
"screenId": "COMMON-OPS-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"integration",
|
|
"outbox",
|
|
"permanent-failure",
|
|
"exception"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.integration.outbox"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.operations.read"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "dispatch",
|
|
"action": "inject-integration-result",
|
|
"fixtureRef": "integration.message.dispatch.2",
|
|
"result": "http-400"
|
|
},
|
|
{
|
|
"id": "project",
|
|
"action": "run-operations-projector"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"no automatic retry is scheduled",
|
|
"integration attempt is failed/permanent",
|
|
"business state is not rolled back",
|
|
"one operations work item is created with source reference"
|
|
],
|
|
"evidence": [
|
|
"db-assertions",
|
|
"integration-attempt-log",
|
|
"correlation-ids"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.integration.outbox.transient-retry",
|
|
"title": "Outbox 외부연계 일시 장애 후 자동 재시도",
|
|
"screenId": "COMMON-OPS-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"integration",
|
|
"outbox",
|
|
"retry",
|
|
"polly",
|
|
"hangfire"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.integration.outbox"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.operations.read"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "dispatch",
|
|
"action": "inject-integration-result",
|
|
"fixtureRef": "integration.message.dispatch.1",
|
|
"result": "http-503"
|
|
},
|
|
{
|
|
"id": "short-retry",
|
|
"action": "assert-short-retry"
|
|
},
|
|
{
|
|
"id": "durable-retry",
|
|
"action": "advance-hangfire-clock"
|
|
},
|
|
{
|
|
"id": "success",
|
|
"action": "inject-integration-result",
|
|
"fixtureRef": "integration.message.dispatch.1",
|
|
"result": "http-200"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"business order remains confirmed while integration state is retrying",
|
|
"short retry is bounded",
|
|
"durable retry persists beyond request lifetime",
|
|
"message is delivered once",
|
|
"no operations exception remains after success"
|
|
],
|
|
"evidence": [
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"integration-attempt-log",
|
|
"correlation-ids"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.oms.order-import.partial",
|
|
"title": "OMS 주문 Excel 검증 후 부분 반영",
|
|
"screenId": "OMS-ORD-003",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"excel",
|
|
"mapping",
|
|
"staging",
|
|
"partial-success",
|
|
"job"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.oms.order-import"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.import"
|
|
],
|
|
"apiOperations": [
|
|
"common.imports.createSession",
|
|
"common.imports.validate",
|
|
"common.imports.commit",
|
|
"common.imports.getSession"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "upload",
|
|
"action": "upload-synthetic-file",
|
|
"fixtureRef": "file.order-import.6rows",
|
|
"operationId": "common.imports.createSession"
|
|
},
|
|
{
|
|
"id": "mapping",
|
|
"action": "assert-mapping-sources",
|
|
"expected": [
|
|
"exact",
|
|
"alias"
|
|
]
|
|
},
|
|
{
|
|
"id": "validate",
|
|
"action": "api-command",
|
|
"operationId": "common.imports.validate"
|
|
},
|
|
{
|
|
"id": "commit",
|
|
"action": "api-command",
|
|
"operationId": "common.imports.commit",
|
|
"idempotencyKeyRef": "idem.import.commit.001"
|
|
},
|
|
{
|
|
"id": "status",
|
|
"action": "api-query",
|
|
"operationId": "common.imports.getSession"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"total rows=6",
|
|
"valid rows=4",
|
|
"invalid rows=2",
|
|
"invalid rows retain original Excel row number",
|
|
"only valid rows commit",
|
|
"error workbook is available",
|
|
"commit retry does not duplicate orders"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"api-transcript",
|
|
"import-session-snapshot",
|
|
"error-workbook-metadata",
|
|
"db-assertions",
|
|
"problem-json"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.oms.order-list.all-filtered-ship",
|
|
"title": "OMS 주문조회 검색결과 전체선택 안전 출고 계약",
|
|
"screenId": "OMS-ORD-001",
|
|
"kind": "contract",
|
|
"tags": [
|
|
"golden-screen",
|
|
"bulk",
|
|
"all-filtered",
|
|
"filter-snapshot",
|
|
"idempotency"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.oms.orders.bulk-ship"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.read",
|
|
"oms.order.ship"
|
|
],
|
|
"apiOperations": [
|
|
"oms.orders.search",
|
|
"oms.orders.ship"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "search",
|
|
"action": "api-query",
|
|
"operationId": "oms.orders.search"
|
|
},
|
|
{
|
|
"id": "all-filtered",
|
|
"action": "grid-select-all-filtered"
|
|
},
|
|
{
|
|
"id": "edit-without-search",
|
|
"action": "edit-search-form-without-execute"
|
|
},
|
|
{
|
|
"id": "ship",
|
|
"action": "api-command",
|
|
"operationId": "oms.orders.ship",
|
|
"idempotencyKeyRef": "idem.oms.bulk-ship.001"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"browser selection uses all-filtered plus excludedIds instead of materializing every id",
|
|
"bulk command uses the last applied search snapshot, not unexecuted form edits",
|
|
"server resolves filter targets and exclusions inside PostgreSQL",
|
|
"audit and outbox are created set-based for accepted orders",
|
|
"idempotency replay prevents duplicate state transition"
|
|
],
|
|
"evidence": [
|
|
"vitest-contract",
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"correlation-ids"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.oms.order-list.bulk-ship",
|
|
"title": "OMS 주문조회 대량 출고지시 부분성공",
|
|
"screenId": "OMS-ORD-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"bulk",
|
|
"idempotency",
|
|
"audit",
|
|
"outbox"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.oms.orders.bulk-ship"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.read",
|
|
"oms.order.ship"
|
|
],
|
|
"apiOperations": [
|
|
"oms.orders.search",
|
|
"oms.orders.ship"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "search",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F3"
|
|
],
|
|
"operationId": "oms.orders.search"
|
|
},
|
|
{
|
|
"id": "select",
|
|
"action": "select-fixtures",
|
|
"refs": [
|
|
"order.ship.ready.1",
|
|
"order.ship.ready.2",
|
|
"order.ship.rejected.1"
|
|
]
|
|
},
|
|
{
|
|
"id": "ship",
|
|
"action": "api-command",
|
|
"operationId": "oms.orders.ship",
|
|
"idempotencyKeyRef": "idem.oms.bulk-ship.001"
|
|
},
|
|
{
|
|
"id": "replay",
|
|
"action": "api-command",
|
|
"operationId": "oms.orders.ship",
|
|
"idempotencyKeyRef": "idem.oms.bulk-ship.001"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"requested=3",
|
|
"accepted=2",
|
|
"rejected=1",
|
|
"replay response equals first response",
|
|
"accepted orders version increments once",
|
|
"audit entries exist for accepted orders",
|
|
"outbox events exist once per accepted order"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot",
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"correlation-ids"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.oms.order-register.conflict",
|
|
"title": "OMS 주문수정 Optimistic Concurrency 충돌",
|
|
"screenId": "OMS-ORD-002",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"concurrency",
|
|
"conflict",
|
|
"recovery"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.oms.order-conflict"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.read",
|
|
"erp.item.read",
|
|
"oms.order.write"
|
|
],
|
|
"apiOperations": [
|
|
"oms.orders.update",
|
|
"oms.orders.get"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "actor-a-read",
|
|
"action": "fixture-read",
|
|
"fixtureRef": "order.conflict.1"
|
|
},
|
|
{
|
|
"id": "actor-b-update",
|
|
"action": "fixture-mutate-version",
|
|
"fixtureRef": "order.conflict.1"
|
|
},
|
|
{
|
|
"id": "actor-a-save",
|
|
"action": "expect-problem",
|
|
"problemType": "conflict",
|
|
"operationId": "oms.orders.update"
|
|
},
|
|
{
|
|
"id": "latest-read",
|
|
"action": "api-query",
|
|
"operationId": "oms.orders.get",
|
|
"fixtureRef": "order.conflict.1"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"stale write rejected",
|
|
"latest version remains authoritative",
|
|
"client input is not silently discarded",
|
|
"conflict evidence contains currentVersion",
|
|
"conflict resolver can reload latest record without discarding input before user action"
|
|
],
|
|
"evidence": [
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"problem-json"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.oms.order-register.fast-entry",
|
|
"title": "OMS 주문등록 Fast Entry Grid 대량입력 계약",
|
|
"screenId": "OMS-ORD-002",
|
|
"kind": "contract",
|
|
"tags": [
|
|
"golden-screen",
|
|
"fast-entry",
|
|
"paste",
|
|
"fill-down",
|
|
"inline-validation"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.oms.order-register"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.create",
|
|
"oms.order.read"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "ui-only",
|
|
"steps": [
|
|
{
|
|
"id": "paste",
|
|
"action": "grid-paste",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "fill-down",
|
|
"action": "grid-fill-down"
|
|
},
|
|
{
|
|
"id": "duplicate",
|
|
"action": "grid-row-duplicate"
|
|
},
|
|
{
|
|
"id": "error-nav",
|
|
"action": "grid-next-error"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"Excel-like numeric values normalize before validation",
|
|
"invalid pasted value remains visible and addressable by rowKey/field",
|
|
"Fill Down changes only editable cells in selected rows",
|
|
"row duplicate generates new client identity",
|
|
"first/next error moves focus without modal interruption"
|
|
],
|
|
"evidence": [
|
|
"vitest-contract",
|
|
"playwright-trace",
|
|
"component-catalog"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.oms.order-register.keyboard",
|
|
"title": "OMS 주문등록 키보드 중심 저장",
|
|
"screenId": "OMS-ORD-002",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"keyboard",
|
|
"lookup",
|
|
"validation",
|
|
"audit"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.oms.order-register"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.create",
|
|
"oms.order.read",
|
|
"erp.item.read"
|
|
],
|
|
"apiOperations": [
|
|
"lookup.customers.search",
|
|
"lookup.items.resolveByCode",
|
|
"lookup.warehouses.resolveByCode",
|
|
"oms.orders.register"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "customer",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F2",
|
|
"Enter"
|
|
],
|
|
"operationId": "lookup.customers.search"
|
|
},
|
|
{
|
|
"id": "warehouse",
|
|
"action": "lookup-resolve",
|
|
"operationId": "lookup.warehouses.resolveByCode",
|
|
"fixtureRef": "warehouse.seoul"
|
|
},
|
|
{
|
|
"id": "item",
|
|
"action": "lookup-resolve",
|
|
"operationId": "lookup.items.resolveByCode",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "save",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F8"
|
|
],
|
|
"operationId": "oms.orders.register"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"order persisted",
|
|
"order line uses resolved itemId",
|
|
"amount recalculated by server",
|
|
"audit created",
|
|
"outbox created when business handler requires it",
|
|
"focus flow contains no mouse-only step"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot",
|
|
"api-transcript",
|
|
"db-assertions"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.shell.unsaved-tab",
|
|
"title": "Workspace Tab 미저장 변경 보호",
|
|
"screenId": "OMS-ORD-002",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"shell",
|
|
"dirty-state",
|
|
"keyboard"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.shell.user"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.create",
|
|
"oms.order.read",
|
|
"erp.item.read"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "ui-only",
|
|
"steps": [
|
|
{
|
|
"id": "edit",
|
|
"action": "change-field",
|
|
"fixtureRef": "ui.order.receiver"
|
|
},
|
|
{
|
|
"id": "close",
|
|
"action": "close-workspace-tab"
|
|
},
|
|
{
|
|
"id": "keep",
|
|
"action": "choose-unsaved-action",
|
|
"choice": "continue-editing"
|
|
},
|
|
{
|
|
"id": "close-again",
|
|
"action": "close-workspace-tab"
|
|
},
|
|
{
|
|
"id": "discard",
|
|
"action": "choose-unsaved-action",
|
|
"choice": "discard"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"dirty tab shows indicator",
|
|
"first close does not lose input",
|
|
"discard closes tab",
|
|
"browser-native confirm is not used"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.wms.picking.idempotent-replay",
|
|
"title": "WMS 피킹 응답유실 후 동일 Scan Replay",
|
|
"screenId": "WMS-PICK-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"scanner",
|
|
"idempotency",
|
|
"network"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.wms.picking"
|
|
],
|
|
"requiredPermissions": [
|
|
"wms.picking.execute"
|
|
],
|
|
"apiOperations": [
|
|
"wms.picking.getTask",
|
|
"wms.picking.start",
|
|
"wms.picking.scan"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "get-task",
|
|
"action": "api-query",
|
|
"operationId": "wms.picking.getTask"
|
|
},
|
|
{
|
|
"id": "start",
|
|
"action": "api-command",
|
|
"operationId": "wms.picking.start"
|
|
},
|
|
{
|
|
"id": "location",
|
|
"action": "api-command",
|
|
"operationId": "wms.picking.scan",
|
|
"fixtureRef": "scan.location.a0302",
|
|
"idempotencyKeyRef": "idem.wms.location.001"
|
|
},
|
|
{
|
|
"id": "item-first",
|
|
"action": "api-command-drop-response",
|
|
"operationId": "wms.picking.scan",
|
|
"fixtureRef": "scan.item.shoe",
|
|
"idempotencyKeyRef": "idem.wms.item.001"
|
|
},
|
|
{
|
|
"id": "item-replay",
|
|
"action": "api-command",
|
|
"operationId": "wms.picking.scan",
|
|
"fixtureRef": "scan.item.shoe",
|
|
"idempotencyKeyRef": "idem.wms.item.001"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"pickedQty increments once",
|
|
"scan receipt unique by task and idempotency key",
|
|
"replay body equals committed response",
|
|
"success feedback is emitted only after server result"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"scanner-event-log"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.wms.picking.wrong-item",
|
|
"title": "WMS 다른 상품 Scan은 재시도 Queue 비대상",
|
|
"screenId": "WMS-PICK-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"scanner",
|
|
"business-error",
|
|
"retry-policy"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.wms.picking"
|
|
],
|
|
"requiredPermissions": [
|
|
"wms.picking.execute"
|
|
],
|
|
"apiOperations": [
|
|
"wms.picking.scan"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "location",
|
|
"action": "api-command",
|
|
"operationId": "wms.picking.scan",
|
|
"fixtureRef": "scan.location.a0302",
|
|
"idempotencyKeyRef": "idem.wms.location.002"
|
|
},
|
|
{
|
|
"id": "wrong-item",
|
|
"action": "api-command",
|
|
"operationId": "wms.picking.scan",
|
|
"fixtureRef": "scan.item.wrong",
|
|
"idempotencyKeyRef": "idem.wms.wrong.001"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"response is business rejection",
|
|
"pickedQty unchanged",
|
|
"client safe-retry queue remains empty",
|
|
"message states next corrective action"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"api-transcript",
|
|
"db-assertions"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.erp.item-master.lifecycle",
|
|
"title": "ERP 품목관리 CRUD·Concurrency·Audit Lifecycle",
|
|
"screenId": "ERP-MST-ITEM-001",
|
|
"kind": "integration",
|
|
"tags": [
|
|
"golden-screen",
|
|
"master-crud",
|
|
"concurrency",
|
|
"audit",
|
|
"state-policy"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.erp.item-master-lifecycle"
|
|
],
|
|
"requiredPermissions": [
|
|
"erp.item.read",
|
|
"erp.item.write"
|
|
],
|
|
"apiOperations": [
|
|
"erp.items.get",
|
|
"erp.items.update",
|
|
"erp.items.audit",
|
|
"erp.items.deactivate"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "read",
|
|
"action": "api-query",
|
|
"operationId": "erp.items.get",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "update",
|
|
"action": "api-command",
|
|
"operationId": "erp.items.update",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "audit",
|
|
"action": "api-query",
|
|
"operationId": "erp.items.audit",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "stale-write",
|
|
"action": "expect-problem",
|
|
"problemType": "conflict",
|
|
"operationId": "erp.items.update",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "deactivate",
|
|
"action": "api-command",
|
|
"operationId": "erp.items.deactivate",
|
|
"fixtureRef": "item.shoe.black270"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"update increments version exactly once",
|
|
"stale write is rejected without overwriting latest value",
|
|
"audit records actor and changed fields",
|
|
"deactivated item becomes read-only in UI policy",
|
|
"projection and write model expose the same active/version state"
|
|
],
|
|
"evidence": [
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"problem-json",
|
|
"screenshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.oms.order-register.lifecycle",
|
|
"title": "OMS 주문 작성→확정·읽기전용·Audit Lifecycle",
|
|
"screenId": "OMS-ORD-002",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"transaction",
|
|
"workflow",
|
|
"audit",
|
|
"state-policy"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.oms.order-conflict"
|
|
],
|
|
"requiredPermissions": [
|
|
"oms.order.create",
|
|
"oms.order.read",
|
|
"oms.order.confirm",
|
|
"erp.item.read"
|
|
],
|
|
"apiOperations": [
|
|
"oms.orders.get",
|
|
"oms.orders.confirm",
|
|
"oms.orders.audit"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "open-edit",
|
|
"action": "api-query",
|
|
"operationId": "oms.orders.get",
|
|
"fixtureRef": "order.conflict.1"
|
|
},
|
|
{
|
|
"id": "confirm",
|
|
"action": "api-command",
|
|
"operationId": "oms.orders.confirm",
|
|
"fixtureRef": "order.conflict.1",
|
|
"idempotencyKeyRef": "idem.oms.order-confirm.001"
|
|
},
|
|
{
|
|
"id": "audit",
|
|
"action": "api-query",
|
|
"operationId": "oms.orders.audit",
|
|
"fixtureRef": "order.conflict.1"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"DRAFT is presented as 작성",
|
|
"unsaved dirty state blocks confirm in command contract",
|
|
"confirm increments version and changes status to 확정",
|
|
"confirmed order is read-only by state policy",
|
|
"audit contains 작성→확정 status transition"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"screenshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.shell.home-navigation-security",
|
|
"title": "Home Workbench·권한필터·Scope/Route/Workspace 격리",
|
|
"screenId": "OMS-ORD-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"shell",
|
|
"home",
|
|
"navigation",
|
|
"security",
|
|
"preference",
|
|
"workspace"
|
|
],
|
|
"fixtureSets": [],
|
|
"requiredPermissions": [
|
|
"oms.order.read"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "ui-only",
|
|
"steps": [
|
|
{
|
|
"id": "scope-a",
|
|
"action": "set-preference-scope",
|
|
"value": "tenant-a:user-a"
|
|
},
|
|
{
|
|
"id": "open",
|
|
"action": "open-menu",
|
|
"screenId": "OMS-ORD-001"
|
|
},
|
|
{
|
|
"id": "home",
|
|
"action": "navigate-home"
|
|
},
|
|
{
|
|
"id": "resume",
|
|
"action": "resume-workspace-tab",
|
|
"screenId": "OMS-ORD-001"
|
|
},
|
|
{
|
|
"id": "search",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"Ctrl+K",
|
|
"ArrowDown",
|
|
"Enter"
|
|
]
|
|
},
|
|
{
|
|
"id": "scope-b",
|
|
"action": "set-preference-scope",
|
|
"value": "tenant-a:user-b"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"Home 바로 시작 Workbench represents open workspace with a 열림 source",
|
|
"menu search prioritizes favorites and recent safe screens",
|
|
"unauthorized/menuVisible=false screens are absent from home side navigation and menu search",
|
|
"recent navigation stores catalog path by default instead of dynamic full route",
|
|
"preference scope change does not expose previous user favorites or recents",
|
|
"missing preference scope uses session-only preference storage",
|
|
"scope change clears in-memory workspace tabs and returns the active route to Home",
|
|
"unsafe persisted route falls back to the current authorized catalog path",
|
|
"workspace max-tab policy evicts only the least-recent clean unpinned tab and never silently drops dirty work",
|
|
"permission changes remove unauthorized open tabs and close contextual utility/runtime panels",
|
|
"Home 바로 시작 deduplicates dirty/open/favorite/recent/catalog-priority sources by ScreenId"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot",
|
|
"local-storage-snapshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.kbx.template-runtime-utility",
|
|
"title": "T01~T09 Runtime State·Help/AI/제안 Utility 공통 계약",
|
|
"screenId": "OMS-ORD-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"template-contract",
|
|
"runtime-state",
|
|
"utility",
|
|
"accessibility",
|
|
"recovery"
|
|
],
|
|
"fixtureSets": [],
|
|
"requiredPermissions": [
|
|
"oms.order.read",
|
|
"common.ai.use",
|
|
"common.suggestion.create"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "ui-only",
|
|
"steps": [
|
|
{
|
|
"id": "open",
|
|
"action": "open-menu",
|
|
"screenId": "OMS-ORD-001"
|
|
},
|
|
{
|
|
"id": "refresh",
|
|
"action": "assert-template-state",
|
|
"states": [
|
|
"loading",
|
|
"refreshing",
|
|
"empty",
|
|
"error",
|
|
"ready"
|
|
]
|
|
},
|
|
{
|
|
"id": "help",
|
|
"action": "open-screen-utility",
|
|
"utility": "help"
|
|
},
|
|
{
|
|
"id": "ai",
|
|
"action": "open-screen-utility",
|
|
"utility": "ai"
|
|
},
|
|
{
|
|
"id": "suggestion",
|
|
"action": "open-screen-utility",
|
|
"utility": "suggestion"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"refreshing preserves the existing business context instead of clearing the workspace",
|
|
"empty and error states keep search and command surfaces available for recovery",
|
|
"Help AI and suggestion appear in the standard Page Header when permitted",
|
|
"utility availability follows the current permission set and closes after permission or scope change",
|
|
"WMS mobile exposes contextual Help without adding desktop-only chrome"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.common.design-system.form-composition",
|
|
"title": "Desktop Form 2-column·Full-span·Responsive 표준 계약",
|
|
"screenId": "COMMON-DS-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"template-contract",
|
|
"form",
|
|
"responsive",
|
|
"accessibility",
|
|
"design-token"
|
|
],
|
|
"fixtureSets": [],
|
|
"requiredPermissions": [],
|
|
"apiOperations": [],
|
|
"isolation": "ui-only",
|
|
"steps": [
|
|
{
|
|
"id": "open",
|
|
"action": "open-menu",
|
|
"screenId": "COMMON-DS-001"
|
|
},
|
|
{
|
|
"id": "desktop",
|
|
"action": "assert-viewport",
|
|
"width": 1440,
|
|
"height": 900
|
|
},
|
|
{
|
|
"id": "compact",
|
|
"action": "assert-viewport",
|
|
"width": 1024,
|
|
"height": 768
|
|
}
|
|
],
|
|
"assertions": [
|
|
"KbxFormSection delegates field layout to KbxFormGrid instead of embedding an ad-hoc grid",
|
|
"desktop default form composition uses two columns",
|
|
"KbxFormSpan full keeps address-like related fields across the available form width",
|
|
"compact viewport collapses the form composition to one column without changing source order",
|
|
"form spacing uses canonical design tokens"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.shell.module-route-recovery",
|
|
"title": "Global Module Selector·딥링크 복구·Navigation Capability 보안 계약",
|
|
"screenId": "OMS-ORD-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"shell",
|
|
"navigation",
|
|
"security",
|
|
"deep-link",
|
|
"accessibility"
|
|
],
|
|
"fixtureSets": [],
|
|
"requiredPermissions": [
|
|
"oms.order.read"
|
|
],
|
|
"apiOperations": [],
|
|
"isolation": "ui-only",
|
|
"steps": [
|
|
{
|
|
"id": "home",
|
|
"action": "navigate-home"
|
|
},
|
|
{
|
|
"id": "module",
|
|
"action": "select-module",
|
|
"value": "ERP"
|
|
},
|
|
{
|
|
"id": "bad-route",
|
|
"action": "navigate",
|
|
"path": "/unknown/deep/link"
|
|
},
|
|
{
|
|
"id": "recover",
|
|
"action": "open-menu-search"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"Global Header module selector and Side Navigation share the same focused module context",
|
|
"unknown routes render a standard recovery screen instead of a blank shell",
|
|
"persisted or requested workspace routes must match the registered ScreenId route pattern",
|
|
"cross-origin and route-pattern-mismatched paths fall back to Home or canonical navigation",
|
|
"profile action is propagated by the application shell instead of remaining a dead control",
|
|
"access denied does not disclose screen title or ScreenId unless explicitly allowed by policy"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.erp.item-master.keyboard-recovery",
|
|
"title": "ERP 품목 T02 조회·권한·Dirty·Conflict 복구",
|
|
"screenId": "ERP-MST-ITEM-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"master-crud",
|
|
"keyboard",
|
|
"permission",
|
|
"concurrency",
|
|
"audit",
|
|
"recovery"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.erp.item-master-lifecycle"
|
|
],
|
|
"requiredPermissions": [
|
|
"erp.item.read",
|
|
"erp.item.write"
|
|
],
|
|
"apiOperations": [
|
|
"erp.items.search",
|
|
"erp.items.get"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "open",
|
|
"action": "navigate-screen"
|
|
},
|
|
{
|
|
"id": "search",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F3"
|
|
],
|
|
"operationId": "erp.items.search"
|
|
},
|
|
{
|
|
"id": "select",
|
|
"action": "query-detail",
|
|
"operationId": "erp.items.get",
|
|
"fixtureRef": "item.shoe.black270"
|
|
},
|
|
{
|
|
"id": "dirty",
|
|
"action": "edit-field",
|
|
"field": "itemName"
|
|
},
|
|
{
|
|
"id": "conflict",
|
|
"action": "assert-conflict-recovery",
|
|
"problemType": "conflict"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"T02 master list and detail surfaces are both visible",
|
|
"save command is permission-aware and disabled when no editable change exists",
|
|
"dirty navigation requires explicit keep discard or save decision",
|
|
"conflict recovery offers latest-value reload instead of silent overwrite",
|
|
"audit remains accessible after recovery"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot",
|
|
"api-transcript",
|
|
"db-assertions",
|
|
"problem-json"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.common.operations.queue-recovery",
|
|
"title": "업무 예외 T06 Queue 조회·상세 Context·재시도 복구",
|
|
"screenId": "COMMON-OPS-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"queue",
|
|
"exception",
|
|
"recovery",
|
|
"context-retention",
|
|
"permission"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.common.operations"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.operations.read",
|
|
"common.operations.retry"
|
|
],
|
|
"apiOperations": [
|
|
"common.operations.search",
|
|
"common.operations.retry"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "search",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F3"
|
|
],
|
|
"operationId": "common.operations.search"
|
|
},
|
|
{
|
|
"id": "open-detail",
|
|
"action": "open-exception-detail",
|
|
"fixtureRef": "event.stock-shortage.v10"
|
|
},
|
|
{
|
|
"id": "retry",
|
|
"action": "invoke-work-item-action",
|
|
"operationId": "common.operations.retry",
|
|
"fixtureRef": "event.stock-shortage.v10"
|
|
},
|
|
{
|
|
"id": "requery",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F3"
|
|
],
|
|
"operationId": "common.operations.search"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"exception summary and queue content are visible without chart-only navigation",
|
|
"opening exception detail preserves queue search context",
|
|
"retry action is permission-aware and distinguishes retryable from terminal failure",
|
|
"requery keeps the user in the same queue context",
|
|
"normal items are not promoted ahead of actionable exceptions"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot",
|
|
"api-transcript",
|
|
"db-assertions"
|
|
]
|
|
},
|
|
{
|
|
"id": "scenario.common.reconcile.mismatch-recovery",
|
|
"title": "업무 대사 T07 불일치 필터·비교 Context·복구",
|
|
"screenId": "COMMON-REC-001",
|
|
"kind": "e2e",
|
|
"tags": [
|
|
"golden-screen",
|
|
"reconcile",
|
|
"mismatch-filter",
|
|
"eventual-consistency",
|
|
"recovery",
|
|
"context-retention"
|
|
],
|
|
"fixtureSets": [
|
|
"fixture.common.reconcile"
|
|
],
|
|
"requiredPermissions": [
|
|
"common.reconcile.read"
|
|
],
|
|
"apiOperations": [
|
|
"common.reconcile.search"
|
|
],
|
|
"isolation": "database-reset",
|
|
"steps": [
|
|
{
|
|
"id": "search",
|
|
"action": "keyboard",
|
|
"keys": [
|
|
"F3"
|
|
],
|
|
"operationId": "common.reconcile.search"
|
|
},
|
|
{
|
|
"id": "filter",
|
|
"action": "toggle-mismatch-only"
|
|
},
|
|
{
|
|
"id": "open-detail",
|
|
"action": "open-reconcile-detail",
|
|
"fixtureRef": "reconcile.oms-wms.pending"
|
|
},
|
|
{
|
|
"id": "recover",
|
|
"action": "retry-query",
|
|
"operationId": "common.reconcile.search"
|
|
}
|
|
],
|
|
"assertions": [
|
|
"expected actual difference and reason remain visible in one comparison context",
|
|
"mismatch-only filter does not discard the criteria context",
|
|
"pending within grace period is not presented as a confirmed mismatch",
|
|
"query recovery retains criteria and mismatch filter state"
|
|
],
|
|
"evidence": [
|
|
"playwright-trace",
|
|
"screenshot",
|
|
"api-transcript",
|
|
"db-assertions"
|
|
]
|
|
}
|
|
]
|
|
}
|