Skip to content

Commit 376ed98

Browse files
Merge pull request #1035 from corbitsdev/cl-7951-rewrite-brittle-copy-pinned-tests-as-behavior-contracts
Rewrite brittle copy-pinned tests as behavior contracts
2 parents 299aa91 + e346512 commit 376ed98

9 files changed

Lines changed: 197 additions & 152 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, test } from "bun:test";
2+
import type { ToolDefinition } from "@intx/types/runtime";
3+
4+
import { createAgentIndex } from "./agent-search.js";
5+
import type { AgentProfile } from "./profiles.js";
6+
import { createSkillSearchTool } from "./skill-search.js";
7+
import type { SkillSummary } from "../extensions/skills.js";
8+
import { createToolIndex } from "./tool-search.js";
9+
10+
/**
11+
* The tool, skill, and agent search surfaces each carry their own copy of the
12+
* lexical ranker. This fixture drives all three with parallel catalogs so a
13+
* weight change in one copy fails loudly instead of drifting silently.
14+
*/
15+
const QUERY = "granola";
16+
17+
const tools: ToolDefinition[] = [
18+
{
19+
name: "granola-notes",
20+
description: "unrelated helper",
21+
inputSchema: { type: "object", properties: {}, required: [] },
22+
},
23+
{
24+
name: "mygranolahoard",
25+
description: "unrelated helper",
26+
inputSchema: { type: "object", properties: {}, required: [] },
27+
},
28+
{
29+
name: "notebook",
30+
description: "granola syncing helper",
31+
inputSchema: { type: "object", properties: {}, required: [] },
32+
},
33+
{
34+
name: "calendar",
35+
description: "scheduling helper",
36+
inputSchema: { type: "object", properties: {}, required: [] },
37+
},
38+
];
39+
40+
const skills: SkillSummary[] = [
41+
{ name: "granola-notes", description: "unrelated helper" },
42+
{ name: "mygranolahoard", description: "unrelated helper" },
43+
{ name: "notebook", description: "granola syncing helper" },
44+
{ name: "calendar", description: "scheduling helper" },
45+
];
46+
47+
const agents: AgentProfile[] = [
48+
{ id: "granola-notes", description: "unrelated helper" },
49+
{ id: "mygranolahoard", description: "unrelated helper" },
50+
{ id: "notebook", description: "granola syncing helper" },
51+
{ id: "calendar", description: "scheduling helper" },
52+
];
53+
54+
async function skillOrder(query: string): Promise<string[]> {
55+
const tool = createSkillSearchTool({ skills });
56+
if (tool.kind !== "string") throw new Error("expected string tool");
57+
const out = await tool.handler({ query }, new AbortController().signal);
58+
return out
59+
.split("\n")
60+
.filter((line) => line.startsWith("- "))
61+
.map((line) => line.slice(2).split(":")[0] ?? "");
62+
}
63+
64+
describe("search scorer parity", () => {
65+
test("tool, skill, and agent search rank one catalog the same way", async () => {
66+
const expected = ["granola-notes", "mygranolahoard", "notebook"];
67+
expect(createToolIndex(() => tools, []).search(QUERY)).toEqual(expected);
68+
expect(await skillOrder(QUERY)).toEqual(expected);
69+
expect(
70+
createAgentIndex(() => agents)
71+
.search(QUERY)
72+
.map((profile) => profile.id),
73+
).toEqual(expected);
74+
});
75+
});

src/plugins/path-escape-plugin.test.ts

Lines changed: 22 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,34 @@ describe("pathEscapePlugin", () => {
4444
expect(result.isError).not.toBe(true);
4545
});
4646

47-
test("blocks paths that escape cwd", async () => {
48-
const plugin = pathEscapePlugin("/project");
49-
const handler = plugin.middleware
50-
? plugin.middleware(nextHandler)
51-
: nextHandler;
52-
const result = await handler(
53-
makeCall("read_file", { path: "../secret.txt" }),
54-
new AbortController().signal,
55-
);
56-
expect(result.isError).toBe(true);
57-
expect(result.content).toMatch(/escapes working directory/);
58-
});
47+
for (const [key, tool, value] of [
48+
["path", "read_file", "../secret.txt"],
49+
["path", "read_file", "/etc/passwd"],
50+
["cwd", "run_shell", "../secret"],
51+
["directory", "list_dir", "/etc"],
52+
["source", "copy", "/etc/passwd"],
53+
["filename", "write_file", "../secret.txt"],
54+
] as const) {
55+
test(`blocks escape via ${key} key (${tool})`, async () => {
56+
const plugin = pathEscapePlugin("/project");
57+
const handler = plugin.middleware
58+
? plugin.middleware(nextHandler)
59+
: nextHandler;
60+
const result = await handler(
61+
makeCall(tool, { [key]: value }),
62+
new AbortController().signal,
63+
);
64+
expect(result.isError).toBe(true);
65+
});
66+
}
5967

60-
test("blocks absolute paths outside cwd", async () => {
68+
test("the block message tells the operator the path escapes the working directory", async () => {
6169
const plugin = pathEscapePlugin("/project");
6270
const handler = plugin.middleware
6371
? plugin.middleware(nextHandler)
6472
: nextHandler;
6573
const result = await handler(
66-
makeCall("read_file", { path: "/etc/passwd" }),
74+
makeCall("read_file", { path: "../secret.txt" }),
6775
new AbortController().signal,
6876
);
6977
expect(result.isError).toBe(true);
@@ -82,58 +90,6 @@ describe("pathEscapePlugin", () => {
8290
expect(result.isError).not.toBe(true);
8391
});
8492

85-
test("blocks escape via cwd key", async () => {
86-
const plugin = pathEscapePlugin("/project");
87-
const handler = plugin.middleware
88-
? plugin.middleware(nextHandler)
89-
: nextHandler;
90-
const result = await handler(
91-
makeCall("run_shell", { cwd: "../secret" }),
92-
new AbortController().signal,
93-
);
94-
expect(result.isError).toBe(true);
95-
expect(result.content).toMatch(/escapes working directory/);
96-
});
97-
98-
test("blocks escape via directory key", async () => {
99-
const plugin = pathEscapePlugin("/project");
100-
const handler = plugin.middleware
101-
? plugin.middleware(nextHandler)
102-
: nextHandler;
103-
const result = await handler(
104-
makeCall("list_dir", { directory: "/etc" }),
105-
new AbortController().signal,
106-
);
107-
expect(result.isError).toBe(true);
108-
expect(result.content).toMatch(/escapes working directory/);
109-
});
110-
111-
test("blocks escape via source key", async () => {
112-
const plugin = pathEscapePlugin("/project");
113-
const handler = plugin.middleware
114-
? plugin.middleware(nextHandler)
115-
: nextHandler;
116-
const result = await handler(
117-
makeCall("copy", { source: "/etc/passwd" }),
118-
new AbortController().signal,
119-
);
120-
expect(result.isError).toBe(true);
121-
expect(result.content).toMatch(/escapes working directory/);
122-
});
123-
124-
test("blocks escape via filename key", async () => {
125-
const plugin = pathEscapePlugin("/project");
126-
const handler = plugin.middleware
127-
? plugin.middleware(nextHandler)
128-
: nextHandler;
129-
const result = await handler(
130-
makeCall("write_file", { filename: "../secret.txt" }),
131-
new AbortController().signal,
132-
);
133-
expect(result.isError).toBe(true);
134-
expect(result.content).toMatch(/escapes working directory/);
135-
});
136-
13793
test("allowOutside passes outside paths through as absolute", async () => {
13894
const plugin = pathEscapePlugin("/project", () => [], {
13995
allowOutside: true,

src/provider/opencode-go-models.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ describe("discoverGoModels", () => {
102102
});
103103
});
104104

105+
test("labels every catalog failure as OpenCode Go, never OpenCode Zen", async () => {
106+
globalThis.fetch = (async () =>
107+
new Response("no", { status: 503 })) as unknown as typeof fetch;
108+
const http = await discoverGoModels();
109+
expect(http.status).toBe("unavailable");
110+
if (http.status !== "unavailable") throw new Error("expected unavailable");
111+
expect(http.message.startsWith("OpenCode Go")).toBe(true);
112+
expect(http.message).not.toContain("OpenCode Zen");
113+
114+
globalThis.fetch = (async () =>
115+
oversizedCatalogResponse(
116+
MAX_GO_CATALOG_BYTES + 1,
117+
)) as unknown as typeof fetch;
118+
const oversize = await discoverGoModels();
119+
expect(oversize.status).toBe("malformed");
120+
if (oversize.status !== "malformed") throw new Error("expected malformed");
121+
expect(oversize.message.startsWith("OpenCode Go")).toBe(true);
122+
expect(oversize.message).not.toContain("OpenCode Zen");
123+
});
124+
105125
test("rejects an oversized catalog body without treating it as models", async () => {
106126
globalThis.fetch = (async () =>
107127
oversizedCatalogResponse(

src/provider/zen-models.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ describe("discoverZenModels", () => {
102102
});
103103
});
104104

105+
test("labels every catalog failure as OpenCode Zen, never OpenCode Go", async () => {
106+
globalThis.fetch = (async () =>
107+
new Response("no", { status: 503 })) as unknown as typeof fetch;
108+
const http = await discoverZenModels();
109+
expect(http.status).toBe("unavailable");
110+
if (http.status !== "unavailable") throw new Error("expected unavailable");
111+
expect(http.message.startsWith("OpenCode Zen")).toBe(true);
112+
expect(http.message).not.toContain("OpenCode Go");
113+
114+
globalThis.fetch = (async () =>
115+
oversizedCatalogResponse(
116+
MAX_ZEN_CATALOG_BYTES + 1,
117+
)) as unknown as typeof fetch;
118+
const oversize = await discoverZenModels();
119+
expect(oversize.status).toBe("malformed");
120+
if (oversize.status !== "malformed") throw new Error("expected malformed");
121+
expect(oversize.message.startsWith("OpenCode Zen")).toBe(true);
122+
expect(oversize.message).not.toContain("OpenCode Go");
123+
});
124+
105125
test("rejects an oversized catalog body without treating it as models", async () => {
106126
globalThis.fetch = (async () =>
107127
oversizedCatalogResponse(

src/tui/landing.test.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -715,23 +715,15 @@ describe("landing screen", () => {
715715
});
716716
try {
717717
await settle(h);
718-
const frame = h.captureCharFrame();
719-
// Assert the badge token, not "queue": this worktree path contains
720-
// "queued" and would false-fail a cwd substring check.
721-
for (const gone of [
722-
"BUSY",
723-
"IDLE",
724-
"FOLLOW",
725-
"follow-up",
726-
"lines",
727-
"focus",
728-
]) {
729-
expect(frame).not.toContain(gone);
730-
}
731-
// The old header blue and status green are gone as fills.
732-
const fills = new Set(backgrounds(h));
733-
expect(fills.has("#3d59a1")).toBe(false);
734-
expect(fills.has("#9ece6a")).toBe(false);
718+
// A bare landing seats exactly two zones: the transcript canvas above
719+
// the prompt box. Resurrected chrome would arrive as a new region.
720+
expect(Object.keys(shell.layout.regions).sort()).toEqual([
721+
"prompt",
722+
"transcript",
723+
]);
724+
// The old header blue and status green were fills; no chrome fill
725+
// survives when every painted span shares one background.
726+
expect(new Set(backgrounds(h)).size).toBe(1);
735727
} finally {
736728
shell.dispose();
737729
}

src/tui/selection-copy.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ function host(): SelectionCopyHost & {
3232
describe("copyFinishedSelection", () => {
3333
test("writes selected text, flashes, and clears the highlight", () => {
3434
const h = host();
35+
const selected = "hello world";
3536
const ok = copyFinishedSelection(h, {
3637
isDragging: false,
37-
getSelectedText: () => "hello world",
38+
getSelectedText: () => selected,
3839
});
3940
expect(ok).toBe(true);
40-
expect(h.clipboard.writes).toEqual(["hello world"]);
41-
expect(h.flashes[0]).toContain("Copied 11 chars");
42-
expect(h.flashes[0]).toContain("hello world");
41+
expect(h.clipboard.writes).toEqual([selected]);
42+
expect(h.flashes).toHaveLength(1);
43+
expect(h.flashes[0]).toContain(String(selected.length));
44+
expect(h.flashes[0]).toContain(selected);
4345
expect(h.cleared).toBe(1);
4446
});
4547

@@ -120,7 +122,7 @@ describe("copyFinishedSelection", () => {
120122
resolveWrite();
121123
await writeP;
122124
await Promise.resolve();
123-
expect(flashes[0]).toContain("Copied 7 chars");
125+
expect(flashes[0]).toContain(String("pending".length));
124126
expect(cleared).toBe(1);
125127
});
126128

src/tui/startup-transcript.test.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,6 @@ describe("startup transcript", () => {
4848
});
4949
});
5050

51-
test("three identical system rows in a row paint once", async () => {
52-
await withTestRenderer(async (h) => {
53-
const shell = createAppShell(h.renderer, OPTIONS);
54-
try {
55-
for (let i = 0; i < 3; i += 1) {
56-
appendStreamRow(shell, {
57-
role: "system",
58-
text: DUPLICATE_TEXT,
59-
meta: "synthetic source",
60-
});
61-
}
62-
expect(streamRowCount(shell)).toBe(1);
63-
expect(shell.streamLog.map((row) => row.text)).toEqual([
64-
DUPLICATE_TEXT,
65-
]);
66-
} finally {
67-
shell.dispose();
68-
}
69-
});
70-
});
71-
7251
test("separated repeats and other roles still paint", async () => {
7352
await withTestRenderer(async (h) => {
7453
const shell = createAppShell(h.renderer, OPTIONS);

src/tui/welcome.test.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,13 @@ describe("runWelcome", () => {
7171
});
7272

7373
describe("resolveWelcomeLine", () => {
74-
test("keeps the full factory sentence or hides it, never a mid-word slice", () => {
75-
expect(resolveWelcomeLine(80)).toBe(WELCOME_LINE);
76-
expect(resolveWelcomeLine(stringWidth(WELCOME_LINE))).toBe(WELCOME_LINE);
77-
78-
const truncated = WELCOME_LINE.slice(0, 39);
79-
expect(truncated).toContain("facto");
80-
expect(truncated).not.toBe(WELCOME_LINE);
81-
82-
const narrow = resolveWelcomeLine(40);
83-
expect(narrow === "" || narrow === WELCOME_LINE).toBe(true);
84-
expect(narrow).not.toBe(truncated);
85-
expect(narrow.includes("facto") && !narrow.includes("factory")).toBe(false);
74+
test("returns the full line or nothing, never a fragment", () => {
75+
const fullWidth = stringWidth(WELCOME_LINE);
76+
for (let columns = 0; columns < fullWidth; columns += 1) {
77+
expect(resolveWelcomeLine(columns)).toBe("");
78+
}
79+
expect(resolveWelcomeLine(fullWidth)).toBe(WELCOME_LINE);
80+
expect(resolveWelcomeLine(fullWidth + 40)).toBe(WELCOME_LINE);
8681
});
8782
});
8883

@@ -114,8 +109,14 @@ describe("runWelcome hold and cancel", () => {
114109
await harness.renderOnce();
115110
await harness.renderOnce();
116111
const frame = harness.captureCharFrame();
117-
expect(frame).not.toContain("software facto");
118-
expect(frame.includes("facto") && !frame.includes("factory")).toBe(false);
112+
expect(frame).not.toContain(WELCOME_LINE);
113+
const words = WELCOME_LINE.split(/[^A-Za-z]+/).filter(
114+
(word) => word.length >= 6,
115+
);
116+
expect(words.length).toBeGreaterThan(0);
117+
for (const word of words) {
118+
expect(frame).not.toContain(word);
119+
}
119120
} finally {
120121
harness.pressKey("Ctrl+C");
121122
await Promise.race([done, new Promise((r) => setTimeout(r, 50))]);

0 commit comments

Comments
 (0)