feat(M09-D3): 完成商品寄存与安全取出闭环
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const root = process.cwd();
|
||||
const read = (path) => readFileSync(join(root, path), 'utf8');
|
||||
|
||||
for (const suffix of ['up', 'down', 'verify']) {
|
||||
assert.ok(
|
||||
existsSync(join(root,
|
||||
`database/migrations/2026081109_m09d3_product_storage.${suffix}.sql`)),
|
||||
`M09-D3 ${suffix} migration is missing`
|
||||
);
|
||||
}
|
||||
|
||||
const migration = read('database/migrations/2026081109_m09d3_product_storage.up.sql');
|
||||
for (const pattern of [
|
||||
'qipai_product_storage_records',
|
||||
'qipai_product_storage_items',
|
||||
'qipai_product_storage_events',
|
||||
'qipai_product_storage_event_items',
|
||||
'PARTIALLY_RETRIEVED',
|
||||
'claim_credential_hash',
|
||||
'remaining_quantity',
|
||||
'uq_qipai_product_storage_source_order',
|
||||
'PRODUCT_STORAGE_EVENT_IMMUTABLE',
|
||||
'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE',
|
||||
'goods.storage.read',
|
||||
'goods.storage.manage'
|
||||
]) assert.ok(migration.includes(pattern), `M09-D3 migration is missing ${pattern}`);
|
||||
|
||||
const service = read('backend/src/products/product-storage-service.ts');
|
||||
for (const pattern of [
|
||||
'ProductStorageService',
|
||||
'createFromOrder',
|
||||
'createManual',
|
||||
'rotateCredential',
|
||||
'expireDueForManagement',
|
||||
'timingSafeEqual',
|
||||
'FOR UPDATE',
|
||||
'FOR UPDATE SKIP LOCKED',
|
||||
'qipai_product_storage_event_items',
|
||||
'qipai_outbox_events',
|
||||
'qipai_audit_logs',
|
||||
'PRODUCT_STORAGE_SOURCE_ORDER_ALREADY_STORED'
|
||||
]) assert.ok(service.includes(pattern), `product storage service is missing ${pattern}`);
|
||||
assert.match(service, /createHash\('sha256'\)/);
|
||||
assert.match(service, /beginTransaction\(\)[\s\S]*commit\(\)[\s\S]*rollback\(\)/);
|
||||
assert.match(service, /claimCredential: null/);
|
||||
|
||||
const routes = read('backend/src/routes/product-storages.ts');
|
||||
for (const pattern of [
|
||||
'/app-api/product-storages',
|
||||
'/admin-api',
|
||||
'/app-api/management',
|
||||
'/credential/rotate',
|
||||
'/expire-due',
|
||||
'goods.storage.read',
|
||||
'goods.storage.manage'
|
||||
]) assert.ok(routes.includes(pattern), `product storage routes are missing ${pattern}`);
|
||||
|
||||
const app = read('backend/src/app.ts');
|
||||
const server = read('backend/src/server.ts');
|
||||
assert.match(app, /registerProductStorageRoutes/);
|
||||
assert.match(app, /productStorages\?: ProductStorageRouteOptions/);
|
||||
assert.match(server, /new ProductStorageService\(pool\)/);
|
||||
|
||||
const rbac = read('backend/src/auth/rbac-repository.ts');
|
||||
assert.match(rbac, /'goods\.storage\.read', 'goods\.storage\.manage'/);
|
||||
const packageJson = read('backend/package.json');
|
||||
assert.match(packageJson, /product-storage-service\.test\.mjs/);
|
||||
assert.match(packageJson, /product-storage-route\.test\.mjs/);
|
||||
|
||||
const mysqlTest = read('backend/tests/mysql-migration-roundtrip.test.mjs');
|
||||
for (const pattern of [
|
||||
'assertProductStorageLifecycle',
|
||||
'only one concurrent full retrieval may succeed',
|
||||
'PRODUCT_STORAGE_CREDENTIAL_INVALID',
|
||||
'PRODUCT_STORAGE_EVENT_IMMUTABLE',
|
||||
'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE',
|
||||
'expireDueForManagement'
|
||||
]) assert.ok(mysqlTest.includes(pattern), `live MySQL test is missing ${pattern}`);
|
||||
|
||||
console.log(
|
||||
'PASS: M09-D3 storage ownership, one-time credential, partial/concurrent retrieval and immutable-ledger gates are present.'
|
||||
);
|
||||
Reference in New Issue
Block a user