Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ function buildInvocationArgs(
modelArgs: readonly string[] | undefined,
model: string | undefined,
): string[] {
const args = template.map((arg) => arg.replaceAll("{prompt}", prompt));
const args = template.map((arg) => arg.replaceAll("{prompt}", () => prompt));
if (model !== undefined && modelArgs) {
args.push(...modelArgs.map((arg) => arg.replaceAll("{model}", model)));
args.push(...modelArgs.map((arg) => arg.replaceAll("{model}", () => model)));
}
return args;
}
Expand Down Expand Up @@ -420,7 +420,7 @@ export abstract class Harness {
if (!template) return null;
return {
command,
args: template.map((arg) => arg.replaceAll("{search}", search ?? "")),
args: template.map((arg) => arg.replaceAll("{search}", () => search ?? "")),
};
}

Expand Down
25 changes: 25 additions & 0 deletions test/invoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ class FakeCursor extends Harness {
}

describe("normalized invocation", () => {
it("preserves replacement tokens in prompts across invocation modes", () => {
const prompt = "Explain $$, $&, $`, $', $1 and {prompt}";
const claude = getHarness("claude");
const codex = getHarness("codex");

for (const built of [
claude.buildInvocation(prompt),
claude.buildInvocation(prompt, { structured: true }),
claude.buildInvocation(prompt, { tools: true }),
claude.buildInvocation(prompt, { tools: true, structured: true }),
codex.buildInvocation(prompt, { readOnly: true }),
codex.buildInvocation(prompt, { readOnly: true, structured: true }),
]) {
expect(built?.args).toContain(prompt);
}
});

it("delivers replacement tokens unchanged to the child process", async () => {
const prompt = "Explain $$, $&, $`, $', $1 and {prompt}";
const result = await new FakeCursor().invoke(prompt, { structured: true });

expect(result.exitCode).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({ advisor: prompt });
});

it("keeps the Claude advisor prompt outside the variadic tools option", () => {
const claude = getHarness("claude");
expect(claude.buildInvocation("answer this")).toEqual({
Expand Down
18 changes: 18 additions & 0 deletions test/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ describe("model listing", () => {
});
});

it("preserves replacement tokens in model names", () => {
const model = "custom/$$-$&-$`-$'-$1-{model}";

expect(new Pi().buildInvocation("answer this", { model })).toEqual({
command: "pi",
args: ["-p", "--no-tools", "answer this", "--model", model],
});
});

it("preserves replacement tokens in model search filters", () => {
const search = "$$ $& $` $' $1 {search}";

expect(new Pi().buildModelListInvocation(search)).toEqual({
command: "pi",
args: ["--list-models", search],
});
});

it("adds an explicit model to a Pi run invocation", () => {
const pi = new Pi();

Expand Down
Loading