diff --git a/src/tools/filesystem/grep_files.zig b/src/tools/filesystem/grep_files.zig index f2244c6f3..561136e22 100644 --- a/src/tools/filesystem/grep_files.zig +++ b/src/tools/filesystem/grep_files.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const build_options = @import("build_options"); const glob_pattern = @import("../../core/workspace/glob_pattern.zig"); const grep_search = @import("../../core/workspace/grep_search.zig"); const tool_files = @import("../../core/workspace/tool_files.zig"); @@ -165,7 +166,22 @@ pub fn validate(_: tool_dispatch.DispatchContext, _: tool_dispatch.ToolInput) to /// Searches the resolved root and returns an owned tool result body. pub fn call(ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput) tool_dispatch.DispatchError!tool_dispatch.ToolResult { - return callWithOps(ctx, erased, .{}); + const result = try callWithOps(ctx, erased, .{}); + holdForTest(); + return result; +} + +// Only `tests/e2e/tui-interrupt-recovery.test.ts` sets +// `FX_E2E_GREP_RELEASE`: a finished search then waits until the file it +// names exists, so the test can cancel the turn while the tool is still +// running. The libfx artifacts leave it out. +fn holdForTest() void { + if (comptime build_options.libfx) return; + const release_path = io_mod.getenv("FX_E2E_GREP_RELEASE") orelse return; + while (true) : (io_mod.sleep(std.time.ns_per_ms)) { + std.Io.Dir.accessAbsolute(io_mod.getIo(), release_path, .{}) catch continue; + return; + } } const StatRootFn = *const fn ([]const u8) anyerror!std.Io.File.Stat; diff --git a/tests/e2e/tui-interrupt-recovery.test.ts b/tests/e2e/tui-interrupt-recovery.test.ts index b67d7bfdb..96dcfdb26 100644 --- a/tests/e2e/tui-interrupt-recovery.test.ts +++ b/tests/e2e/tui-interrupt-recovery.test.ts @@ -853,20 +853,14 @@ while :; do sleep 1; done const corpus = join(workspace, "corpus"); const stderrPath = join(root, "stderr.log"); const tracePath = join(root, "trace.log"); + const releasePath = join(root, "grep-release"); const pattern = "LATE_SUCCESS_NEEDLE"; const callId = "late_success_grep"; mkdirSync(join(home, ".fx"), { recursive: true }); mkdirSync(corpus, { recursive: true }); writeFileSync(join(home, ".fx", "settings.json"), "{}"); - // The search must still be running when the second Escape lands, about - // half a second after it starts. Linux searches 30,000 files in about a - // third of a second, so it needs a larger corpus than macOS. - const corpusFiles = process.platform === "darwin" ? 30_000 : 120_000; - for (let index = 0; index < corpusFiles; index += 1) { - writeFileSync( - join(corpus, `candidate-${String(index).padStart(6, "0")}.txt`), - "ordinary corpus text\n", - ); + for (let index = 0; index < 3; index += 1) { + writeFileSync(join(corpus, `candidate-${index}.txt`), "ordinary corpus text\n"); } gateway = startFakeGateway([ @@ -892,6 +886,9 @@ while :; do sleep 1; done FX_MODEL: FAKE_GATEWAY_MODEL, FX_TRACE_SCOPES: `${TRACE_SCOPES},tool,ui_activity`, FX_TRACE_LOG: tracePath, + // The search holds its result until this file exists, so it is + // still running when the turn is cancelled. + FX_E2E_GREP_RELEASE: releasePath, }, }); await session.waitForComposer(TIMEOUT); @@ -909,6 +906,10 @@ while :; do sleep 1; done await session.sendKeys("Escape"); await session.waitForText("What can fx do differently?", TIMEOUT); expect(Date.now() - cancelStartedAt).toBeLessThan(500); + expect(readTrace(tracePath)).not.toContain( + `event=after_tool_execution call_id=${callId}`, + ); + writeFileSync(releasePath, ""); await waitForTrace(tracePath, `event=after_tool_execution`, TIMEOUT); await waitForTrace(tracePath, "finish processing queued=0", TIMEOUT);