V13-FE-011: finalize search list layout slice

This commit is contained in:
2026-08-09 02:57:26 +09:00
parent 9efd202e76
commit 6422cb2b13
984 changed files with 120811 additions and 1498 deletions
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { isKbxProblem, kbxApiCatalog } from '@kbx/contracts'
describe('KBX API contract', () => {
it('keeps operation IDs and routes unique', () => {
const operations = Object.values(kbxApiCatalog)
expect(new Set(operations.map(x => x.id)).size).toBe(operations.length)
expect(new Set(operations.map(x => `${x.method} ${x.path}`)).size).toBe(operations.length)
})
it('requires a stable key only for mutation operations', () => {
const required = Object.values(kbxApiCatalog).filter(x => x.idempotency === 'required')
expect(required.length).toBeGreaterThan(0)
expect(required.every(x => x.method !== 'GET')).toBe(true)
})
it('recognizes the public problem discriminator family', () => {
for (const type of ['validation','business-rule','conflict','permission','not-found','integration','system']) {
expect(isKbxProblem({ type, title: 'test' })).toBe(true)
}
})
})
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import { isKbxBarcodeDuplicate, normalizeKbxBarcode } from '@kbx/ui'
describe('KbxBarcodeCapture contract', () => {
it('normalizes scanner text but never invents another barcode', () => {
expect(normalizeKbxBarcode(' 8801234567890 ', 'keyboard-wedge', 3, 256, 1)).toEqual({
rawValue: ' 8801234567890 ',
normalizedValue: '8801234567890',
source: 'keyboard-wedge',
occurredAt: 1,
})
})
it('rejects implausibly short and oversized scanner buffers', () => {
expect(normalizeKbxBarcode('A', 'keyboard-wedge', 3, 256, 1)).toBeNull()
expect(normalizeKbxBarcode('123456', 'keyboard-wedge', 3, 5, 1)).toBeNull()
})
it('ignores only the same barcode inside the short device debounce window', () => {
const last=normalizeKbxBarcode('ABC001','keyboard-wedge',3,256,1000)!
const duplicate=normalizeKbxBarcode('ABC001','keyboard-wedge',3,256,1100)!
const legitimateLaterScan=normalizeKbxBarcode('ABC001','keyboard-wedge',3,256,1400)!
expect(isKbxBarcodeDuplicate(duplicate,last,180)).toBe(true)
expect(isKbxBarcodeDuplicate(legitimateLaterScan,last,180)).toBe(false)
})
})
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import { kbxComponentCatalog, kbxComponentManifest } from '@kbx/ui'
const requiredCore=[
'KbxButton','KbxInput','KbxNumberField','KbxMoneyField','KbxQuantityField','KbxDateField','KbxDateRange','KbxSelect','KbxLookup','KbxCheckbox','KbxRadio','KbxTextarea','KbxTabs','KbxBadge','KbxTooltip','KbxBarcodeField',
'KbxPageHeader','KbxCommandBar','KbxSearchPanel','KbxFormSection','KbxSectionHeader','KbxDataGrid','KbxBulkActionBar','KbxQuickFilterBar','KbxSummaryBar','KbxStatus',
'KbxDialog','KbxDrawer','KbxToast','KbxConfirm','KbxExcelMenu','KbxExcelImport','KbxJobProgress','KbxAuditTrail','KbxHelpPanel','KbxAiPanel','KbxProposalPanel',
'KbxListPage','KbxMasterPage','KbxTransactionPage','KbxMasterDetailPage','KbxQueuePage','KbxReconcilePage','KbxWmsMobilePage',
]
describe('KBX source-standard core component contract',()=>{
it('publishes every v1 core component',()=>{
const names=new Set(kbxComponentManifest.map(x=>x.name))
for(const name of requiredCore) expect(names.has(name),name).toBe(true)
})
it('catalogs the highest-risk interactive components',()=>{
const catalogued=new Set(kbxComponentCatalog.map(x=>x.component))
for(const name of ['KbxInput','KbxLookup','KbxDataGrid','KbxCommandBar','KbxExcelImport','KbxWmsMobilePage','KbxApplicationShell']) expect(catalogued.has(name),name).toBe(true)
})
})
@@ -0,0 +1,6 @@
import { describe, expect, it } from 'vitest'
import { kbxExperiments, kbxFeatureFlags } from '../../packages/kbx-contracts/src/generated/experimentCatalog'
describe('KBX experiment contract',()=>{
it('ships reference experiment disabled by default',()=>{const exp=kbxExperiments['exp.oms.order-list.exception-summary-v2'];expect(exp.state).toBe('draft');expect(exp.rolloutPercent).toBe(0);expect(kbxFeatureFlags[exp.flagId].killSwitch).toBe(true)})
it('keeps variants balanced and has control',()=>{const variants=kbxExperiments['exp.oms.order-list.exception-summary-v2'].variants;expect(variants.reduce((n,x)=>n+x.weight,0)).toBe(100);expect(variants.some(x=>x.key==='control')).toBe(true)})
})
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { getKbxField, kbxImportField, kbxFieldCatalog } from '@kbx/contracts'
describe('KBX canonical field dictionary', () => {
it('keeps sensitive fields maskable', () => {
const sensitive = Object.values(kbxFieldCatalog).filter(field => field.sensitive)
expect(sensitive.length).toBeGreaterThan(0)
expect(sensitive.every(field => 'masking' in field && Boolean(field.masking))).toBe(true)
})
it('uses orderQty as the OMS order quantity field', () => {
expect(getKbxField('orderQty').dataType).toBe('quantity')
expect(kbxImportField('orderQty').required).toBe(true)
})
it('does not make server/domain rules part of metadata', () => {
const field = getKbxField('orderQty') as Record<string, unknown>
expect(field.allowedStatuses).toBeUndefined()
expect(field.stockPolicy).toBeUndefined()
})
})
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import type { KbxGridColumn, KbxSelectionState } from '@kbx/contracts'
import { normalizeKbxGridClipboardData, toKbxBulkSelectionRequest } from '@kbx/ui'
type Row={code:string;qty:number;price:number;date:string;active:boolean}
const columns:KbxGridColumn<Row>[]=[
{field:'code',header:'코드',type:'code'},
{field:'qty',header:'수량',type:'quantity'},
{field:'price',header:'단가',type:'money'},
{field:'date',header:'일자',type:'date'},
{field:'active',header:'사용',type:'boolean'},
]
describe('KBX grid productivity contract',()=>{
it('normalizes Excel-like pasted values before cell validation',()=>{
const {data,result}=normalizeKbxGridClipboardData([[' ABC001 ','1,200','3,500','20260808','Y']],columns,0)
expect(data[0]).toEqual(['ABC001',1200,3500,'2026-08-08',true])
expect(result.cellCount).toBe(5)
expect(result.rejectedCount).toBe(0)
})
it('keeps invalid pasted values visible for inline validation',()=>{
const {data,result}=normalizeKbxGridClipboardData([['ABC001','not-number']],columns,0)
expect(data[0][1]).toBe('not-number')
expect(result.issues[0]).toMatchObject({field:'qty',code:'INVALID_NUMBER'})
})
it('does not expand all-filtered selection into browser ids',()=>{
const state:KbxSelectionState<string>={mode:'all-filtered',selectedIds:[],excludedIds:['ORDER-9']}
expect(toKbxBulkSelectionRequest(state,{status:'READY'})).toEqual({mode:'filter',filter:{status:'READY'},excludedIds:['ORDER-9']})
})
})
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { buildKbxHomeAttention, buildKbxHomeAttentionQueue } from '@kbx/ui'
describe('KBX Home attention queue',()=>{
const entries=[{screenId:'OMS-ORD-001',path:'/oms/orders',section:'주문',title:'주문관리',module:'OMS' as const}]
it('prioritizes dirty, failed work, urgent notifications, running work, then ordinary notifications',()=>{
const tabs=[{key:'tab-1',screenId:'OMS-ORD-001',title:'주문관리',path:'/oms/orders',dirty:true,openedAt:'2026-08-08T00:00:00Z',lastActivatedAt:'2026-08-08T00:05:00Z'}]
const operations=[{id:'op-f',type:'import',title:'주문 반영',status:'failed' as const,requestedAt:'2026-08-08T00:00:00Z'},{id:'op-r',type:'export',title:'주문 내보내기',status:'running' as const,requestedAt:'2026-08-08T00:00:00Z'}]
const notifications=[{id:'n-w',severity:'warning' as const,title:'확인 필요',createdAt:'2026-08-08T00:00:00Z'},{id:'n-i',severity:'info' as const,title:'완료 안내',createdAt:'2026-08-08T00:00:00Z'}]
expect(buildKbxHomeAttention(entries,tabs,operations,notifications).map(item=>item.source)).toEqual(['dirty','operation-failed','notification-urgent','operation-running','notification'])
})
it('sorts newest-first within priority and reports overflow without hiding the real total',()=>{
const operations=[
{id:'old',type:'import',title:'이전 실패',status:'failed' as const,requestedAt:'2026-08-08T00:00:00Z',completedAt:'2026-08-08T00:10:00Z'},
{id:'new',type:'import',title:'최근 실패',status:'failed' as const,requestedAt:'2026-08-08T00:20:00Z',completedAt:'2026-08-08T00:30:00Z'},
]
const queue=buildKbxHomeAttentionQueue(entries,[],operations,[],1)
expect(queue.items.map(item=>item.title)).toEqual(['최근 실패'])
expect(queue.totalCount).toBe(2)
expect(queue.overflowCount).toBe(1)
expect(queue.sourceCounts['operation-failed']).toBe(2)
})
})
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest'
import type { KbxImportDefinition, KbxImportMapping } from '@kbx/contracts'
import { validateKbxImportFileCandidate, validateKbxImportMappings } from '@kbx/ui'
const definition:KbxImportDefinition={
id:'orders',screenId:'OMS-ORD-003',entity:'order',title:'주문 Excel 업로드',allowCreate:true,allowUpdate:true,maxFileSizeBytes:1048576,
fields:[
{key:'orderNo',label:'주문번호',dataType:'text',required:true,importable:true},
{key:'itemCode',label:'품목코드',dataType:'code',required:true,importable:true},
{key:'remark',label:'비고',dataType:'text',importable:true},
],
}
describe('KbxExcelImport guard contract',()=>{
it('rejects missing required targets and duplicate target mappings before server validation',()=>{
const mappings:KbxImportMapping[]=[
{sourceColumn:'주문번호',targetField:'orderNo',source:'exact'},
{sourceColumn:'상품번호',targetField:'orderNo',source:'manual'},
]
expect(validateKbxImportMappings(definition,mappings).map(x=>x.code)).toEqual(['duplicate-target','required-missing'])
})
it('accepts one-to-one required mappings',()=>{
const mappings:KbxImportMapping[]=[
{sourceColumn:'주문번호',targetField:'orderNo',source:'exact'},
{sourceColumn:'상품번호',targetField:'itemCode',source:'alias'},
]
expect(validateKbxImportMappings(definition,mappings)).toEqual([])
})
it('preflights xlsx extension and configured upload size without parsing business rows',()=>{
expect(validateKbxImportFileCandidate(definition,{name:'orders.csv',size:100})).toContain('.xlsx')
expect(validateKbxImportFileCandidate(definition,{name:'orders.xlsx',size:2097152})).toContain('1MB')
expect(validateKbxImportFileCandidate(definition,{name:'orders.xlsx',size:512})).toBeNull()
})
})
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import type { KbxLookupProvider } from '@kbx/contracts'
const provider: KbxLookupProvider<string> = {
async search(request) {
return { items: [{ id: '1', code: '10001', displayName: '대한상사' }], totalCount: 1 }
},
async resolveById(id) {
return id === '1' ? { id: '1', code: '10001', displayName: '대한상사' } : null
},
async resolveByCode(code) {
return code === '10001' ? { id: '1', code: '10001', displayName: '대한상사' } : null
},
}
describe('KbxLookup provider contract', () => {
it('never invents an entity: unknown code resolves null', async () => {
expect(await provider.resolveByCode('UNKNOWN')).toBeNull()
})
it('search returns stable domain id + code + displayName', async () => {
const result = await provider.search({ query: '대한', page: 1, pageSize: 30 })
expect(result.items[0]).toEqual(expect.objectContaining({ id: '1', code: '10001', displayName: '대한상사' }))
})
})
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest'
import type { KbxNavigationResolvedEntry } from '@kbx/contracts'
import { buildKbxHomeQuickStart, buildKbxHomeWorkbench, matchKbxRoutePattern, resolveKbxSafeRecentPath, resolveKbxSafeWorkspacePath } from '@kbx/ui'
const entry=(screenId:string,path:string,homePriority?:number):KbxNavigationResolvedEntry=>({screenId,path,title:screenId,module:'ERP',section:'업무',permissions:[],menuVisible:true,favoriteAllowed:true,recentPolicy:'screen',homePriority})
describe('KBX navigation hardening',()=>{
it('deduplicates home quick start in favorite, tab, recent, priority order',()=>{
const a=entry('A','/a',30),b=entry('B','/b',10),c=entry('C','/c',20)
const result=buildKbxHomeQuickStart([a,b,c],[a],[{screenId:'B',path:'/b',title:'B',visitedAt:'2026-08-08T10:00:00Z'}],[{key:'C:/c',screenId:'C',title:'C',path:'/c',openedAt:'2026-08-08T09:00:00Z',lastActivatedAt:'2026-08-08T11:00:00Z'}])
expect(result.map(x=>x.screenId)).toEqual(['A','C','B'])
})
it('never trusts a persisted route unless route mode and path boundary are both valid',()=>{
const base={...entry('A','/erp/items'),recentPolicy:'route' as const}
expect(resolveKbxSafeRecentPath(base,'/erp/items/123?tab=stock')).toBe('/erp/items/123')
expect(resolveKbxSafeRecentPath(base,'/erp/items-evil/123')).toBe('/erp/items')
expect(resolveKbxSafeRecentPath(base,'https://evil.example/x')).toBe('/erp/items')
expect(resolveKbxSafeRecentPath({...base,recentPolicy:'screen'},'/erp/items/123')).toBe('/erp/items')
})
it('keeps dirty/open work ahead of favorites and avoids duplicate home launch rows',()=>{
const a=entry('A','/a',30),b=entry('B','/b',10)
const result=buildKbxHomeWorkbench([a,b],[a],[],[
{key:'A:/a',screenId:'A',title:'A',path:'/a',openedAt:'2026-08-08T09:00:00Z',lastActivatedAt:'2026-08-08T11:00:00Z',dirty:true},
])
expect(result.filter(x=>x.screenId==='A')).toHaveLength(1)
expect(result[0]?.source).toBe('dirty')
})
it('validates workspace deep links against the registered route capability',()=>{
expect(matchKbxRoutePattern('/oms/orders/:orderId/edit','/oms/orders/123/edit')).toBe(true)
expect(matchKbxRoutePattern('/oms/orders/:orderId/edit','/oms/orders/123/delete')).toBe(false)
expect(resolveKbxSafeWorkspacePath(['/oms/orders/:orderId/edit'],'/oms/orders/123/edit','/home')).toBe('/oms/orders/123/edit')
expect(resolveKbxSafeWorkspacePath(['/oms/orders/:orderId/edit'],'https://evil.example/oms/orders/123/edit','/home')).toBe('/home')
})
it('rejects oversized and control-character workspace paths',()=>{
const fallback='/home'
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],`/erp/items/${'a'.repeat(2100)}`,fallback)).toBe(fallback)
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],'/erp/items/abc\n123',fallback)).toBe(fallback)
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],'/erp/items/%2e%2e',fallback)).toBe(fallback)
})
})
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest'
import type { KbxWorkItem } from '@kbx/contracts'
function nextActions(item: KbxWorkItem) {
return new Set((item.actions ?? []).map(x => x.kind))
}
describe('KBX operations contract', () => {
it('domain exception can navigate without offering unsafe manual resolution', () => {
const item: KbxWorkItem = {
id: '1', sourceModule: 'WMS', sourceType: 'picking-task', sourceId: 'uuid-p100', referenceNo: 'P-100', sourceScreenId: 'WMS-PICK-001', code: 'PICKING_SHORTAGE',
title: '피킹 수량 부족', severity: 'warning', status: 'open', occurredAt: new Date().toISOString(), ageMinutes: 3, version: 1,
actions: [{ id: 'navigate', label: '원 업무 보기', kind: 'navigate' }],
}
expect(nextActions(item).has('navigate')).toBe(true)
expect(nextActions(item).has('resolve')).toBe(false)
})
it('retry is explicit rather than inferred from the error text', () => {
const item: KbxWorkItem = {
id: '2', sourceModule: 'OMS', sourceType: 'shipment', sourceId: 'uuid-s1', referenceNo: 'S-1', sourceScreenId: 'OMS-ORD-001', code: 'LABEL_FAILED',
title: '송장 발급 실패', severity: 'warning', status: 'claimed', occurredAt: new Date().toISOString(), ageMinutes: 1, version: 2,
actions: [{ id: 'retry', label: '재처리', kind: 'retry' }],
}
expect(nextActions(item)).toEqual(new Set(['retry']))
})
})
@@ -0,0 +1,8 @@
import { describe,it,expect } from 'vitest'
import { kbxFieldReadonly, resolveKbxRecordStatePolicy, type KbxRecordStatePolicy } from '@kbx/contracts'
const policies:KbxRecordStatePolicy[]=[{status:'작성',editability:'editable'},{status:'확정',editability:'readonly'},{status:'부분수정',editability:'restricted',editableFields:['remark']}]
describe('record lifecycle policy',()=>{
it('makes confirmed records read-only',()=>expect(kbxFieldReadonly(resolveKbxRecordStatePolicy(policies,'확정'),'receiverName')).toBe(true))
it('allows only explicitly editable fields in restricted state',()=>{const p=resolveKbxRecordStatePolicy(policies,'부분수정');expect(kbxFieldReadonly(p,'remark')).toBe(false);expect(kbxFieldReadonly(p,'quantity')).toBe(true)})
it('fails safe for unknown states',()=>expect(resolveKbxRecordStatePolicy(policies,'UNKNOWN').editability).toBe('readonly'))
})
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import type { KbxConflictSnapshot, KbxOperationRun, KbxRuntimeNotice } from '@kbx/contracts'
describe('KBX runtime contract', () => {
it('distinguishes degraded mode from normal business state', () => {
const notice: KbxRuntimeNotice = { mode: 'degraded', title: '택배 연계 지연', retryAllowed: true }
expect(notice.mode).toBe('degraded')
})
it('keeps concurrency conflict explicit instead of silent overwrite', () => {
const conflict: KbxConflictSnapshot = {
code: 'ORDER_VERSION_CONFLICT', title: '다른 사용자가 변경했습니다.', requestedVersion: 3, currentVersion: 4,
changes: [{ field: 'quantity', label: '수량', mine: 10, latest: 8 }],
}
expect(conflict.currentVersion).toBeGreaterThan(conflict.requestedVersion!)
})
it('can represent partial completion without reporting full success', () => {
const run: KbxOperationRun = {
id: 'op-1', type: 'bulk-ship', title: '대량 출고지시', status: 'partially-completed', requestedAt: new Date().toISOString(),
total: 100, processed: 100, succeeded: 97, failed: 3,
}
expect(run.status).toBe('partially-completed')
expect(run.failed).toBe(3)
})
})
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { kbxScreenRecipeCatalog, kbxScreenRecipeVerificationCatalog } from '@kbx/contracts'
import { kbxTemplateManifest } from '@kbx/ui'
describe('KBX screen recipes',()=>{
it('closes every T01-T09 template to an implementation and verification recipe',()=>{
expect(Object.keys(kbxScreenRecipeCatalog)).toHaveLength(9)
for(const template of kbxTemplateManifest){
const recipe=kbxScreenRecipeCatalog[template.code]
const verification=kbxScreenRecipeVerificationCatalog[template.code]
expect(recipe.type).toBe(template.type)
expect(recipe.templateComponent).toBe(template.component)
expect(recipe.requiredPolicies.length).toBeGreaterThan(0)
expect(recipe.recoveryPolicies.length).toBeGreaterThan(0)
expect(recipe.securityPolicies.length).toBeGreaterThan(0)
expect(recipe.canonicalScenarioIds.length).toBeGreaterThan(0)
expect(recipe.testProfile.requiredChecks.length).toBeGreaterThan(0)
expect(recipe.testProfile.requiredScenarioKinds).toContain('e2e')
expect(verification.complete).toBe(true)
expect(verification.missingScenarioKinds).toEqual([])
expect(verification.missingTags).toEqual([])
expect(verification.missingEvidence).toEqual([])
}
})
})
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import { getKbxShortcutDebugSnapshot, registerKbxShortcuts } from '@kbx/ui'
describe('KBX shortcut manager contract',()=>{
it('rejects protected browser shortcuts',()=>{
for(const key of ['F5','Ctrl+L','Ctrl+T','Ctrl+W','Ctrl+R']){
expect(()=>registerKbxShortcuts([{key,scope:'application',execute(){}}])).toThrow(/protected/)
}
})
it('registers semantic scope without page-level window ownership',()=>{
const unregister=registerKbxShortcuts([{key:'F8',scope:'page',priority:10,execute(){}}])
expect(getKbxShortcutDebugSnapshot()).toEqual(expect.arrayContaining([expect.objectContaining({key:'F8',scope:'page',priority:10})]))
unregister()
})
})
@@ -0,0 +1,8 @@
import { describe,expect,it } from 'vitest'
import { kbxTelemetryCatalog,kbxUxMetricCatalog } from '@kbx/contracts'
describe('KBX telemetry contract',()=>{
it('contains Manual Intervention Rate',()=>expect(kbxUxMetricCatalog.manual_intervention_rate).toBeTruthy())
it('never allows raw identity attributes',()=>{for(const event of Object.values(kbxTelemetryCatalog)){expect(event.allowedAttributes.join('|')).not.toMatch(/phone|address|orderNo|customer|barcode|keyword|query|entityId/i)}})
it('requires duration for completion metrics',()=>expect(kbxTelemetryCatalog['task.complete'].requiresDuration).toBe(true))
})
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { kbxTemplateManifest } from '@kbx/ui'
describe('KBX template state matrix',()=>{
it('requires explicit idle/runtime/permission states where applicable',()=>{
const byCode=new Map(kbxTemplateManifest.map(item=>[item.code,item]))
expect(byCode.get('T01')?.runtimeStates).toContain('idle')
for(const template of kbxTemplateManifest)expect(template.stateCapabilities).toContain('permission')
expect(byCode.get('T02')?.stateCapabilities).toEqual(expect.arrayContaining(['dirty','conflict']))
expect(byCode.get('T03')?.stateCapabilities).toEqual(expect.arrayContaining(['dirty','conflict','validation']))
expect(byCode.get('T04')?.stateCapabilities).toContain('validation')
expect(byCode.get('T08')?.stateCapabilities).toContain('job')
expect(byCode.get('T09')?.stateCapabilities).toContain('network')
})
})
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import {
kbxTestScenarioCatalog,
kbxTestFixtureSetCatalog,
kbxTestFixtureRefs,
kbxTestReferenceClock,
} from '@kbx/contracts'
describe('KBX canonical test scenario contract', () => {
it('covers the three golden screens', () => {
const screens = new Set(Object.values(kbxTestScenarioCatalog).map(x => x.screenId))
expect(screens.has('OMS-ORD-001')).toBe(true)
expect(screens.has('OMS-ORD-002')).toBe(true)
expect(screens.has('WMS-PICK-001')).toBe(true)
})
it('keeps deterministic synthetic references', () => {
expect(kbxTestReferenceClock).toBe('2026-08-08T09:00:00Z')
expect(Object.keys(kbxTestFixtureSetCatalog).length).toBeGreaterThan(0)
expect(kbxTestFixtureRefs['order.ship.ready.1'].orderNo).toMatch(/^TEST-/)
})
})
@@ -0,0 +1,5 @@
import { describe,it,expect } from 'vitest'
import { allowedKbxTransitions, type KbxWorkflowDefinition } from '@kbx/contracts'
const wf:KbxWorkflowDefinition={id:'x',version:'1',states:[{value:'DRAFT',label:'작성',semantic:'draft'},{value:'DONE',label:'완료',semantic:'completed'}],transitions:[{id:'done',from:['DRAFT'],to:'DONE',label:'완료'}]}
describe('workflow contract',()=>{it('returns only transitions allowed from current state',()=>expect(allowedKbxTransitions(wf,'DRAFT').map(x=>x.id)).toEqual(['done']));it('returns none from terminal-like state',()=>expect(allowedKbxTransitions(wf,'DONE')).toHaveLength(0))})
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest'
import { searchNavigationEntries } from '../../packages/kbx-ui/src/shell/navigationSearch'
import type { KbxNavigationResolvedEntry } from '@kbx/contracts'
const entries:KbxNavigationResolvedEntry[]=[
{screenId:'OMS-ORD-001',title:'주문관리',module:'OMS',section:'주문',path:'/oms/orders',keywords:['출고대기','주문조회']},
{screenId:'ERP-INV-001',title:'재고현황',module:'ERP',section:'재고',path:'/erp/inventory',keywords:['현재고','가용재고']},
]
describe('navigation search',()=>{
it('finds by Korean business keyword',()=>expect(searchNavigationEntries(entries,'출고')[0]?.screenId).toBe('OMS-ORD-001'))
it('finds by screen id',()=>expect(searchNavigationEntries(entries,'ERP-INV-001')[0]?.screenId).toBe('ERP-INV-001'))
it('finds familiar menu title without command syntax',()=>expect(searchNavigationEntries(entries,'주문관리')[0]?.title).toBe('주문관리'))
})
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest'
import type { KbxWmsScanCommand } from '@kbx/contracts'
function replayOrder(commands: KbxWmsScanCommand[]) {
// Scanner commands are sequence-sensitive and must never be parallelized.
return [...commands].sort((a, b) => a.occurredAt.localeCompare(b.occurredAt))
}
describe('WMS scanner retry contract', () => {
it('preserves the same idempotency key across transport retry', () => {
const command: KbxWmsScanCommand = {
taskId: 'task-1',
barcode: 'LOC-A-01',
source: 'keyboard-wedge',
idempotencyKey: 'task-1:abc',
expectedVersion: 7,
occurredAt: '2026-08-08T07:00:00Z',
}
const retried = { ...command }
expect(retried.idempotencyKey).toBe(command.idempotencyKey)
})
it('replays sequential scans in occurrence order', () => {
const make = (key: string, occurredAt: string): KbxWmsScanCommand => ({
taskId: 'task-1', barcode: key, source: 'keyboard-wedge', idempotencyKey: key,
expectedVersion: 7, occurredAt,
})
expect(replayOrder([
make('B', '2026-08-08T07:00:02Z'),
make('A', '2026-08-08T07:00:01Z'),
]).map(x => x.idempotencyKey)).toEqual(['A', 'B'])
})
})