Skip to content

Commit c423b91

Browse files
Merge test stabilisation
2 parents f0dc610 + 12c027e commit c423b91

6 files changed

Lines changed: 64 additions & 27 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ When refactoring replaces an old path, delete the old one. No back-compat shims,
3939
```bash
4040
bun run typecheck
4141
bun run build
42-
bun test
42+
bun run test
4343
```
4444

4545
Run the full suite before declaring any task complete. Do not substitute individual targets. If a failure is pre-existing and unrelated to your change, say so explicitly.
4646

47+
`bun run test` runs `bun test ./src ./tests ./evals`. A bare `bun test` also
48+
scans `vendor/`, adding hundreds of unrelated results and making pass/fail
49+
counts meaningless to compare across branches — always use `bun run test`.
50+
4751
## Commits
4852

4953
Follow the `style` skill's message format: plain-English summary, no `feat:`/`fix:` prefixes, no filename in the summary. Separate refactors from feature additions. Commit with the user's local git identity.

src/plugins/ripgrep-plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
type BoundedGrepArgs,
99
} from "./bounded-grep-fallback.js";
1010
import { createRgCollector } from "./rg-output.js";
11-
import { MAX_OUTPUT_BYTES, runRg, type RgLimits } from "./rg-run.js";
11+
import { MAX_OUTPUT_BYTES, runRg, type RgLimits, type SpawnRg } from "./rg-run.js";
1212

1313
// A grep over a large tree with the pure-TypeScript walker enumerates the whole
1414
// directory (node_modules, build output, the lot) before searching, which stalls
@@ -66,7 +66,7 @@ function searchLocation(path: string, fallbackCwd: string): { cwd: string; targe
6666
}
6767
}
6868

69-
export function ripgrepPlugin(cwd: string, limits: RgLimits = {}): ToolPlugin {
69+
export function ripgrepPlugin(cwd: string, limits: RgLimits = {}, spawnChild?: SpawnRg): ToolPlugin {
7070
const maxBytes = limits.maxOutputBytes ?? MAX_OUTPUT_BYTES;
7171
return {
7272
middleware: (next) => async (call, signal) => {
@@ -86,7 +86,7 @@ export function ripgrepPlugin(cwd: string, limits: RgLimits = {}): ToolPlugin {
8686
if (glob !== undefined) rgArgs.push("-g", glob);
8787
rgArgs.push("--regexp", pattern, target);
8888

89-
const result = await runRg(rgArgs, rgCwd, signal, limits);
89+
const result = await runRg(rgArgs, rgCwd, signal, limits, spawnChild);
9090
if (result.kind === "unavailable") {
9191
try {
9292
const boundedArgs: BoundedGrepArgs = {

src/tui-opentui/markdown-rows.test.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,22 @@ const shellOpts = {
2323
} as const
2424

2525
/**
26-
* Markdown blocks highlight asynchronously; settle before capturing a frame.
27-
* A row with several top-level blocks (heading, list, fence, link) resolves
28-
* its highlight promises one render at a time, so a fixed couple of ticks
29-
* that was enough for one block is not enough for several.
26+
* Highlighting runs on a worker outside the render scheduler, so the
27+
* scheduler goes idle before the highlighted frame lands. Pass a predicate
28+
* for the settled shape and get the frame back the moment it's true, rather
29+
* than gambling on a fixed sleep long enough to outrun load.
3030
*/
31-
async function settle(h: Harness): Promise<string> {
32-
for (let i = 0; i < 8; i += 1) {
33-
await new Promise((resolve) => setTimeout(resolve, 50))
31+
async function settle(h: Harness, isSettled: (frame: string) => boolean): Promise<string> {
32+
const deadline = Date.now() + 2000
33+
for (;;) {
3434
await h.renderOnce()
35+
const frame = h.captureCharFrame()
36+
if (isSettled(frame)) return frame
37+
if (Date.now() >= deadline) {
38+
throw new Error(`markdown row never settled; last frame:\n${frame}`)
39+
}
40+
await new Promise((resolve) => setTimeout(resolve, 20))
3541
}
36-
return h.captureCharFrame()
3742
}
3843

3944
describe("markdown transcript rows", () => {
@@ -68,7 +73,10 @@ describe("markdown transcript rows", () => {
6873
].join("\n"),
6974
})
7075

71-
const frame = await settle(h)
76+
const frame = await settle(
77+
h,
78+
(f) => f.includes("docs") && !f.includes("## Title") && !f.includes("**bolded**"),
79+
)
7280
expect(frame).toContain("Title")
7381
expect(frame).not.toContain("## Title")
7482
expect(frame).toContain("bolded")
@@ -94,7 +102,10 @@ describe("markdown transcript rows", () => {
94102
].join("\n"),
95103
})
96104

97-
const frame = await settle(h)
105+
const frame = await settle(
106+
h,
107+
(f) => f.includes("What the site is") && !f.includes("###") && !f.includes("**Hardware:**"),
108+
)
98109
expect(frame).toContain("What the site is")
99110
expect(frame).not.toContain("###")
100111
expect(frame).toContain("Hardware:")
@@ -108,7 +119,7 @@ describe("markdown transcript rows", () => {
108119
appendStreamRow(shell, { role: "tool", text: "## not a heading" })
109120
appendStreamRow(shell, { role: "system", text: "**raw**" })
110121

111-
const frame = await settle(h)
122+
const frame = await settle(h, (f) => f.includes("**raw**"))
112123
expect(frame).toContain("## not a heading")
113124
expect(frame).toContain("**raw**")
114125
}, WIDE)
@@ -123,7 +134,10 @@ describe("markdown transcript rows", () => {
123134
text: ["## Done", "", "```ts", "const partial = "].join("\n"),
124135
})
125136

126-
const frame = await settle(h)
137+
const frame = await settle(
138+
h,
139+
(f) => f.includes("Done") && !f.includes("## Done") && f.includes("const partial ="),
140+
)
127141
expect(frame).toContain("Done")
128142
expect(frame).not.toContain("## Done")
129143
expect(frame).toContain("const partial =")
@@ -143,7 +157,7 @@ describe("markdown transcript rows", () => {
143157
// literal text and the row paints the bare markers until the title's
144158
// first character lands. Held back instead, so the line's classification
145159
// cannot flip under text already on screen.
146-
const frame = await settle(h)
160+
const frame = await settle(h, (f) => f.includes("Some body text.") && !f.includes("####"))
147161
expect(frame).toContain("Some body text.")
148162
expect(frame).not.toContain("####")
149163

@@ -152,7 +166,7 @@ describe("markdown transcript rows", () => {
152166
streaming: true,
153167
text: ["Some body text.", "", "#### Title"].join("\n"),
154168
})
155-
const next = await settle(h)
169+
const next = await settle(h, (f) => f.includes("Title") && !f.includes("#### Title"))
156170
expect(next).toContain("Title")
157171
expect(next).not.toContain("#### Title")
158172
}, WIDE)
@@ -240,7 +254,7 @@ describe("markdown transcript rows", () => {
240254
role: "assistant",
241255
text: ["### Title", "", "Here is the list:", "- alpha", "- beta"].join("\n"),
242256
})
243-
const frame = await settle(h)
257+
const frame = await settle(h, (f) => f.includes("alpha"))
244258
const lines = frame.split("\n").map((line) => line.trimEnd())
245259
const listLine = lines.findIndex((line) => line.includes("Here is the list:"))
246260
expect(listLine).toBeGreaterThan(-1)
@@ -257,7 +271,7 @@ describe("markdown transcript rows", () => {
257271
role: "assistant",
258272
text: ["### Steps", "", ...items].join("\n"),
259273
})
260-
const frame = await settle(h)
274+
const frame = await settle(h, (f) => f.includes("10. item 10"))
261275
expect(frame).toContain("1. item 1")
262276
expect(frame).toContain("10. item 10")
263277
}, WIDE)
@@ -290,7 +304,8 @@ describe("markdown transcript rows", () => {
290304
})
291305
// Warm up once: the first highlight pass in a process loads the
292306
// tree-sitter grammar and is not itself part of what this test samples.
293-
const baseline = await settle(h).then(() => headingSpan(h))
307+
await settle(h, () => headingSpan(h) !== null)
308+
const baseline = headingSpan(h)
294309
expect(baseline).not.toBeNull()
295310

296311
for (let i = 2; i <= full.length; i += 1) {
@@ -375,7 +390,10 @@ describe("markdown transcript rows", () => {
375390
"more prose streaming in",
376391
].join("\n"),
377392
})
378-
const frame = await settle(h)
393+
const frame = await settle(
394+
h,
395+
(f) => f.includes("# this is a comment, not a heading") && f.includes("more prose streaming in"),
396+
)
379397
expect(frame).toContain("# this is a comment, not a heading")
380398
expect(frame).toContain("echo hi")
381399
expect(frame).toContain("more prose streaming in")

src/tui-opentui/shell.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,9 @@ describe("product skin: stream + queue + overlay", () => {
355355
expect(frame).toContain("hi there")
356356
expect(frame).toContain("bash")
357357
// One agent is answering, so no row spends columns naming it.
358-
expect(frame).not.toContain("agent")
359-
expect(frame).not.toContain(" tool ")
358+
const inkRows = frame.split("\n").filter((row) => row.trim().length > 0)
359+
expect(inkRows.filter((row) => row.includes("● agent"))).toHaveLength(0)
360+
expect(inkRows.filter((row) => row.includes(" tool "))).toHaveLength(0)
360361
// User row content is in the scroll buffer (pure paint covered in stream.test).
361362
expect(
362363
paintStreamRow(

src/tui-opentui/transcript-layout.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ describe("transcript turn layout", () => {
7373
expect(row).toBeDefined()
7474
const painted = row as string
7575
expect(painted.indexOf("listing")).toBe(resolveSideMargin(80))
76-
expect(frame).not.toContain("agent")
77-
expect(frame).not.toContain("you")
76+
expect(rowsContaining(frame, "● agent")).toHaveLength(0)
77+
expect(rowsContaining(frame, "● you")).toHaveLength(0)
7878
},
7979
)
8080
})

tests/unit/ripgrep-plugin.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
55
import type { ToolCall, ToolResult } from "@intx/types/runtime";
66

77
import { ripgrepPlugin } from "../../src/plugins/ripgrep-plugin.js";
8+
import type { RgChild, SpawnRg } from "../../src/plugins/rg-run.js";
89

910
// Repo root derived from this file, not process.cwd(): these cases search real
1011
// repo paths, so they must not depend on where the runner was invoked from.
@@ -14,11 +15,23 @@ const fallback = async (): Promise<ToolResult> => ({ callId: "c", content: "FALL
1415
function run(
1516
call: ToolCall,
1617
limits: { timeoutMs?: number; maxOutputBytes?: number } = {},
18+
spawnChild?: SpawnRg,
1719
): Promise<ToolResult> {
18-
const handler = ripgrepPlugin(cwd, limits).middleware!(fallback);
20+
const handler = ripgrepPlugin(cwd, limits, spawnChild).middleware!(fallback);
1921
return handler(call, new AbortController().signal);
2022
}
2123

24+
// A child that never emits data or closes, so the timeout is the only path
25+
// to settlement — the trigger the timeout test needs, not a race against how
26+
// fast a real ripgrep process happens to run.
27+
const stalledSpawn: SpawnRg = (): RgChild => ({
28+
pid: undefined,
29+
stdout: { on: () => undefined },
30+
stderr: { on: () => undefined },
31+
on: (() => undefined) as RgChild["on"],
32+
kill: () => undefined,
33+
});
34+
2235
async function withTempDir(run: (dir: string) => Promise<void>): Promise<void> {
2336
const dir = await mkdtemp(join(tmpdir(), "ripgrep-plugin-"));
2437
try {
@@ -114,6 +127,7 @@ test("grep returns partial matches when the timeout fires", async () => {
114127
const result = await run(
115128
{ id: "c", name: "grep", arguments: { pattern: "e", path: "src" } },
116129
{ timeoutMs: 1 },
130+
stalledSpawn,
117131
);
118132
expect(result.isError).toBeUndefined();
119133
expect(result.content).toContain("timed out");

0 commit comments

Comments
 (0)