Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/contracts/testing/kbx.test-scenarios.json
T

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"
]
}
]
}