Skip to content

Commit 296706f

Browse files
committed
add failing test
1 parent 977de8f commit 296706f

1 file changed

Lines changed: 77 additions & 0 deletions

File tree

tests/completion-exec.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { exec } from 'node:child_process';
2+
import { describe, it, expect } from 'vitest';
3+
4+
// Verifies the command that generated completion scripts invoke to request
5+
// suggestions. Completion is triggered by the shell only once the command name
6+
// is resolvable (via PATH, an alias, or a shell function), so the script should
7+
// invoke the CLI by its plain program name rather than a reconstructed,
8+
// runtime-specific launch path. Baking in a launch path makes the script
9+
// depend on how the CLI happened to be started, which does not hold across
10+
// runtimes (e.g. compiled single-file binaries).
11+
12+
function run(command: string): Promise<string> {
13+
return new Promise((resolve, reject) => {
14+
exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => {
15+
if (error) reject(new Error(stderr || String(error)));
16+
else resolve(stdout);
17+
});
18+
});
19+
}
20+
21+
// Pull out the command the generated script will exec to request completions
22+
// (the value baked in as `requestComp` / `$RequestComp`), across every shell.
23+
function extractExecCommand(shell: string, script: string): string | null {
24+
let match: RegExpMatchArray | null = null;
25+
switch (shell) {
26+
case 'zsh':
27+
case 'bash':
28+
match = script.match(/requestComp="(.+?) complete --/);
29+
break;
30+
case 'fish':
31+
match = script.match(/set -l requestComp "(.+?) complete --/);
32+
break;
33+
case 'powershell':
34+
match = script.match(/\$RequestComp = "& (.+?) complete '--'/);
35+
break;
36+
}
37+
return match ? match[1] : null;
38+
}
39+
40+
const adapters = [
41+
{ adapter: 'commander', programName: 'myapp' },
42+
{ adapter: 'cac', programName: 'vite' },
43+
{ adapter: 'citty', programName: 'vite' },
44+
] as const;
45+
46+
const shells = ['zsh', 'bash', 'fish', 'powershell'] as const;
47+
48+
describe('generated completion scripts invoke the CLI by program name', () => {
49+
for (const { adapter, programName } of adapters) {
50+
describe(`${adapter} adapter`, () => {
51+
for (const shell of shells) {
52+
it(`${shell}: requestComp uses the program name, not a runtime launch path`, async () => {
53+
const script = await run(
54+
`pnpm tsx examples/demo.${adapter}.ts complete ${shell}`
55+
);
56+
57+
const execCommand = extractExecCommand(shell, script);
58+
expect(
59+
execCommand,
60+
`could not locate requestComp in generated ${shell} script`
61+
).not.toBeNull();
62+
63+
// The command baked into the script must be exactly the program name.
64+
expect(execCommand).toBe(programName);
65+
66+
// A bare program name never contains any of these substrings, so
67+
// their presence signals a runtime-specific launch path leaking in.
68+
expect(execCommand).not.toContain('/');
69+
expect(execCommand).not.toContain('node');
70+
expect(execCommand).not.toContain('tsx');
71+
expect(execCommand).not.toContain('$bunfs');
72+
expect(execCommand).not.toContain('.ts');
73+
});
74+
}
75+
});
76+
}
77+
});

0 commit comments

Comments
 (0)