141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
OperationRegistry,
|
|
RuntimeRegistryError,
|
|
listOperations,
|
|
operationRegistry,
|
|
requireExecutableOperation,
|
|
requireOperation,
|
|
} from '../src/index.ts';
|
|
import { upstream58e2204Operations } from '../src/upstream-58e2204.ts';
|
|
import { operationAcceptanceOverrides58e2204 } from '../src/acceptance-58e2204.ts';
|
|
import { operationMetadata58e2204 } from '../src/catalog-metadata-58e2204.ts';
|
|
|
|
test('generated API catalog is an exact safe projection of the runtime registry', async () => {
|
|
const { default: generated } = await import(
|
|
'../../../apps/api/src/application/operations/operation-catalog-58e2204.json',
|
|
{ with: { type: 'json' } }
|
|
);
|
|
const expected = listOperations().map(
|
|
({ operationId, title, riskLevel, capability, batchable, parameterSchemaId }) => ({
|
|
operationId,
|
|
title,
|
|
riskLevel,
|
|
capability,
|
|
batchable,
|
|
parameterSchemaId,
|
|
}),
|
|
);
|
|
assert.deepEqual(generated, expected);
|
|
});
|
|
test('runtime registry projects exactly 117 descriptors in stable operationId order', () => {
|
|
const listed = listOperations();
|
|
assert.equal(listed.length, 117);
|
|
assert.equal(new Set(listed.map((row) => row.operationId)).size, 117);
|
|
assert.deepEqual(
|
|
listed.map((row) => row.operationId),
|
|
listed.map((row) => row.operationId).toSorted(),
|
|
);
|
|
assert.ok(Object.isFrozen(listed));
|
|
assert.ok(listed.every((row) => Object.isFrozen(row)));
|
|
|
|
const device = requireOperation('getDevice');
|
|
assert.equal(device.module, device.upstreamDomain);
|
|
assert.equal(device.uiStrategy, operationAcceptanceOverrides58e2204.getDevice.uiStrategy);
|
|
assert.equal(device.method, 'GET');
|
|
assert.ok(Object.isFrozen(device.sourceEvidence));
|
|
assert.strictEqual(requireOperation('getDevice'), device);
|
|
assert.strictEqual(operationRegistry.requireOperation('getDevice'), device);
|
|
});
|
|
|
|
test('runtime construction rejects duplicate rows and missing acceptance overrides', () => {
|
|
const rows = upstream58e2204Operations as readonly (typeof upstream58e2204Operations)[number][];
|
|
assert.throws(
|
|
() =>
|
|
new OperationRegistry(
|
|
[...rows, rows[0]],
|
|
operationAcceptanceOverrides58e2204,
|
|
operationMetadata58e2204,
|
|
),
|
|
/duplicate operationId: getDevice/,
|
|
);
|
|
const missing = { ...operationAcceptanceOverrides58e2204 };
|
|
delete (missing as Record<string, unknown>).getDevice;
|
|
assert.throws(
|
|
() => new OperationRegistry(rows, missing, operationMetadata58e2204),
|
|
/missing acceptance override: getDevice/,
|
|
);
|
|
});
|
|
|
|
test('requireOperation validates only operationId and fails closed with a stable typed error', () => {
|
|
for (const id of [
|
|
'',
|
|
' ',
|
|
'GET /api/device',
|
|
'/api/device',
|
|
'__proto__',
|
|
'constructor',
|
|
'get-device',
|
|
undefined,
|
|
null,
|
|
]) {
|
|
assert.throws(
|
|
() => requireOperation(id as string),
|
|
(error: unknown) =>
|
|
error instanceof RuntimeRegistryError && error.code === 'UNKNOWN_OPERATION',
|
|
String(id),
|
|
);
|
|
}
|
|
assert.throws(
|
|
() => requireOperation('getDefinitelyUnknown'),
|
|
(error: unknown) => error instanceof RuntimeRegistryError && error.code === 'UNKNOWN_OPERATION',
|
|
);
|
|
});
|
|
|
|
test('dedicated auth flows are catalogued but cannot cross executable boundary', () => {
|
|
for (const id of [
|
|
'postAuthSetup',
|
|
'postAuthPassword',
|
|
'postAuthSettings',
|
|
'postAuthLogin',
|
|
'postAuthLogout',
|
|
]) {
|
|
assert.equal(requireOperation(id).executionPolicy, 'dedicatedFlow');
|
|
assert.throws(
|
|
() => requireExecutableOperation(id),
|
|
(error: unknown) =>
|
|
error instanceof RuntimeRegistryError && error.code === 'DEDICATED_FLOW_REQUIRED',
|
|
id,
|
|
);
|
|
}
|
|
const malformed = new OperationRegistry(
|
|
upstream58e2204Operations.map((row) =>
|
|
row.operationId === 'getDevice' ? { ...row, executionPolicy: 'futurePolicy' } : row,
|
|
),
|
|
operationAcceptanceOverrides58e2204,
|
|
operationMetadata58e2204,
|
|
);
|
|
assert.throws(
|
|
() => malformed.requireExecutableOperation('getDevice'),
|
|
(error: unknown) =>
|
|
error instanceof RuntimeRegistryError && error.code === 'DEDICATED_FLOW_REQUIRED',
|
|
);
|
|
assert.strictEqual(requireExecutableOperation('getDevice'), requireOperation('getDevice'));
|
|
});
|
|
|
|
test('descriptors are deeply immutable and mutation cannot affect later lookups or frozen source', () => {
|
|
const source = upstream58e2204Operations.find((row) => row.operationId === 'getDevice')!;
|
|
const originalSourceEvidence = structuredClone(source.sourceEvidence);
|
|
const descriptor = requireOperation('getDevice');
|
|
|
|
assert.ok(Object.isFrozen(descriptor));
|
|
assert.ok(Object.isFrozen(descriptor.sourceEvidence));
|
|
assert.ok(Object.isFrozen(descriptor.sensitiveFields));
|
|
assert.throws(() => ((descriptor as { method: string }).method = 'DELETE'), TypeError);
|
|
assert.throws(() => (descriptor.sourceEvidence as string[]).push('mutation'), TypeError);
|
|
|
|
assert.equal(requireOperation('getDevice').method, 'GET');
|
|
assert.deepEqual(source.sourceEvidence, originalSourceEvidence);
|
|
});
|