import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import argumentMap from '../argumentMap.json' with { type: 'json' }; // The spec is ~5MB; load it via require so tsc doesn't try to infer a type from it. const require = createRequire(import.meta.url); type RequestBodySchema = { properties?: Record; required?: string[]; }; type SpecDoc = { paths: Record< string, { post?: { requestBody?: { content?: Record; }; }; } >; }; const spec = require('@tlecommunity/api-spec/build/spec.json') as SpecDoc; // Spec paths never need an argument map entry. Entries are `/module/method` refs // (the spec path with the leading `/v2` stripped), e.g. `/empire/change_password`. const EXEMPT_PATHS: string[] = []; type Argument = { name: string; required: boolean }; const positional = argumentMap.positional as Record; const noah = new Set(argumentMap.namedOnlyArgumentHash as string[]); const nolfp = new Set(argumentMap.namedOnlyArgumentListFirstPositionHash as string[]); const nolsp = new Set(argumentMap.namedOnlyArgumentListSecondPositionHash as string[]); const mapKeys = new Set([...Object.keys(positional), ...noah, ...nolfp, ...nolsp]); type SpecEntry = { props: string[]; required: Set; hasBody: boolean }; // Build `/module/method` -> request-body shape, validating the spec's own layout as we go. const specRefs = new Map(); for (const [path, item] of Object.entries(spec.paths)) { assert.ok(path.startsWith('/v2/'), `spec path ${path} does not start with /v2/`); assert.ok(item.post, `spec path ${path} has no POST operation`); const ref = path.slice('/v2'.length); const schema = item.post.requestBody?.content?.['application/json']?.schema; specRefs.set(ref, { props: schema?.properties ? Object.keys(schema.properties) : [], required: new Set(schema?.required ?? []), hasBody: schema !== undefined, }); } function assertClean(label: string, violations: string[]) { assert.equal( violations.length, 0, `${violations.length} ${label}:\n${violations.map((v) => ` - ${v}`).join('\n')}` ); } test('every argumentMap key exists in the spec', () => { const violations: string[] = []; for (const key of mapKeys) { if (!specRefs.has(key)) violations.push(key); } assertClean('argumentMap keys missing from the spec', violations); }); test('argumentMap sections are mutually exclusive', () => { const violations: string[] = []; for (const key of mapKeys) { const sections = [ key in positional ? 'positional' : null, noah.has(key) ? 'namedOnlyArgumentHash' : null, nolfp.has(key) ? 'namedOnlyArgumentListFirstPositionHash' : null, nolsp.has(key) ? 'namedOnlyArgumentListSecondPositionHash' : null, ].filter(Boolean); if (sections.length > 1) violations.push(`${key} in ${sections.join(' + ')}`); } assertClean('argumentMap keys in more than one section', violations); }); test('every positional argument is a spec request-body property', () => { const violations: string[] = []; for (const [key, args] of Object.entries(positional)) { const entry = specRefs.get(key); if (!entry) continue; // covered by "every argumentMap key exists in the spec" for (const arg of args) { if (!entry.props.includes(arg.name)) violations.push(`${key} :: ${arg.name}`); } } assertClean('positional arguments absent from the spec request body', violations); }); test('positional required flags match the spec', () => { const violations: string[] = []; for (const [key, args] of Object.entries(positional)) { const entry = specRefs.get(key); if (!entry) continue; for (const arg of args) { if (!entry.props.includes(arg.name)) continue; // covered by the previous test const specRequired = entry.required.has(arg.name); if (arg.required !== specRequired) { violations.push( `${key} :: ${arg.name} (argumentMap required=${arg.required}, spec required=${specRequired})` ); } } } assertClean('positional required flags disagreeing with the spec', violations); }); test('every spec request-body property is covered by the positional map', () => { const violations: string[] = []; for (const [key, args] of Object.entries(positional)) { const entry = specRefs.get(key); if (!entry || !entry.hasBody) continue; const argNames = new Set(args.map((a) => a.name)); for (const prop of entry.props) { if (!argNames.has(prop)) violations.push(`${key} :: ${prop}`); } } assertClean('spec request-body properties missing from the positional map', violations); }); test('every spec path is mapped or exempt', () => { const exempt = new Set(EXEMPT_PATHS); const violations: string[] = []; for (const key of specRefs.keys()) { if (!mapKeys.has(key) && !exempt.has(key)) violations.push(key); } assertClean( 'spec paths with no argumentMap entry (add them to argumentMap.json or EXEMPT_PATHS)', violations ); }); test('EXEMPT_PATHS entries are real, unmapped spec paths', () => { const violations: string[] = []; for (const key of EXEMPT_PATHS) { if (!specRefs.has(key)) violations.push(`${key} (not a spec path)`); else if (mapKeys.has(key)) violations.push(`${key} (also present in argumentMap)`); } assertClean('stale EXEMPT_PATHS entries', violations); });