Files
gongxue-base/scripts/taro-api-compatibility-contract-test.js
2026-07-12 19:26:57 +08:00

53 lines
4.0 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
const read = file => fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
const httpSource = read('apps/api/src/core/http.ts');
const serverSource = read('apps/api/src/server.ts');
const apiSource = read('apps/taro/src/services/api.ts');
const typesSource = read('apps/taro/src/types.ts');
const tenantLocatorSource = read('apps/api/src/features/tenant/locator.ts');
const tenantResolutionSource = read('apps/taro/src/app/tenant-resolution.ts');
const studentRouteSource = read('apps/api/src/features/tenant-admin/classes.ts');
const studentCursorSource = read('apps/api/src/features/tenant-admin/student-cursor.ts');
assert.match(httpSource, /meta:\s*\{ \.\.\.existingMeta, requestId \}/, 'API responses must expose requestId without discarding endpoint metadata');
assert.match(serverSource, /sendJson\(res, 200, withResponseMeta\(result, requestId\)\)/, 'Successful API responses must carry requestId metadata');
assert.match(serverSource, /withResponseMeta\(\{ \.\.\.body, requestId \}, requestId\)/, 'Error API responses must carry the same requestId in the legacy field and metadata envelope');
assert.match(apiSource, /responseHeaderValue\(response\.header, 'x-request-id'\)/, 'The Taro API client must fall back to the response header requestId');
assert.match(apiSource, /this\.requestId = payload\.requestId/, 'ApiError must retain requestId for support and observability');
assert.doesNotMatch(typesSource, /\[key:\s*string\]:\s*unknown/, 'The shared API envelope must not silently accept arbitrary response fields');
assert.match(typesSource, /interface ApiResponseMeta[\s\S]*requestId:\s*string/, 'The Taro response envelope must type requestId metadata');
assert.match(tenantLocatorSource, /TENANT_HOST_CONFLICT/, 'Tenant host conflicts must fail closed');
assert.match(tenantLocatorSource, /TENANT_LOCATOR_REQUIRED/, 'Tenant resolution must reject a missing locator');
assert.match(tenantResolutionSource, /tenantCode:\s*!host \|\| isLocalRuntimeHost\(host\)/, 'Production H5 host resolution must suppress tenantCode overrides');
assert.ok(
studentRouteSource.includes('(tm.created_at, tm.id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)'),
'Deep student pages must use a composite keyset cursor',
);
assert.ok(
studentRouteSource.includes('order by tm.created_at desc, tm.id desc')
&& studentRouteSource.includes('limit $${params.length}'),
'Student pagination must keep stable ordering and a bounded limit',
);
assert.match(studentRouteSource, /const hasMore = rows\.length > limit/, 'List responses must derive hasMore from a limit+1 query');
assert.match(studentRouteSource, /nextCursor = hasMore && lastItem/, 'List responses must only issue a next cursor when another page exists');
assert.match(studentCursorSource, /parsed\.version !== 1/, 'Opaque cursors must be versioned and fail closed');
const statusContracts = [
['order', "('pending', 'paid', 'failed', 'closed', 'refunded')", read('supabase/migrations/202606210001_core_multitenant_schema.sql')],
['refund', "('requested', 'approved', 'processing', 'succeeded', 'failed', 'rejected', 'cancelled')", read('supabase/migrations/202606290011_commerce_refunds.sql')],
['content import', "('preview', 'pending', 'importing', 'completed', 'completed_with_errors', 'failed', 'rejected')", read('supabase/migrations/202606210007_content_import_assets.sql')],
['CRM queue', "('pending', 'processing', 'retrying', 'sent', 'failed', 'discarded')", read('supabase/migrations/202606290010_crm_worker_hardening.sql')],
['commission settlement', "('draft', 'pending_review', 'approved', 'paid', 'rejected', 'cancelled')", read('supabase/migrations/202606290009_commission_settlements.sql')],
];
for (const [name, values, source] of statusContracts) {
assert.ok(source.includes(values), `${name} state values are part of the frontend compatibility boundary`);
}
console.log('[PASS] Taro API response, tenant resolution, pagination and state-machine compatibility contract');