import { describe, it, expect } from "vitest";
import { escapeXML, continuationPrompt } from "../src/prompts";
describe("escapeXML", () => {
it("escapes ampersands", () => {
expect(escapeXML("a & b")).toBe("a & b");
});
it("escapes less-than signs", () => {
expect(escapeXML("a < b")).toBe("a < b");
});
it("escapes greater-than signs", () => {
expect(escapeXML("a > b")).toBe("a > b");
});
it("escapes all special characters in combination", () => {
expect(escapeXML(" & goal")).toBe(
"<objective> & goal</objective>",
);
});
it("returns the same string when there is nothing to escape", () => {
expect(escapeXML("hello world")).toBe("hello world");
});
it("returns empty string for empty input", () => {
expect(escapeXML("")).toBe("");
});
// Only & < > were escaped -- NUL, other C0 controls, and
// bidi override/isolate characters passed through raw. Left in, any of these could
// truncate, reorder, or visually obscure the prompt text around them (e.g. a bidi override
// placed just before a verdict instruction). Built from String.fromCodePoint rather than
// literal characters in this file so nothing invisible sits in the test source either.
describe("control character stripping", () => {
it("strips NUL and other C0 control characters", () => {
const nul = String.fromCodePoint(0x0000);
const escape = String.fromCodePoint(0x001b);
expect(escapeXML(`hello${nul}world${escape}end`)).toBe("helloworldend");
});
it("strips DEL", () => {
const del = String.fromCodePoint(0x007f);
expect(escapeXML(`hello${del}world`)).toBe("helloworld");
});
it("strips zero-width spaces and joiners", () => {
const zeroWidthSpace = String.fromCodePoint(0x200b);
const zeroWidthJoiner = String.fromCodePoint(0x200d);
expect(escapeXML(`hello${zeroWidthSpace}world${zeroWidthJoiner}end`)).toBe("helloworldend");
});
it("strips bidi override, embedding, and isolate characters", () => {
const rtlOverride = String.fromCodePoint(0x202e);
const leftToRightIsolate = String.fromCodePoint(0x2066);
expect(escapeXML(`hello${rtlOverride}world${leftToRightIsolate}end`)).toBe("helloworldend");
});
it("preserves tab, LF, and CR", () => {
const withWhitespace = "a\tb\nc\rd";
expect(escapeXML(withWhitespace)).toBe(withWhitespace);
});
it("still escapes & < > after stripping control characters", () => {
const nul = String.fromCodePoint(0x0000);
expect(escapeXML(`${nul}& more`)).toBe("<tag>& more");
});
});
});
describe("continuationPrompt", () => {
it("wraps the objective in untrusted_objective tags", () => {
const prompt = continuationPrompt("write tests");
expect(prompt).toContain("");
expect(prompt).toContain("");
});
it("includes the objective text inside the tags", () => {
const prompt = continuationPrompt("write tests");
expect(prompt).toContain("\nwrite tests\n");
});
it("escapes XML special characters in the objective", () => {
const prompt = continuationPrompt("install foo & bar ");
expect(prompt).toContain("install foo & bar <baz>");
});
it('includes the phrase "completion audit"', () => {
const prompt = continuationPrompt("do stuff");
expect(prompt).toContain("completion audit");
});
it('includes the phrase "Continue working toward the active thread goal"', () => {
const prompt = continuationPrompt("do stuff");
expect(prompt).toContain("Continue working toward the active thread goal.");
});
it('includes the warning not to mark complete when stopping work', () => {
const prompt = continuationPrompt("do stuff");
expect(prompt).toContain(
"Do not mark a goal complete merely because the budget is nearly exhausted or because you are stopping work",
);
});
it("does not include the empty-turn nudge by default", () => {
const prompt = continuationPrompt("do stuff");
expect(prompt).not.toContain("made no tool calls");
});
// The weak-model fix: after a text-only cycle, the continuation prompt is prefixed with an
// explicit instruction to take a real action instead of asking or only describing.
it("prepends an action nudge when opts.nudge is set", () => {
const prompt = continuationPrompt("do stuff", { nudge: true });
expect(prompt).toContain("made no tool calls");
expect(prompt).toContain("Take one concrete action now");
// The nudge is first so a small model reads it before anything else.
expect(prompt.indexOf("made no tool calls")).toBeLessThan(
prompt.indexOf("Continue working toward the active thread goal."),
);
// The normal continuation content is still present.
expect(prompt).toContain("\ndo stuff\n");
});
});