Files
KArtSell.Aegis/docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/scripts/validate-fe-authoritative-outcome-v57.mjs
T
kjh2064 3f293d8aa8
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s
V13-FE-006: consolidate approved UI and contract hardening
2026-08-13 02:41:00 +09:00

82 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs'
const read=file=>fs.readFileSync(file,'utf8')
const expect=(condition,message)=>{if(!condition){console.error(`FAIL: ${message}`);process.exit(1)}console.log(`PASS: ${message}`)}
function versionAtLeast(source,min){
const found=source.match(/version\s*:\s*['"](\d+)\.(\d+)\.(\d+)['"]/);if(!found)return false
const a=found.slice(1).map(Number),b=min.split('.').map(Number)
return a[0]>b[0]||(a[0]===b[0]&&(a[1]>b[1]||(a[1]===b[1]&&a[2]>=b[2])))
}
const app=read('apps/web/src/App.vue')
expect(/app-version="v\d+-fe-[^"]+"/.test(app),'Runtime metadata remains explicitly versioned for operational reproduction')
const operationsApi=read('apps/web/src/modules/common/operations/operationsApi.ts')
const operations=read('apps/web/src/modules/common/operations/OperationsQueuePage.vue')
expect(operationsApi.includes('ResolveWorkItemsResponse')&&operationsApi.includes('resolvedCount')&&operationsApi.includes('rejectedCount'),'T06 API preserves authoritative resolve counts')
expect(operations.includes("kind:'resolve'")&&operations.includes('succeeded:result.resolvedCount')&&operations.includes('skipped:result.rejectedCount'),'T06 maps HTTP success to authoritative business outcome instead of all-success')
expect(operations.includes('if(result.resolvedCount>0)')&&operations.includes("kbxTelemetry.track('exception.resolved'"),'T06 resolved telemetry is emitted only for actual server-resolved work')
expect(operations.includes('data-kbx-surface="authoritative-action-receipt"')&&operations.includes('actionReceiptEl.value?.focus'),'T06 persists and focuses the authoritative action receipt')
expect(operations.includes('해결 처리가 완료되지 않았습니다.')&&operations.includes('최신 상태 조회'),'T06 partial resolve gives explicit recovery guidance')
const reconcileApi=read('apps/web/src/modules/common/reconcile/reconcileApi.ts')
const reconcile=read('apps/web/src/modules/common/reconcile/ReconcilePage.vue')
expect(reconcileApi.includes('CreateReconcileExceptionsResponse')&&reconcileApi.includes('createdOrUpdatedCount')&&reconcileApi.includes('skippedCount'),'T07 API preserves authoritative exception-creation counts')
expect(reconcile.includes('created:result.createdOrUpdatedCount')&&reconcile.includes('skipped:result.skippedCount'),'T07 consumes server result counts without client inference')
expect(reconcile.includes('data-kbx-surface="reconcile-authoritative-receipt"')&&reconcile.includes('exceptionReceiptEl.value?.focus'),'T07 partial result persists as a focusable recovery receipt')
expect(reconcile.includes("router.push('/operations/exceptions')")&&reconcile.includes('원천값을 화면에서 직접 덮어쓰지 않습니다.'),'T07 routes to resolution work without mutating source truth')
const demo=read('apps/web/src/http/demo/installFrontendDemoApi.ts')
for(const marker of ['sourceModule','referenceNo','ageMinutes','allowManualResolution','expectedValue','actualValue','differenceValue','summary'])expect(demo.includes(marker),`Demo API carries real T06/T07 contract field: ${marker}`)
expect(demo.includes('createdOrUpdatedCount')&&demo.includes('skippedCount'),'Demo Reconcile mirrors authoritative create-exception outcome')
expect(demo.includes("item.status='resolved';item.version++"),'Demo resolve advances server-like version when authoritative state changes')
const picking=read('apps/web/src/modules/wms/picking/useWmsPicking.ts')
expect(picking.includes('type ScopedScanMutation')&&picking.includes('scope: string'),'T09 scan mutation captures issuing tenant/user scope')
expect(picking.includes('removePickingScan(input.command.idempotencyKey, input.scope)')&&picking.includes('queuePickingScan(input.command, input.scope)'),'T09 success/ambiguity always reads or writes the captured issuing scope')
expect(picking.includes('if (input.scope !== retryScope.value) return'),'T09 stale in-flight response cannot mutate a newly switched operator UI')
expect(picking.includes('const scope = retryScope.value')&&picking.includes('removePickingScan(item.command.idempotencyKey, scope)'),'T09 retry replay is pinned to its captured scope for the full async loop')
expect(picking.includes("taskTelemetry?.abandon('scope_changed')")&&picking.includes('lastConfirmedScan.value = null'),'T09 operator scope switch abandons prior task truth and clears prior server-confirmation display')
const pkg=JSON.parse(read('package.json'))
expect(/^pnpm@\d+\.\d+\.\d+$/.test(pkg.packageManager),'Root packageManager uses an exact Corepack-compatible pnpm semver')
expect(Boolean(pkg.devDependencies?.['@playwright/test'])&&Boolean(pkg.devDependencies?.vitest),'Root declares actual Playwright and Vitest test runners')
for(const script of ['test:unit','test:e2e','test:e2e:desktop','test:e2e:wms','test:fe','validate:fe-authoritative-outcome'])expect(Boolean(pkg.scripts?.[script]),`Root exposes executable FE QA script: ${script}`)
const playwright=read('playwright.config.ts')
expect(playwright.includes("baseURL")&&playwright.includes("command: 'pnpm dev:web:demo'"),'Playwright owns baseURL and starts the demo runtime when no external URL is supplied')
expect(playwright.includes('width: 1440, height: 900')&&playwright.includes('desktop-chromium'),'Playwright defines the KBX 1440×900 desktop evidence project')
expect(playwright.includes('width: 390, height: 844')&&playwright.includes('wms-mobile-chromium'),'Playwright defines the KBX 390×844 WMS evidence project')
expect(playwright.includes("trace: 'retain-on-failure'")&&playwright.includes("screenshot: 'only-on-failure'")&&playwright.includes("video: 'retain-on-failure'"),'Playwright retains failure evidence instead of ceremonial pass/fail only')
const opsE2e=read('tests/e2e/common-operations.spec.ts')
expect(opsE2e.includes('resolvedCount: 0, rejectedCount: 1')&&opsE2e.includes('authoritative-action-receipt')&&opsE2e.includes('toBeFocused()'),'T06 E2E proves HTTP 200 + business rejection and focusable recovery receipt')
expect(opsE2e.includes('다음 긴급건')&&opsE2e.includes('출고 연계 지연'),'T06 E2E proves corrected demo DTO drives urgent work navigation')
const recE2e=read('tests/e2e/common-reconcile.spec.ts')
expect(recE2e.includes('createdOrUpdatedCount: 0, skippedCount: 1')&&recE2e.includes('reconcile-authoritative-receipt')&&recE2e.includes('toBeFocused()'),'T07 E2E proves HTTP 200 + skipped business outcome and recovery focus')
expect(recE2e.includes('원천값을 화면에서 직접 덮어쓰지 않습니다'),'T07 E2E protects the source-truth policy in the actual UI flow')
const wmsE2e=read('tests/e2e/wms-picking.spec.ts')
expect(wmsE2e.includes("route.abort('failed')")&&wmsE2e.includes('scanBodies.length).toBe(2)'),'T09 E2E models response loss followed by one safe retry')
expect(wmsE2e.includes('scanBodies[1]?.idempotencyKey).toBe(scanBodies[0]?.idempotencyKey)'),'T09 E2E asserts the exact idempotency key is reused across ambiguous retry')
expect(wmsE2e.includes('아직 확인된 스캔 없음')&&wmsE2e.includes('재전송된 스캔을 서버에서 확인했습니다.'),'T09 E2E separates local scan from server-confirmed truth')
const qualityGate=read('.gitea/workflows/kbx-quality-gate.yaml')
const releaseGate=read('.gitea/workflows/kbx-release-readiness.yaml')
for(const marker of ['pnpm typecheck:web','pnpm test:unit','pnpm build:web:demo','pnpm test:e2e'])expect(qualityGate.includes(marker),`Quality Gate executes runtime FE evidence step when reproducible install is available: ${marker}`)
expect(qualityGate.includes('report-fe-runtime-readiness.mjs')&&qualityGate.includes('not reported as PASS'),'Quality Gate reports missing lockfile as blocked runtime evidence instead of fake PASS')
expect(releaseGate.includes('Frontend runtime evidence is BLOCKED')&&releaseGate.includes('exit 1'),'Release Readiness fails closed when reproducible FE runtime evidence cannot be installed')
const reference=read('apps/web/fe-reference/index.html')
expect(/KBX FE Reference Lab v\d+/.test(reference)&&reference.includes('Runtime Evidence'),'Static HTML reference remains explicitly versioned and identifies runtime-evidence intent')
for(const [file,minimum] of [
['apps/web/src/modules/common/operations/operations.definition.ts','1.2.0'],
['apps/web/src/modules/common/reconcile/reconcile.definition.ts','1.2.0'],
['apps/web/src/modules/wms/picking/picking.definition.ts','1.2.0'],
])expect(versionAtLeast(read(file),minimum),`${file} versions the v57 behavior contract at ${minimum} or later`)
console.log('PASS: FE authoritative outcome + runtime evidence hardening v57 contract')