mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
d85072d935
* perf(cli): route command aliases through the help fast path bin.ts's `--help` fast path resolved aliases through a hand-written two-entry table that had drifted out of sync with the real CLI_COMMAND_ALIASES registry (five entries). `tap`, `launch`, and `relaunch` missed the table and silently fell through to a full runCli() bootstrap just to print static help text (~150-165ms vs ~45-50ms for aliases already in the table). Delegate to the shared normalizeCliCommandAlias registry instead of the stale local table, so every alias the registry knows about gets the fast path automatically. * test(cli): add R12 layering guard for bin.ts's alias delegation The unit test added for the alias fast-path fix (cli-help-alias-fast-path.test.ts) calls normalizeCliCommandAlias directly, so it stays green even if bin.ts itself reverts to a hand-rolled table — it pins the registry composition, not bin.ts's own wiring, and bin.ts cannot be safely unit-imported (it runs unguarded top-level dispatch on import and is deliberately excluded from coverage). Add an AST-based structural guard instead, in the style already established by scripts/layering/session-state.ts, facade-exports.ts, and zero-dep-jobs.ts (oxc-parser's module/program records, not a line scan, so a fixture's string literal can't produce a false hit). R12 asserts two facts about src/bin.ts: it holds a value import of normalizeCliCommandAlias from commands/cli-command-aliases.ts, and it contains none of the registry's own alias tokens as string literals. The token list is read out of the registry's own source (CLI_COMMAND_ALIASES's `alias:` property values), not hard-coded, so a future sixth alias is covered automatically. Both facts were false on the pre-fix bin.ts, verified by reverting locally and capturing the failure before restoring the fix. Wired into the existing check:layering chain (already part of check:tooling), next to R7's session-state ownership rule, which pins the same "delegate to your single owner" shape. * test(cli): pin the alias-resolver call into buildCommandUsageText (R12 P2) Maintainer review of R12 (PR #1641): import-presence and literal-absence alone let bin.ts regress to buildCommandUsageText(helpTarget) while the normalizeCliCommandAlias import stays in place, used harmlessly elsewhere (or not at all) — the real-tree gate stayed green through that exact regression. Add a third fact: bin.ts's call to buildCommandUsageText must receive, as its argument, a call to the LOCAL binding the resolver was imported as (aliasResolverLocalName + usageTextCallsResolver, both AST-based). Binding by local name rather than the literal export name means a renamed import (`as resolveAlias`) still verifies, and an unrelated same-named local cannot be mistaken for it. Verified by reverting locally to exactly the missed regression — import left in place, call reverted to buildCommandUsageText(helpTarget) — and confirming R12 now fails where the two-fact version passed; restored after. Two negative fixtures pin the scenario going forward: import present but unused, and import present but used only unrelated to the call. * test(cli): make R12's delegation fact universal and value-bound The previous fact 3 asked whether *any* `buildCommandUsageText(resolver(...))` existed in bin.ts. That quantifier is satisfied by a decoy call while the line that actually ships resolves nothing: void buildCommandUsageText(normalizeCliCommandAlias('open')); const commandHelp = buildCommandUsageText(helpTarget); Fact 3 now requires EVERY `buildCommandUsageText` call to receive the imported resolver applied to the fast path's own help-target binding, which rejects both lines above independently. The help-target name is read from bin.ts (the variable initialized by `resolveSimpleHelpTarget`), so renaming it re-points the guard instead of disarming it. Because fact 3 claims binding identity by name, it also now rejects a local shadow of the resolver and an ambiguous second help-target declaration — a same-named local would otherwise let the composition read as delegation while calling something that resolves nothing. The predicate returns the reason rather than a boolean, so the gate names which of the several distinct failures happened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Rva4YGtSCAKJqH5PbpcCU --------- Co-authored-by: Claude <noreply@anthropic.com>
315 lines
13 KiB
TypeScript
315 lines
13 KiB
TypeScript
// R12 bin-alias-fast-path, tested directly: what each pure function reports for a fixture,
|
|
// independently of the check.ts wiring that turns it into a violation.
|
|
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
import {
|
|
ALIAS_REGISTRY_FILE,
|
|
aliasResolverLocalName,
|
|
BIN_FILE,
|
|
countLocalBindings,
|
|
helpTargetBindingName,
|
|
importsAliasResolver,
|
|
localAliasLiterals,
|
|
registryAliasTokens,
|
|
usageTextDelegationFailure,
|
|
} from './bin-alias-fast-path.ts';
|
|
|
|
/**
|
|
* The shape of bin.ts's real `--help` fast path, minus everything R12 does not read. Fixtures
|
|
* below vary one thing against this baseline, so a test's subject is the line it changed.
|
|
*/
|
|
function binFixture(fastPathBody: string, prelude = ''): string {
|
|
return `
|
|
import { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';
|
|
${prelude}
|
|
function runHelpFastPath(argv) {
|
|
const helpTarget = resolveSimpleHelpTarget(argv);
|
|
if (helpTarget === undefined) return false;
|
|
${fastPathBody}
|
|
return true;
|
|
}
|
|
`;
|
|
}
|
|
|
|
/** `usageTextDelegationFailure` for a fixture, resolving the local name the way check.ts does. */
|
|
function delegationFailure(source: string): string | null {
|
|
return usageTextDelegationFailure(source, aliasResolverLocalName(source)!);
|
|
}
|
|
|
|
const REGISTRY_FIXTURE = `
|
|
import type { CliFlags } from '@agent-device/contracts/command';
|
|
const CLI_COMMAND_ALIASES = [
|
|
{ alias: 'long-press', command: 'longpress' },
|
|
{ alias: 'metrics', command: 'perf' },
|
|
{ alias: 'tap', command: 'press' },
|
|
{ alias: 'launch', command: 'open' },
|
|
{ alias: 'relaunch', command: 'open', impliedFlags: ['relaunch'] },
|
|
];
|
|
export function normalizeCliCommandAlias(command) { return command; }
|
|
`;
|
|
|
|
test('registryAliasTokens reads every alias property value out of the registry source', () => {
|
|
assert.deepEqual(registryAliasTokens(REGISTRY_FIXTURE), [
|
|
'launch',
|
|
'long-press',
|
|
'metrics',
|
|
'relaunch',
|
|
'tap',
|
|
]);
|
|
});
|
|
|
|
test('registryAliasTokens is not fooled by an unrelated `alias` string elsewhere in the file', () => {
|
|
// Only a `{ alias: '<token>' }` object-property VALUE counts. A same-named local variable, or
|
|
// the word appearing inside a comment, must not contribute a token.
|
|
const source = "const alias = 'not-a-token';\n// alias: also not a token\n";
|
|
assert.deepEqual(registryAliasTokens(source), []);
|
|
});
|
|
|
|
test('importsAliasResolver is true only for a real VALUE import of the resolver', () => {
|
|
assert.equal(
|
|
importsAliasResolver(
|
|
"import { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';\n",
|
|
),
|
|
true,
|
|
);
|
|
// A renamed local binding still delegates to the real function — the registry specifier and
|
|
// the imported name are what matter, not what the caller calls it locally.
|
|
assert.equal(
|
|
importsAliasResolver(
|
|
"import { normalizeCliCommandAlias as resolve } from './commands/cli-command-aliases.ts';\n",
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('importsAliasResolver is false for a type-only import', () => {
|
|
// Erased at compile time — no runtime delegation at all, which is exactly the STOP condition
|
|
// the original plan called out: importing the registry as a type only would look wired
|
|
// without actually being wired.
|
|
assert.equal(
|
|
importsAliasResolver(
|
|
"import type { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';\n",
|
|
),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('importsAliasResolver is false when the import is missing or from the wrong module', () => {
|
|
assert.equal(importsAliasResolver('const x = 1;\n'), false);
|
|
assert.equal(
|
|
importsAliasResolver("import { normalizeCliCommandAlias } from './wrong-file.ts';\n"),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
importsAliasResolver("import { somethingElse } from './commands/cli-command-aliases.ts';\n"),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('aliasResolverLocalName resolves the LOCAL binding, following an `as` alias', () => {
|
|
assert.equal(
|
|
aliasResolverLocalName(
|
|
"import { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';\n",
|
|
),
|
|
'normalizeCliCommandAlias',
|
|
);
|
|
assert.equal(
|
|
aliasResolverLocalName(
|
|
"import { normalizeCliCommandAlias as resolveAlias } from './commands/cli-command-aliases.ts';\n",
|
|
),
|
|
'resolveAlias',
|
|
);
|
|
});
|
|
|
|
test('aliasResolverLocalName is null when there is no matching value import', () => {
|
|
assert.equal(aliasResolverLocalName('const x = 1;\n'), null);
|
|
assert.equal(
|
|
aliasResolverLocalName(
|
|
"import type { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';\n",
|
|
),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test('helpTargetBindingName reads the binding resolveSimpleHelpTarget produces', () => {
|
|
assert.equal(
|
|
helpTargetBindingName(binFixture(' buildCommandUsageText(normalizeCliCommandAlias(x));')),
|
|
'helpTarget',
|
|
);
|
|
// Renaming the local re-points the guard rather than disarming it — the name is never assumed.
|
|
const renamed = `
|
|
function runHelpFastPath(argv) {
|
|
const target = resolveSimpleHelpTarget(argv);
|
|
}
|
|
`;
|
|
assert.equal(helpTargetBindingName(renamed), 'target');
|
|
});
|
|
|
|
test('helpTargetBindingName is null when the fast path no longer produces one', () => {
|
|
assert.equal(helpTargetBindingName('const helpTarget = argv[0];\n'), null);
|
|
});
|
|
|
|
test('countLocalBindings counts value declarations only, not the import or type positions', () => {
|
|
const source = `
|
|
import { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';
|
|
function f(helpTarget: normalizeCliCommandAlias) { const other = 1; }
|
|
`;
|
|
// The import itself is not a shadow, and a type annotation naming the resolver binds nothing.
|
|
assert.equal(countLocalBindings(source, 'normalizeCliCommandAlias'), 0);
|
|
assert.equal(countLocalBindings(source, 'helpTarget'), 1);
|
|
assert.equal(countLocalBindings('const x = 1;\nfunction x() {}\n', 'x'), 2);
|
|
});
|
|
|
|
test('usageTextDelegationFailure accepts the real composition, by local name', () => {
|
|
assert.equal(
|
|
delegationFailure(
|
|
binFixture(
|
|
' const commandHelp = buildCommandUsageText(normalizeCliCommandAlias(helpTarget));',
|
|
),
|
|
),
|
|
null,
|
|
);
|
|
// Binds by whatever LOCAL name the import resolved to — an aliased import's local name must
|
|
// still be found at the call site, since that is the only name available to call it by.
|
|
const aliased = `
|
|
import { normalizeCliCommandAlias as resolveAlias } from './commands/cli-command-aliases.ts';
|
|
function runHelpFastPath(argv) {
|
|
const helpTarget = resolveSimpleHelpTarget(argv);
|
|
const commandHelp = buildCommandUsageText(resolveAlias(helpTarget));
|
|
}
|
|
`;
|
|
assert.equal(delegationFailure(aliased), null);
|
|
});
|
|
|
|
test('usageTextDelegationFailure rejects a raw call, with no wrapping resolver call', () => {
|
|
const failure = delegationFailure(
|
|
binFixture(' const commandHelp = buildCommandUsageText(helpTarget);'),
|
|
);
|
|
assert.match(failure ?? '', /buildCommandUsageText\(helpTarget\)/);
|
|
});
|
|
|
|
// #P2 (first maintainer review of R12): import presence and literal absence both still pass a
|
|
// bin.ts that imports the resolver and never calls it, or calls it on something unrelated, while
|
|
// buildCommandUsageText runs on the raw, unresolved helpTarget.
|
|
test('usageTextDelegationFailure rejects a present-but-unused import', () => {
|
|
const source = binFixture(' const commandHelp = buildCommandUsageText(helpTarget);');
|
|
assert.equal(importsAliasResolver(source), true);
|
|
assert.notEqual(delegationFailure(source), null);
|
|
});
|
|
|
|
test('usageTextDelegationFailure rejects an import used only unrelated to buildCommandUsageText', () => {
|
|
const source = binFixture(
|
|
' const commandHelp = buildCommandUsageText(helpTarget);',
|
|
'void normalizeCliCommandAlias;',
|
|
);
|
|
assert.equal(importsAliasResolver(source), true);
|
|
assert.notEqual(delegationFailure(source), null);
|
|
});
|
|
|
|
// #P2 (second maintainer review of R12): the fixture below is the reviewer's own, verbatim in
|
|
// shape. An EXISTENTIAL fact 3 — "some buildCommandUsageText call somewhere wraps the resolver" —
|
|
// accepts it, because the decoy on the first line satisfies the quantifier while the line that
|
|
// actually ships resolves nothing. This is the regression that motivated making fact 3 universal
|
|
// and value-bound, and it must be rejected for BOTH reasons independently.
|
|
test('usageTextDelegationFailure rejects a decoy wrapped call beside a raw shipped call', () => {
|
|
const source = binFixture(
|
|
` void buildCommandUsageText(normalizeCliCommandAlias('open'));
|
|
const commandHelp = buildCommandUsageText(helpTarget);`,
|
|
);
|
|
assert.equal(importsAliasResolver(source), true);
|
|
const failure = delegationFailure(source);
|
|
assert.match(failure ?? '', /every buildCommandUsageText call must receive/);
|
|
});
|
|
|
|
test('usageTextDelegationFailure rejects the resolver applied to anything but the help target', () => {
|
|
// The decoy alone, with no raw call at all: the only usage-text call in the file wraps the
|
|
// resolver, so a universal-but-not-value-bound fact 3 would still pass it.
|
|
const source = binFixture(
|
|
" const commandHelp = buildCommandUsageText(normalizeCliCommandAlias('open'));",
|
|
);
|
|
assert.match(delegationFailure(source) ?? '', /normalizeCliCommandAlias\("open"\)/);
|
|
});
|
|
|
|
test('usageTextDelegationFailure rejects a local shadow of the imported resolver', () => {
|
|
// Fact 3 binds by NAME, so a same-named local would otherwise let the composition read as
|
|
// delegation while calling something that resolves nothing.
|
|
const source = binFixture(
|
|
' const commandHelp = buildCommandUsageText(normalizeCliCommandAlias(helpTarget));',
|
|
'const normalizeCliCommandAlias = (command) => command;',
|
|
);
|
|
assert.match(delegationFailure(source) ?? '', /shadowing the imported resolver/);
|
|
});
|
|
|
|
test('usageTextDelegationFailure rejects an ambiguous second help-target binding', () => {
|
|
const source = binFixture(
|
|
` const helpTarget = 'open';
|
|
const commandHelp = buildCommandUsageText(normalizeCliCommandAlias(helpTarget));`,
|
|
);
|
|
assert.match(delegationFailure(source) ?? '', /declares helpTarget more than once/);
|
|
});
|
|
|
|
test('usageTextDelegationFailure reports a fast path that no longer builds usage text at all', () => {
|
|
const gone = `
|
|
import { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';
|
|
function runHelpFastPath(argv) {
|
|
const helpTarget = resolveSimpleHelpTarget(argv);
|
|
void normalizeCliCommandAlias(helpTarget);
|
|
}
|
|
`;
|
|
assert.match(delegationFailure(gone) ?? '', /never calls buildCommandUsageText/);
|
|
// …and one whose help-target producer is gone, so the guard says so instead of passing blind.
|
|
const untraceable = `
|
|
import { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';
|
|
const commandHelp = buildCommandUsageText(normalizeCliCommandAlias(argv[1]));
|
|
`;
|
|
assert.match(delegationFailure(untraceable) ?? '', /has no variable initialized by/);
|
|
});
|
|
|
|
test('localAliasLiterals reports every requested token present as a string literal', () => {
|
|
// The pre-fix bin.ts shape: a hand-written table re-declaring two of the five tokens.
|
|
const preFixBinSource = `
|
|
function normalizeHelpTarget(command) {
|
|
if (command === 'long-press') return 'longpress';
|
|
if (command === 'metrics') return 'perf';
|
|
return command;
|
|
}
|
|
`;
|
|
assert.deepEqual(
|
|
localAliasLiterals(preFixBinSource, ['long-press', 'metrics', 'tap', 'launch', 'relaunch']),
|
|
['long-press', 'metrics'],
|
|
);
|
|
});
|
|
|
|
test('localAliasLiterals ignores tokens that only appear as identifiers, not string literals', () => {
|
|
const source = 'const tap = 1;\nfunction launch() {}\n';
|
|
assert.deepEqual(localAliasLiterals(source, ['tap', 'launch']), []);
|
|
});
|
|
|
|
test('localAliasLiterals reports nothing when the fixed bin.ts delegates and holds no literals', () => {
|
|
const fixedBinSource = `
|
|
import { normalizeCliCommandAlias } from './commands/cli-command-aliases.ts';
|
|
const commandHelp = buildCommandUsageText(normalizeCliCommandAlias(helpTarget));
|
|
`;
|
|
assert.deepEqual(
|
|
localAliasLiterals(fixedBinSource, ['long-press', 'metrics', 'tap', 'launch', 'relaunch']),
|
|
[],
|
|
);
|
|
});
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, '../..');
|
|
|
|
test('the real tree imports the resolver, calls it into buildCommandUsageText, holds no local alias literals, and passes R12', () => {
|
|
const registrySource = readFileSync(path.join(repoRoot, ALIAS_REGISTRY_FILE), 'utf8');
|
|
const binSource = readFileSync(path.join(repoRoot, BIN_FILE), 'utf8');
|
|
const tokens = registryAliasTokens(registrySource);
|
|
assert.deepEqual(tokens, ['launch', 'long-press', 'metrics', 'relaunch', 'tap']);
|
|
const localName = aliasResolverLocalName(binSource);
|
|
assert.equal(localName, 'normalizeCliCommandAlias');
|
|
assert.equal(helpTargetBindingName(binSource), 'helpTarget');
|
|
assert.equal(usageTextDelegationFailure(binSource, localName!), null);
|
|
assert.deepEqual(localAliasLiterals(binSource, tokens), []);
|
|
});
|