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
75 changes: 75 additions & 0 deletions src/agent/search-scorer-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, test } from "bun:test";
import type { ToolDefinition } from "@intx/types/runtime";

import { createAgentIndex } from "./agent-search.js";
import type { AgentProfile } from "./profiles.js";
import { createSkillSearchTool } from "./skill-search.js";
import type { SkillSummary } from "../extensions/skills.js";
import { createToolIndex } from "./tool-search.js";

/**
* The tool, skill, and agent search surfaces each carry their own copy of the
* lexical ranker. This fixture drives all three with parallel catalogs so a
* weight change in one copy fails loudly instead of drifting silently.
*/
const QUERY = "granola";

const tools: ToolDefinition[] = [
{
name: "granola-notes",
description: "unrelated helper",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: "mygranolahoard",
description: "unrelated helper",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: "notebook",
description: "granola syncing helper",
inputSchema: { type: "object", properties: {}, required: [] },
},
{
name: "calendar",
description: "scheduling helper",
inputSchema: { type: "object", properties: {}, required: [] },
},
];

const skills: SkillSummary[] = [
{ name: "granola-notes", description: "unrelated helper" },
{ name: "mygranolahoard", description: "unrelated helper" },
{ name: "notebook", description: "granola syncing helper" },
{ name: "calendar", description: "scheduling helper" },
];

const agents: AgentProfile[] = [
{ id: "granola-notes", description: "unrelated helper" },
{ id: "mygranolahoard", description: "unrelated helper" },
{ id: "notebook", description: "granola syncing helper" },
{ id: "calendar", description: "scheduling helper" },
];

async function skillOrder(query: string): Promise<string[]> {
const tool = createSkillSearchTool({ skills });
if (tool.kind !== "string") throw new Error("expected string tool");
const out = await tool.handler({ query }, new AbortController().signal);
return out
.split("\n")
.filter((line) => line.startsWith("- "))
.map((line) => line.slice(2).split(":")[0] ?? "");
}

describe("search scorer parity", () => {
test("tool, skill, and agent search rank one catalog the same way", async () => {
const expected = ["granola-notes", "mygranolahoard", "notebook"];
expect(createToolIndex(() => tools, []).search(QUERY)).toEqual(expected);
expect(await skillOrder(QUERY)).toEqual(expected);
expect(
createAgentIndex(() => agents)
.search(QUERY)
.map((profile) => profile.id),
).toEqual(expected);
});
});
88 changes: 22 additions & 66 deletions src/plugins/path-escape-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,34 @@ describe("pathEscapePlugin", () => {
expect(result.isError).not.toBe(true);
});

test("blocks paths that escape cwd", async () => {
const plugin = pathEscapePlugin("/project");
const handler = plugin.middleware
? plugin.middleware(nextHandler)
: nextHandler;
const result = await handler(
makeCall("read_file", { path: "../secret.txt" }),
new AbortController().signal,
);
expect(result.isError).toBe(true);
expect(result.content).toMatch(/escapes working directory/);
});
for (const [key, tool, value] of [
["path", "read_file", "../secret.txt"],
["path", "read_file", "/etc/passwd"],
["cwd", "run_shell", "../secret"],
["directory", "list_dir", "/etc"],
["source", "copy", "/etc/passwd"],
["filename", "write_file", "../secret.txt"],
] as const) {
test(`blocks escape via ${key} key (${tool})`, async () => {
const plugin = pathEscapePlugin("/project");
const handler = plugin.middleware
? plugin.middleware(nextHandler)
: nextHandler;
const result = await handler(
makeCall(tool, { [key]: value }),
new AbortController().signal,
);
expect(result.isError).toBe(true);
});
}

test("blocks absolute paths outside cwd", async () => {
test("the block message tells the operator the path escapes the working directory", async () => {
const plugin = pathEscapePlugin("/project");
const handler = plugin.middleware
? plugin.middleware(nextHandler)
: nextHandler;
const result = await handler(
makeCall("read_file", { path: "/etc/passwd" }),
makeCall("read_file", { path: "../secret.txt" }),
new AbortController().signal,
);
expect(result.isError).toBe(true);
Expand All @@ -82,58 +90,6 @@ describe("pathEscapePlugin", () => {
expect(result.isError).not.toBe(true);
});

test("blocks escape via cwd key", async () => {
const plugin = pathEscapePlugin("/project");
const handler = plugin.middleware
? plugin.middleware(nextHandler)
: nextHandler;
const result = await handler(
makeCall("run_shell", { cwd: "../secret" }),
new AbortController().signal,
);
expect(result.isError).toBe(true);
expect(result.content).toMatch(/escapes working directory/);
});

test("blocks escape via directory key", async () => {
const plugin = pathEscapePlugin("/project");
const handler = plugin.middleware
? plugin.middleware(nextHandler)
: nextHandler;
const result = await handler(
makeCall("list_dir", { directory: "/etc" }),
new AbortController().signal,
);
expect(result.isError).toBe(true);
expect(result.content).toMatch(/escapes working directory/);
});

test("blocks escape via source key", async () => {
const plugin = pathEscapePlugin("/project");
const handler = plugin.middleware
? plugin.middleware(nextHandler)
: nextHandler;
const result = await handler(
makeCall("copy", { source: "/etc/passwd" }),
new AbortController().signal,
);
expect(result.isError).toBe(true);
expect(result.content).toMatch(/escapes working directory/);
});

test("blocks escape via filename key", async () => {
const plugin = pathEscapePlugin("/project");
const handler = plugin.middleware
? plugin.middleware(nextHandler)
: nextHandler;
const result = await handler(
makeCall("write_file", { filename: "../secret.txt" }),
new AbortController().signal,
);
expect(result.isError).toBe(true);
expect(result.content).toMatch(/escapes working directory/);
});

test("allowOutside passes outside paths through as absolute", async () => {
const plugin = pathEscapePlugin("/project", () => [], {
allowOutside: true,
Expand Down
20 changes: 20 additions & 0 deletions src/provider/opencode-go-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@ describe("discoverGoModels", () => {
});
});

test("labels every catalog failure as OpenCode Go, never OpenCode Zen", async () => {
globalThis.fetch = (async () =>
new Response("no", { status: 503 })) as unknown as typeof fetch;
const http = await discoverGoModels();
expect(http.status).toBe("unavailable");
if (http.status !== "unavailable") throw new Error("expected unavailable");
expect(http.message.startsWith("OpenCode Go")).toBe(true);
expect(http.message).not.toContain("OpenCode Zen");

globalThis.fetch = (async () =>
oversizedCatalogResponse(
MAX_GO_CATALOG_BYTES + 1,
)) as unknown as typeof fetch;
const oversize = await discoverGoModels();
expect(oversize.status).toBe("malformed");
if (oversize.status !== "malformed") throw new Error("expected malformed");
expect(oversize.message.startsWith("OpenCode Go")).toBe(true);
expect(oversize.message).not.toContain("OpenCode Zen");
});

test("rejects an oversized catalog body without treating it as models", async () => {
globalThis.fetch = (async () =>
oversizedCatalogResponse(
Expand Down
20 changes: 20 additions & 0 deletions src/provider/zen-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@ describe("discoverZenModels", () => {
});
});

test("labels every catalog failure as OpenCode Zen, never OpenCode Go", async () => {
globalThis.fetch = (async () =>
new Response("no", { status: 503 })) as unknown as typeof fetch;
const http = await discoverZenModels();
expect(http.status).toBe("unavailable");
if (http.status !== "unavailable") throw new Error("expected unavailable");
expect(http.message.startsWith("OpenCode Zen")).toBe(true);
expect(http.message).not.toContain("OpenCode Go");

globalThis.fetch = (async () =>
oversizedCatalogResponse(
MAX_ZEN_CATALOG_BYTES + 1,
)) as unknown as typeof fetch;
const oversize = await discoverZenModels();
expect(oversize.status).toBe("malformed");
if (oversize.status !== "malformed") throw new Error("expected malformed");
expect(oversize.message.startsWith("OpenCode Zen")).toBe(true);
expect(oversize.message).not.toContain("OpenCode Go");
});

test("rejects an oversized catalog body without treating it as models", async () => {
globalThis.fetch = (async () =>
oversizedCatalogResponse(
Expand Down
26 changes: 9 additions & 17 deletions src/tui/landing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,23 +715,15 @@ describe("landing screen", () => {
});
try {
await settle(h);
const frame = h.captureCharFrame();
// Assert the badge token, not "queue": this worktree path contains
// "queued" and would false-fail a cwd substring check.
for (const gone of [
"BUSY",
"IDLE",
"FOLLOW",
"follow-up",
"lines",
"focus",
]) {
expect(frame).not.toContain(gone);
}
// The old header blue and status green are gone as fills.
const fills = new Set(backgrounds(h));
expect(fills.has("#3d59a1")).toBe(false);
expect(fills.has("#9ece6a")).toBe(false);
// A bare landing seats exactly two zones: the transcript canvas above
// the prompt box. Resurrected chrome would arrive as a new region.
expect(Object.keys(shell.layout.regions).sort()).toEqual([
"prompt",
"transcript",
]);
// The old header blue and status green were fills; no chrome fill
// survives when every painted span shares one background.
expect(new Set(backgrounds(h)).size).toBe(1);
} finally {
shell.dispose();
}
Expand Down
12 changes: 7 additions & 5 deletions src/tui/selection-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ function host(): SelectionCopyHost & {
describe("copyFinishedSelection", () => {
test("writes selected text, flashes, and clears the highlight", () => {
const h = host();
const selected = "hello world";
const ok = copyFinishedSelection(h, {
isDragging: false,
getSelectedText: () => "hello world",
getSelectedText: () => selected,
});
expect(ok).toBe(true);
expect(h.clipboard.writes).toEqual(["hello world"]);
expect(h.flashes[0]).toContain("Copied 11 chars");
expect(h.flashes[0]).toContain("hello world");
expect(h.clipboard.writes).toEqual([selected]);
expect(h.flashes).toHaveLength(1);
expect(h.flashes[0]).toContain(String(selected.length));
expect(h.flashes[0]).toContain(selected);
expect(h.cleared).toBe(1);
});

Expand Down Expand Up @@ -120,7 +122,7 @@ describe("copyFinishedSelection", () => {
resolveWrite();
await writeP;
await Promise.resolve();
expect(flashes[0]).toContain("Copied 7 chars");
expect(flashes[0]).toContain(String("pending".length));
expect(cleared).toBe(1);
});

Expand Down
21 changes: 0 additions & 21 deletions src/tui/startup-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,6 @@ describe("startup transcript", () => {
});
});

test("three identical system rows in a row paint once", async () => {
await withTestRenderer(async (h) => {
const shell = createAppShell(h.renderer, OPTIONS);
try {
for (let i = 0; i < 3; i += 1) {
appendStreamRow(shell, {
role: "system",
text: DUPLICATE_TEXT,
meta: "synthetic source",
});
}
expect(streamRowCount(shell)).toBe(1);
expect(shell.streamLog.map((row) => row.text)).toEqual([
DUPLICATE_TEXT,
]);
} finally {
shell.dispose();
}
});
});

test("separated repeats and other roles still paint", async () => {
await withTestRenderer(async (h) => {
const shell = createAppShell(h.renderer, OPTIONS);
Expand Down
29 changes: 15 additions & 14 deletions src/tui/welcome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,13 @@ describe("runWelcome", () => {
});

describe("resolveWelcomeLine", () => {
test("keeps the full factory sentence or hides it, never a mid-word slice", () => {
expect(resolveWelcomeLine(80)).toBe(WELCOME_LINE);
expect(resolveWelcomeLine(stringWidth(WELCOME_LINE))).toBe(WELCOME_LINE);

const truncated = WELCOME_LINE.slice(0, 39);
expect(truncated).toContain("facto");
expect(truncated).not.toBe(WELCOME_LINE);

const narrow = resolveWelcomeLine(40);
expect(narrow === "" || narrow === WELCOME_LINE).toBe(true);
expect(narrow).not.toBe(truncated);
expect(narrow.includes("facto") && !narrow.includes("factory")).toBe(false);
test("returns the full line or nothing, never a fragment", () => {
const fullWidth = stringWidth(WELCOME_LINE);
for (let columns = 0; columns < fullWidth; columns += 1) {
expect(resolveWelcomeLine(columns)).toBe("");
}
expect(resolveWelcomeLine(fullWidth)).toBe(WELCOME_LINE);
expect(resolveWelcomeLine(fullWidth + 40)).toBe(WELCOME_LINE);
});
});

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