Move the Hub channel table, organization tags and device capability definitions into the workspace packages so the control plane and the console validate the same contract, and refresh the frozen upstream evidence for the new paths.
624 lines
24 KiB
TypeScript
624 lines
24 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { upstream58e2204Operations } from '../src/upstream-58e2204.ts';
|
|
|
|
const fixturePath = fileURLToPath(new URL('./fixtures/main-routes-58e2204.json', import.meta.url));
|
|
const fixture = JSON.parse(await readFile(fixturePath, 'utf8'));
|
|
const key = (operation: { method: string; path?: string; pathTemplate?: string }) =>
|
|
`${operation.method} ${operation.pathTemplate ?? operation.path}`;
|
|
const evidenceFixturePath = fileURLToPath(
|
|
new URL('./fixtures/source-evidence-58e2204.json', import.meta.url),
|
|
);
|
|
const evidenceFixture = JSON.parse(await readFile(evidenceFixturePath, 'utf8'));
|
|
const snapshotRoot = fileURLToPath(new URL('./fixtures/upstream-58e2204/', import.meta.url));
|
|
const snapshotManifest = JSON.parse(await readFile(`${snapshotRoot}/manifest.json`, 'utf8'));
|
|
const requiredFields = [
|
|
'operationId',
|
|
'upstreamDomain',
|
|
'method',
|
|
'pathTemplate',
|
|
'handler',
|
|
'capability',
|
|
'riskLevel',
|
|
'requestSchemaEvidence',
|
|
'responseSchemaEvidence',
|
|
'requestContentType',
|
|
'timeoutMs',
|
|
'idempotency',
|
|
'confirmationPolicy',
|
|
'batchable',
|
|
'syncSafe',
|
|
'sensitiveFields',
|
|
'auditedAtCommit',
|
|
'compatibilityAdapter',
|
|
'uiOwner',
|
|
'sourceEvidence',
|
|
'responseFixtureStatus',
|
|
] as const;
|
|
|
|
const expectedDomains = new Set([
|
|
'instances-auth',
|
|
'device-system',
|
|
'sim',
|
|
'cellular',
|
|
'radio-lock',
|
|
'data-connection',
|
|
'device-network',
|
|
'workmode-esim',
|
|
'messages',
|
|
'calls',
|
|
'notifications',
|
|
'automation',
|
|
'ota',
|
|
]);
|
|
const expectedOwners = new Set([
|
|
'instances-new',
|
|
'settings-instance',
|
|
'fleet',
|
|
'overview',
|
|
'cellular',
|
|
'device-network',
|
|
'messages',
|
|
'calls',
|
|
'esim',
|
|
'notifications',
|
|
'automation',
|
|
'ota',
|
|
]);
|
|
const operationKey = (operation: { method: string; pathTemplate: string; handler: string }) =>
|
|
`${operation.method} ${operation.pathTemplate} ${operation.handler}`;
|
|
|
|
test('sensitive fields are independently snapshot-rebuildable model-chain anchors', async () => {
|
|
const directionClaim = (operation: any, direction: string) =>
|
|
['request', 'path', 'query'].includes(direction)
|
|
? operation.requestEvidence
|
|
: operation.responseEvidence;
|
|
for (const operation of upstream58e2204Operations as any[])
|
|
for (const field of operation.sensitiveFields) {
|
|
const evidence = field.evidence;
|
|
assert.ok(evidence, `${operation.operationId}: ${field.path} missing evidence`);
|
|
assert.match(evidence.sourceSha256, /^[a-f0-9]{64}$/);
|
|
assert.ok(
|
|
evidence.sourceFile &&
|
|
evidence.symbol &&
|
|
(evidence.fieldPath || evidence.modelField || evidence.dynamicKey),
|
|
);
|
|
const source = await readFile(`${snapshotRoot}/${evidence.sourceFile}`, 'utf8');
|
|
const slice = source
|
|
.split('\n')
|
|
.slice(evidence.startLine - 1, evidence.endLine)
|
|
.join('\n');
|
|
assert.equal(
|
|
createHash('sha256').update(slice).digest('hex'),
|
|
evidence.sourceSha256,
|
|
`${operation.operationId}: ${field.path}`,
|
|
);
|
|
assert.ok(directionClaim(operation, field.direction), `${operation.operationId}: direction`);
|
|
if (evidence.handlerToken) assert.ok(slice.includes(evidence.handlerToken));
|
|
else if (evidence.dynamicKey)
|
|
assert.match(slice, new RegExp(`["']${evidence.dynamicKey}["']`));
|
|
else {
|
|
assert.match(slice, new RegExp(`(?:struct|enum)\\s+${evidence.symbol}\\b`));
|
|
assert.match(slice, new RegExp(`\\b${evidence.modelField}\\s*:`));
|
|
assert.equal(
|
|
evidence.modelField,
|
|
field.path
|
|
.replace(/\[\*\]/g, '')
|
|
.split('.')
|
|
.at(-1),
|
|
);
|
|
for (const link of evidence.containerPath ?? []) {
|
|
const linkSource = await readFile(`${snapshotRoot}/${link.sourceFile}`, 'utf8');
|
|
const linkSlice = linkSource
|
|
.split('\n')
|
|
.slice(link.startLine - 1, link.endLine)
|
|
.join('\n');
|
|
assert.equal(createHash('sha256').update(linkSlice).digest('hex'), link.sourceSha256);
|
|
assert.match(linkSlice, new RegExp(`struct\\s+${link.symbol}\\b`));
|
|
assert.match(
|
|
linkSlice,
|
|
new RegExp(`\\b${link.field}\\s*:\\s*(?:Vec<)?${link.targetSymbol}`),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
const get = (method: string, path: string) =>
|
|
(upstream58e2204Operations as any[]).find(
|
|
(o) => o.method === method && o.pathTemplate === path,
|
|
);
|
|
assert.ok(!get('GET', '/api/sim').sensitiveFields.some((f: any) => f.path.endsWith('.imei')));
|
|
assert.deepEqual(
|
|
get('GET', '/api/device-network/wlan/status').sensitiveFields.map((f: any) => f.path),
|
|
['$.response.data.ssid', '$.response.data.ipv4_addresses', '$.response.data.ipv6_addresses'],
|
|
);
|
|
assert.deepEqual(
|
|
get('GET', '/api/device-network/wlan/profiles').sensitiveFields.map((f: any) => f.path),
|
|
['$.response.data.profiles[*].ssid'],
|
|
);
|
|
});
|
|
|
|
test('Phase 0.2 uses structured, snapshot-rebuildable evidence and directional redaction contracts', () => {
|
|
for (const operation of upstream58e2204Operations as any[]) {
|
|
for (const name of ['requestEvidence', 'responseEvidence']) {
|
|
const claim = operation[name];
|
|
assert.ok(claim && typeof claim === 'object', `${operation.operationId}: ${name}`);
|
|
assert.match(
|
|
claim.kind,
|
|
/^(none|path|query|json|multipart|bytes|explicit-return|api-response|dynamic-json|opaque)$/,
|
|
);
|
|
assert.match(claim.sourceSha256, /^[a-f0-9]{64}$/);
|
|
assert.ok(
|
|
claim.sourceFile && Number.isInteger(claim.startLine) && Number.isInteger(claim.endLine),
|
|
);
|
|
assert.ok(Array.isArray(claim.symbols));
|
|
assert.ok(Array.isArray(claim.modelEvidence));
|
|
}
|
|
for (const field of operation.sensitiveFields) {
|
|
assert.match(field.direction, /^(request|response|path|query|session)$/);
|
|
assert.match(field.path, /^\$\.(?:body|response|path|query|session)/);
|
|
assert.ok(field.reason);
|
|
assert.match(field.redactionMode, /^(drop|mask)$/);
|
|
assert.ok(field.sourceAnchor?.symbol || field.sourceAnchor?.token);
|
|
}
|
|
}
|
|
const get = (method: string, path: string) =>
|
|
(upstream58e2204Operations as any[]).find(
|
|
(o) => o.method === method && o.pathTemplate === path,
|
|
);
|
|
assert.ok(
|
|
get('GET', '/api/sms/list').responseEvidence.symbols.some(
|
|
(s: any) => s.symbol === 'SmsListResponse',
|
|
),
|
|
);
|
|
assert.ok(
|
|
get('GET', '/api/device').responseEvidence.symbols.some(
|
|
(s: any) => s.symbol === 'DeviceInfoResponse',
|
|
),
|
|
);
|
|
assert.ok(
|
|
get('GET', '/api/network').responseEvidence.symbols.some(
|
|
(s: any) => s.symbol === 'NetworkInfoResponse',
|
|
),
|
|
);
|
|
assert.ok(
|
|
get('POST', '/api/ota/upload').responseEvidence.symbols.some(
|
|
(s: any) => s.symbol === 'OtaUploadResponse',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('structured claims are rebuilt from the independent snapshot, with exact braces and symbols', async () => {
|
|
for (const operation of upstream58e2204Operations as any[])
|
|
for (const claim of [operation.requestEvidence, operation.responseEvidence]) {
|
|
const source = await readFile(`${snapshotRoot}/${claim.sourceFile}`, 'utf8');
|
|
const slice = source
|
|
.split('\n')
|
|
.slice(claim.startLine - 1, claim.endLine)
|
|
.join('\n');
|
|
assert.equal(
|
|
createHash('sha256').update(slice).digest('hex'),
|
|
claim.sourceSha256,
|
|
operation.operationId,
|
|
);
|
|
const fn = operation.handler.split('::').at(-1);
|
|
const signature = new RegExp(`(?:pub\\s+)?async\\s+fn\\s+${fn}\\b`).exec(source);
|
|
assert.ok(signature);
|
|
const open = source.indexOf('{', signature!.index);
|
|
let depth = 0,
|
|
close = -1;
|
|
for (let i = open; i < source.length; i++) {
|
|
if (source[i] === '{') depth++;
|
|
else if (source[i] === '}' && --depth === 0) {
|
|
close = i;
|
|
break;
|
|
}
|
|
}
|
|
assert.equal(
|
|
source.slice(0, close + 1).split('\n').length,
|
|
claim.endLine,
|
|
`${operation.operationId}: exact end`,
|
|
);
|
|
for (const symbol of claim.symbols)
|
|
assert.ok(
|
|
slice.includes(symbol.symbol) ||
|
|
claim.modelEvidence.some((m: any) => m.symbol === symbol.symbol),
|
|
`${operation.operationId}: ${symbol.symbol}`,
|
|
);
|
|
for (const model of claim.modelEvidence) {
|
|
const modelSource = await readFile(`${snapshotRoot}/${model.sourceFile}`, 'utf8');
|
|
const modelSlice = modelSource
|
|
.split('\n')
|
|
.slice(model.startLine - 1, model.endLine)
|
|
.join('\n');
|
|
assert.equal(
|
|
createHash('sha256').update(modelSlice).digest('hex'),
|
|
model.sourceSha256,
|
|
model.symbol,
|
|
);
|
|
assert.match(modelSlice, new RegExp(`(?:struct|enum)\\s+${model.symbol}\\b`));
|
|
for (const field of model.fields) assert.match(modelSlice, new RegExp(`\\b${field}\\s*:`));
|
|
}
|
|
if (claim.kind === 'dynamic-json') assert.match(slice, /json!|Json\s*\(|ApiResponse::/);
|
|
}
|
|
});
|
|
|
|
test('Axum route chains are independently parsed from frozen main.rs', async () => {
|
|
const source = await readFile(`${snapshotRoot}/backend/src/main.rs`, 'utf8');
|
|
const parsed: string[] = [];
|
|
for (let from = 0; ; ) {
|
|
const start = source.indexOf('.route(', from);
|
|
if (start < 0) break;
|
|
let depth = 0,
|
|
quote = false,
|
|
end = -1;
|
|
for (let i = start + 6; i < source.length; i++) {
|
|
const c = source[i];
|
|
if (c === '"' && source[i - 1] !== '\\') quote = !quote;
|
|
if (!quote && c === '(') depth++;
|
|
else if (!quote && c === ')' && --depth === 0) {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
const chain = source.slice(start, end + 1);
|
|
from = end + 1;
|
|
const path = /\.route\(\s*"([^"]+)"/.exec(chain)?.[1];
|
|
if (!path) continue;
|
|
for (const match of chain.matchAll(
|
|
/\.(get|post|delete)\(([\w:]+)\)|\b(get|post|delete)\(([\w:]+)\)/g,
|
|
)) {
|
|
const method = (match[1] ?? match[3]).toUpperCase();
|
|
const handler = match[2] ?? match[4];
|
|
if (handler !== 'options_handler') parsed.push(`${method} ${path} ${handler}`);
|
|
}
|
|
}
|
|
const expected = fixture.routes.map((r: any) => `${r.method} ${r.path} ${r.handler}`).sort();
|
|
assert.deepEqual(parsed.sort(), expected);
|
|
assert.deepEqual(parsed.sort(), (upstream58e2204Operations as any[]).map(operationKey).sort());
|
|
});
|
|
|
|
test('Bruno files are independently parsed and agree with structured request claims', async () => {
|
|
for (const evidence of evidenceFixture.operations)
|
|
for (const bruno of evidence.brunoEvidence ?? []) {
|
|
const text = await readFile(`${snapshotRoot}/${bruno.file}`, 'utf8');
|
|
const method = /\b(get|post|delete)\s*\{/.exec(text)?.[1].toUpperCase();
|
|
const url = /url:\s*(?:\{\{baseUrl\}\}|https?:\/\/[^/\s]+)(\/[^\s]+)/.exec(text)?.[1];
|
|
assert.equal(method, bruno.method);
|
|
assert.equal(url, bruno.path);
|
|
const bodyMode = /body:(json|multipart|text)/.exec(text)?.[1] ?? 'none';
|
|
assert.equal(bodyMode, bruno.bodyMode, bruno.file);
|
|
const operation: any = (upstream58e2204Operations as any[]).find(
|
|
(o) =>
|
|
o.method === method &&
|
|
(o.pathTemplate === url ||
|
|
new RegExp(`^${o.pathTemplate.replace(/\{[^}]+\}/g, '[^/]+')}$`).test(url!)),
|
|
);
|
|
assert.ok(operation, bruno.file);
|
|
if (bodyMode === 'json' && bruno.bodyFields.length)
|
|
assert.equal(operation.requestEvidence.kind, 'json', bruno.file);
|
|
if (bodyMode === 'json' && !bruno.bodyFields.length)
|
|
assert.ok(
|
|
['none', 'path', 'query', 'json'].includes(operation.requestEvidence.kind),
|
|
`${bruno.file}: empty Bruno JSON has no contract fields`,
|
|
);
|
|
for (const field of bruno.bodyFields) assert.match(text, new RegExp(`\\b${field}\\b`));
|
|
}
|
|
});
|
|
|
|
test('frozen fixture has the audited upstream route totals', () => {
|
|
assert.equal(fixture.upstreamCommit, '58e220411d6599609f0eeda01eb7016e9212f970');
|
|
assert.equal(fixture.routes.length, 117);
|
|
assert.equal(new Set(fixture.routes.map(key)).size, 117);
|
|
assert.equal(new Set(fixture.routes.map((route: { path: string }) => route.path)).size, 100);
|
|
assert.deepEqual(
|
|
Object.fromEntries(
|
|
['GET', 'POST', 'DELETE'].map((method) => [
|
|
method,
|
|
fixture.routes.filter((r: { method: string }) => r.method === method).length,
|
|
]),
|
|
),
|
|
{ GET: 50, POST: 62, DELETE: 5 },
|
|
);
|
|
});
|
|
|
|
test('registry is exactly route-parity complete with independent fixture', () => {
|
|
assert.equal(upstream58e2204Operations.length, 117);
|
|
const fixtureTriples = fixture.routes
|
|
.map((r: any) => `${r.method} ${r.path} ${r.handler}`)
|
|
.sort();
|
|
assert.deepEqual(upstream58e2204Operations.map(operationKey).sort(), fixtureTriples);
|
|
assert.equal(
|
|
new Set(upstream58e2204Operations.map((operation) => operation.pathTemplate)).size,
|
|
100,
|
|
);
|
|
assert.equal(
|
|
new Set(upstream58e2204Operations.map((operation) => operation.operationId)).size,
|
|
117,
|
|
);
|
|
for (const operation of upstream58e2204Operations) {
|
|
const route = fixture.routes.find(
|
|
(r: any) => `${r.method} ${r.path} ${r.handler}` === operationKey(operation),
|
|
);
|
|
assert.ok(route, operationKey(operation));
|
|
assert.ok(
|
|
operation.sourceEvidence.includes(route.source),
|
|
`${operation.operationId}: exact main.rs evidence`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('frozen static-contract evidence is complete and bound to routes', () => {
|
|
assert.equal(evidenceFixture.upstreamCommit, fixture.upstreamCommit);
|
|
assert.equal(evidenceFixture.operations.length, 117);
|
|
assert.deepEqual(
|
|
evidenceFixture.operations.map((e: any) => `${e.method} ${e.path} ${e.handler}`).sort(),
|
|
fixture.routes.map((r: any) => `${r.method} ${r.path} ${r.handler}`).sort(),
|
|
);
|
|
for (const evidence of evidenceFixture.operations) {
|
|
assert.match(
|
|
evidence.handlerEvidence,
|
|
/^backend\/src\/(?:handlers|auth|notification_queue)\.rs:\d+-\d+$/,
|
|
);
|
|
assert.match(evidence.sha256, /^[a-f0-9]{64}$/);
|
|
assert.ok(evidence.requestSummary && evidence.responseSummary);
|
|
}
|
|
});
|
|
|
|
test('source evidence is independently reproducible from frozen whole-file snapshots', async () => {
|
|
assert.equal(snapshotManifest.upstreamCommit, fixture.upstreamCommit);
|
|
assert.equal(snapshotManifest.brunoFileCount, 82);
|
|
for (const [file, expected] of Object.entries(snapshotManifest.files)) {
|
|
const bytes = await readFile(`${snapshotRoot}/${file}`);
|
|
assert.equal(createHash('sha256').update(bytes).digest('hex'), expected, file);
|
|
}
|
|
for (const evidence of evidenceFixture.operations) {
|
|
const range = evidence.handlerRange;
|
|
assert.ok(
|
|
range && Number.isInteger(range.startLine) && Number.isInteger(range.endLine),
|
|
evidence.handler,
|
|
);
|
|
const source = await readFile(`${snapshotRoot}/${range.file}`, 'utf8');
|
|
const slice = source
|
|
.split('\n')
|
|
.slice(range.startLine - 1, range.endLine)
|
|
.join('\n');
|
|
assert.equal(
|
|
createHash('sha256').update(slice).digest('hex'),
|
|
evidence.sha256,
|
|
evidence.handler,
|
|
);
|
|
assert.match(slice, new RegExp(`async\\s+fn\\s+${evidence.handler.split('::').at(-1)}\\b`));
|
|
assert.equal(
|
|
(slice.match(/{/g) ?? []).length,
|
|
(slice.match(/}/g) ?? []).length,
|
|
evidence.handler,
|
|
);
|
|
assert.ok(
|
|
evidence.extractorTokens.every((token: string) =>
|
|
slice.replace(/\\s+/g, '').includes(token.replace(/\\s+/g, '')),
|
|
),
|
|
evidence.handler,
|
|
);
|
|
assert.ok(
|
|
evidence.responseKind && evidence.responseTypeEvidence !== 'Rust return impl IntoResponse',
|
|
evidence.handler,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('high-value response, confirmation, sensitive-field and Bruno contracts are fixed', () => {
|
|
const operations: any[] = upstream58e2204Operations as any;
|
|
const byPath = (method: string, path: string) =>
|
|
operations.find((o) => o.method === method && o.pathTemplate === path)!;
|
|
assert.match(
|
|
byPath('GET', '/api/device').responseSchemaEvidence,
|
|
/ApiResponse<DeviceInfoResponse>/,
|
|
);
|
|
assert.match(
|
|
byPath('GET', '/api/network').responseSchemaEvidence,
|
|
/ApiResponse<NetworkInfoResponse>/,
|
|
);
|
|
assert.match(
|
|
byPath('POST', '/api/ota/upload').responseSchemaEvidence,
|
|
/ApiResponse<OtaUploadResponse>/,
|
|
);
|
|
for (const operation of operations) {
|
|
if (operation.riskLevel === 'R2')
|
|
assert.ok(
|
|
['explicit', 'strong'].includes(operation.confirmationPolicy),
|
|
operation.operationId,
|
|
);
|
|
if (operation.riskLevel === 'R3')
|
|
assert.equal(operation.confirmationPolicy, 'strong', operation.operationId);
|
|
}
|
|
const expectedSensitive: Record<string, string[]> = {
|
|
'POST /api/esim/profiles': ['$.body.matching_id', '$.body.confirmation_code', '$.body.imei'],
|
|
'POST /api/sms/send': ['$.body.phone_number', '$.body.content'],
|
|
'GET /api/sms/conversation': ['$.query.phone_number'],
|
|
'POST /api/call/dial': ['$.body.phone_number'],
|
|
'POST /api/auth/setup': ['$.body.password'],
|
|
'POST /api/auth/login': ['$.body.password'],
|
|
'POST /api/auth/password': ['$.body.new_password'],
|
|
'POST /api/device-network/wlan/connect': ['$.body.ssid', '$.body.password'],
|
|
};
|
|
for (const [route, fields] of Object.entries(expectedSensitive)) {
|
|
const [method, path] = route.split(' ');
|
|
for (const field of fields)
|
|
assert.ok(
|
|
byPath(method, path).sensitiveFields.some((claim: any) => claim.path === field),
|
|
`${route}: ${field}`,
|
|
);
|
|
}
|
|
for (const evidence of evidenceFixture.operations) {
|
|
assert.match(evidence.evidenceLevel, /^route\+handler(?:\+model)?(?:\+bruno)?$/);
|
|
if (evidence.brunoEvidence) assert.ok(evidence.brunoEvidence.length > 0);
|
|
}
|
|
});
|
|
|
|
test('every operation carries auditable orchestration metadata', () => {
|
|
for (const operation of upstream58e2204Operations) {
|
|
for (const field of requiredFields)
|
|
assert.ok(field in operation, `${operation.operationId}: missing ${field}`);
|
|
assert.ok(
|
|
expectedDomains.has(operation.upstreamDomain),
|
|
`${operation.operationId}: unknown domain`,
|
|
);
|
|
assert.ok(expectedOwners.has(operation.uiOwner), `${operation.operationId}: unknown owner`);
|
|
assert.match(operation.operationId, /^[a-z][A-Za-z0-9]*$/);
|
|
assert.ok(['R0', 'R1', 'R2', 'R3'].includes(operation.riskLevel));
|
|
assert.ok(Number.isInteger(operation.timeoutMs) && operation.timeoutMs > 0);
|
|
assert.ok(Array.isArray(operation.sensitiveFields));
|
|
assert.ok(Array.isArray(operation.sourceEvidence) && operation.sourceEvidence.length > 0);
|
|
assert.ok(
|
|
operation.sourceEvidence.some((evidence) =>
|
|
/^backend\/src\/main\.rs:\d+-\d+$/.test(evidence),
|
|
),
|
|
);
|
|
assert.ok(
|
|
operation.handler && operation.requestSchemaEvidence && operation.responseSchemaEvidence,
|
|
);
|
|
for (const field of [
|
|
'requestSchemaEvidence',
|
|
'responseSchemaEvidence',
|
|
'requestContentType',
|
|
'idempotency',
|
|
] as const) {
|
|
assert.doesNotMatch(
|
|
operation[field],
|
|
/TODO_EVIDENCE|not frozen/i,
|
|
`${operation.operationId}: ${field}`,
|
|
);
|
|
}
|
|
assert.ok(
|
|
['safe', 'idempotent', 'non-idempotent', 'conditional'].includes(operation.idempotency),
|
|
);
|
|
assert.equal(operation.auditedAtCommit, fixture.upstreamCommit);
|
|
assert.equal(operation.compatibilityAdapter, 'none-pinned-commit');
|
|
if (operation.riskLevel === 'R2') assert.equal(operation.syncSafe, false);
|
|
if (operation.riskLevel === 'R3') {
|
|
assert.equal(operation.capability, 'job');
|
|
assert.equal(operation.syncSafe, false);
|
|
assert.ok(['explicit', 'strong'].includes(operation.confirmationPolicy));
|
|
}
|
|
if (operation.batchable && operation.riskLevel === 'R2')
|
|
assert.equal(operation.capability, 'job');
|
|
}
|
|
});
|
|
|
|
test('dangerous authentication endpoints use dedicated flows, never generic proxy', () => {
|
|
const dangerous = upstream58e2204Operations.filter((operation) =>
|
|
['/api/auth/setup', '/api/auth/password'].includes(operation.pathTemplate),
|
|
);
|
|
assert.equal(dangerous.length, 2);
|
|
for (const operation of dangerous) {
|
|
assert.equal(operation.riskLevel, 'R3');
|
|
assert.equal(operation.executionPolicy, 'dedicatedFlow');
|
|
}
|
|
const loginLogout = upstream58e2204Operations.filter((operation) =>
|
|
['/api/auth/login', '/api/auth/logout'].includes(operation.pathTemplate),
|
|
);
|
|
assert.equal(loginLogout.length, 2);
|
|
assert.ok(
|
|
loginLogout.every((operation) => operation.riskLevel === 'R1' && operation.sessionSensitive),
|
|
);
|
|
const authSettingsPost = upstream58e2204Operations.find(
|
|
(operation) => key(operation) === 'POST /api/auth/settings',
|
|
);
|
|
assert.ok(authSettingsPost && ['R2', 'R3'].includes(authSettingsPost.riskLevel));
|
|
assert.equal(authSettingsPost?.syncSafe, false);
|
|
});
|
|
|
|
test('product risk floors and auth replay policy are enforced', () => {
|
|
const byPath = (method: string, path: string) =>
|
|
upstream58e2204Operations.find((o) => o.method === method && o.pathTemplate === path)!;
|
|
const r3 = [
|
|
['DELETE', '/api/esim/profiles/{iccid}'],
|
|
['DELETE', '/api/call/history/{id}'],
|
|
['POST', '/api/call/history/clear'],
|
|
['POST', '/api/sms/batch-delete'],
|
|
['DELETE', '/api/sms/conversation/{phone_number}'],
|
|
['DELETE', '/api/sms/message/{id}'],
|
|
['POST', '/api/sms/clear'],
|
|
['POST', '/api/notifications/logs/clear'],
|
|
['POST', '/api/notifications/queue/clear'],
|
|
['DELETE', '/api/notifications/queue/{id}'],
|
|
['POST', '/api/automation/logs/clear'],
|
|
];
|
|
for (const [method, path] of r3) {
|
|
const operation = byPath(method, path);
|
|
assert.equal(operation.riskLevel, 'R3', `${method} ${path}`);
|
|
assert.equal(operation.capability, 'job');
|
|
assert.ok(['explicit', 'strong'].includes(operation.confirmationPolicy));
|
|
}
|
|
const r2 = [
|
|
['POST', '/api/sms/send'],
|
|
['POST', '/api/notifications/test/{channel}'],
|
|
['POST', '/api/notifications/queue/retry-all'],
|
|
['POST', '/api/notifications/queue/{id}/retry'],
|
|
['POST', '/api/automation/test/{task_id}'],
|
|
];
|
|
for (const [method, path] of r2) {
|
|
const operation = byPath(method, path);
|
|
assert.ok(['R2', 'R3'].includes(operation.riskLevel), `${method} ${path}`);
|
|
assert.equal(operation.capability, 'job');
|
|
assert.equal(operation.syncSafe, false);
|
|
}
|
|
for (const path of ['/api/auth/login', '/api/auth/logout']) {
|
|
const operation = byPath('POST', path);
|
|
assert.equal(operation.executionPolicy, 'dedicatedFlow');
|
|
assert.equal(operation.sessionSensitive, true);
|
|
assert.equal(operation.automaticReplay, 'forbidden');
|
|
}
|
|
assert.equal(byPath('POST', '/api/auth/settings').executionPolicy, 'dedicatedFlow');
|
|
});
|
|
|
|
test('evidence matrix lists every registry operation', async () => {
|
|
const documentPath = fileURLToPath(
|
|
new URL('../../../docs/api/simadmin-upstream-58e2204.md', import.meta.url),
|
|
);
|
|
const document = await readFile(documentPath, 'utf8');
|
|
const rows = document.split('\n').filter((line) => /^\| `(?:GET|POST|DELETE)` \|/.test(line));
|
|
assert.equal(rows.length, 117);
|
|
for (const operation of upstream58e2204Operations) {
|
|
const row = rows.find(
|
|
(row) =>
|
|
row.includes(`\`${operation.method}\``) &&
|
|
row.includes(`\`${operation.pathTemplate}\``) &&
|
|
row.includes(`\`${operation.operationId}\``),
|
|
);
|
|
assert.ok(row, key(operation));
|
|
const evidence = evidenceFixture.operations.find(
|
|
(e: any) => `${e.method} ${e.path} ${e.handler}` === operationKey(operation),
|
|
);
|
|
assert.ok(evidence, operation.operationId);
|
|
for (const value of [
|
|
operation.upstreamDomain,
|
|
operation.uiOwner,
|
|
operation.handler,
|
|
operation.requestContentType,
|
|
operation.idempotency,
|
|
operation.riskLevel,
|
|
operation.capability,
|
|
evidence.sha256.slice(0, 16),
|
|
]) {
|
|
assert.ok(row.includes(value), `${operation.operationId}: document drift for ${value}`);
|
|
}
|
|
}
|
|
assert.doesNotMatch(document, /TODO_EVIDENCE|not frozen/i);
|
|
assert.match(document, /Phase 0\.3[^\n]*真实响应样本/);
|
|
assert.match(document, /dynamic-json/);
|
|
assert.match(document, /58e220411d6599609f0eeda01eb7016e9212f970/);
|
|
for (const operation of upstream58e2204Operations as any[])
|
|
for (const field of operation.sensitiveFields) {
|
|
const exactRow = `| ${operation.method} | \`${operation.pathTemplate}\` | ${field.direction} | \`${field.path}\` | ${field.redactionMode} | ${field.reason} |`;
|
|
assert.ok(
|
|
document.includes(exactRow),
|
|
`${operation.operationId}: sensitive ledger drift ${field.path}`,
|
|
);
|
|
}
|
|
});
|