82 lines
8.4 KiB
JavaScript
82 lines
8.4 KiB
JavaScript
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')
|