796 lines · 38.7 KB · Zig
RawDownload
const std = @import("std");const types = @import("../../../shared/types.zig");const session_runtime = @import("../../../session/session.zig");const debug_trace = @import("../../../shared/debug_trace.zig");const io_mod = @import("../../../shared/io.zig"); const test_support = @import("support.zig"); const Allocator = std.mem.Allocator;const HistoryTurn = types.HistoryTurn;const ToolCall = types.ToolCall; const removed_direct_question_guidance = "Treat it as interrupting any previous tool plan.";const removed_resume_guidance = "Continue from the latest meaningful state";const fixture_tools_json =    "[{\"type\":\"function\",\"name\":\"read_file\",\"description\":\"Read a file\",\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}]"; const FakeCompletion = test_support.FakeCompletion;const FakeGateway = test_support.FakeGateway;const FakeAgentRuntimeDeps = test_support.FakeAgentRuntimeDeps;const PromptFixture = test_support.PromptFixture; const runFakePrompt = test_support.runFakePrompt;const expectBodyContains = test_support.expectBodyContains;const expectBodyNotContains = test_support.expectBodyNotContains;const expectBodyContainsInOrder = test_support.expectBodyContainsInOrder;const expectGatewayPromptFinalUserText = test_support.expectGatewayPromptFinalUserText;const readTraceFile = test_support.readTraceFile;const logIndex = test_support.logIndex;const toolCall = test_support.toolCall; const ExpectedToolTerminal = struct {    call_id: []const u8,    kind: types.ToolOutcomeKind,}; fn expectToolTerminalsBeforeTurnFinished(    hooks: *const FakeAgentRuntimeDeps,    expected: []const ExpectedToolTerminal,) !void {    const turn_finished_index = logIndex(hooks, "event:turn_finished") orelse        return error.TestMissingTurnFinalization;    var terminal_log_count: usize = 0;    for (hooks.log.items, 0..) |entry, entry_index| {        if (!std.mem.startsWith(u8, entry, "status:finished:")) continue;        terminal_log_count += 1;        try std.testing.expect(entry_index < turn_finished_index);    }    try std.testing.expectEqual(expected.len, terminal_log_count);     for (expected) |wanted| {        var terminal_count: usize = 0;        for (hooks.lifecycle_events.items) |event| {            if (event != .terminal) continue;            try std.testing.expect(!std.mem.eql(                u8,                event.terminal.outcome.summary,                "Tool cancelled",            ));            if (!std.mem.eql(u8, event.terminal.id.call_id, wanted.call_id)) continue;            terminal_count += 1;            try std.testing.expectEqual(wanted.kind, event.terminal.outcome.kind);        }        try std.testing.expectEqual(@as(usize, 1), terminal_count);    }} fn expectGatewayPromptRoleContentKinds(gateway: *const FakeGateway, index: usize) !void {    const alloc = std.testing.allocator;    try std.testing.expect(index < gateway.request_bodies.items.len);     var parsed = try std.json.parseFromSlice(std.json.Value, alloc, gateway.request_bodies.items[index], .{});    defer parsed.deinit();     const prompt = parsed.value.object.get("prompt").?.array.items;    for (prompt) |entry| {        try std.testing.expect(entry == .object);        const role = entry.object.get("role") orelse return error.TestExpectedPromptRoleMissing;        try std.testing.expect(role == .string);        const content = entry.object.get("content") orelse return error.TestExpectedPromptMessageMissing;        if (std.mem.eql(u8, role.string, "system")) {            try std.testing.expect(content == .string);        } else if (std.mem.eql(u8, role.string, "user") or            std.mem.eql(u8, role.string, "assistant") or            std.mem.eql(u8, role.string, "tool"))        {            try std.testing.expect(content == .array);        } else {            return error.TestUnexpectedPromptRole;        }    }} test "processQueuedPrompt sends former intent text normally with tools" {    const alloc = std.testing.allocator;    const cases = [_][]const u8{        "What are you doing right now?",        "Continue from the last useful progress update.",    };     for (cases) |text| {        const completions = [_]FakeCompletion{.{ .content = "Context-grounded answer" }};        var gateway = FakeGateway.init(alloc, &completions);        defer gateway.deinit();        var hooks = FakeAgentRuntimeDeps.init(alloc);        defer hooks.deinit();        hooks.runtime_context_text = "runtime context unique";        var fixture = PromptFixture{};        var job = fixture.job();        job.prompt = @constCast(text);        var config = fixture.config();        config.gateway_tools_json = fixture_tools_json;         try runFakePrompt(&gateway, &hooks, config, job);         const body = gateway.request_bodies.items[0];        const runtime_idx = std.mem.indexOf(u8, body, "runtime context unique") orelse return error.TestExpectedEqual;        const current_idx = std.mem.indexOf(u8, body, text) orelse return error.TestExpectedEqual;        try std.testing.expect(runtime_idx < current_idx);        try expectGatewayPromptFinalUserText(&gateway, 0, text);        try expectBodyContains(&gateway, 0, "\"name\":\"read_file\"");        try expectBodyNotContains(&gateway, 0, removed_direct_question_guidance);        try expectBodyNotContains(&gateway, 0, removed_resume_guidance);    }} test "processQueuedPrompt persists no-output cancellation as interrupted once" {    const alloc = std.testing.allocator;    const completions = [_]FakeCompletion{.{ .cancel_before_output = true }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};     try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());     try std.testing.expect(fixture.cancel_flag.load(.seq_cst));    try std.testing.expectEqual(@as(usize, 1), hooks.finish_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_history_count);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);    try std.testing.expect(hooks.history_turns.items[0] == .interrupted);    try std.testing.expect(hooks.history_turns.items[0].interrupted.assistant == null);    try std.testing.expect(hooks.history_turns.items[0].interrupted.tool_call == null);} test "processQueuedPrompt persists partial-text cancellation as interrupted once" {    const alloc = std.testing.allocator;    const chunks = [_][]const u8{"Partial answer"};    const completions = [_]FakeCompletion{.{ .chunks = &chunks, .cancel_after_chunks = true }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};     try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());     try std.testing.expectEqual(@as(usize, 1), hooks.finish_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_history_count);    try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);    try std.testing.expect(hooks.history_turns.items[0] == .interrupted);    try std.testing.expectEqualStrings("Partial answer", hooks.history_turns.items[0].interrupted.assistant.?);    try std.testing.expect(hooks.history_turns.items[0].interrupted.tool_call == null);} test "streamed presentation preserves raw partial through cancellation" {    const alloc = std.testing.allocator;    const invisible_chunks = [_][]const u8{" \t\r\n"};    const visible_chunks = [_][]const u8{ " \t\r\n", "Visible content\n" };    const cases = [_]struct {        chunks: []const []const u8,        raw_assistant: []const u8,        rendered_text: ?[]const u8,    }{        .{            .chunks = &invisible_chunks,            .raw_assistant = " \t\r\n",            .rendered_text = null,        },        .{            .chunks = &visible_chunks,            .raw_assistant = " \t\r\nVisible content\n",            .rendered_text = "Visible content\n",        },    };     for (cases) |case| {        const completions = [_]FakeCompletion{.{ .chunks = case.chunks, .cancel_after_chunks = true }};        var gateway = FakeGateway.init(alloc, &completions);        defer gateway.deinit();        var hooks = FakeAgentRuntimeDeps.init(alloc);        defer hooks.deinit();        var fixture = PromptFixture{};         try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());         try std.testing.expect(fixture.cancel_flag.load(.seq_cst));        try std.testing.expectEqual(@as(usize, 1), gateway.index);        try std.testing.expectEqual(@as(usize, 1), gateway.request_bodies.items.len);        try std.testing.expectEqual(@as(usize, 0), hooks.permission_names.items.len);        try std.testing.expectEqual(@as(usize, 0), hooks.executed_names.items.len);        try std.testing.expectEqual(@as(usize, 1), hooks.finalization_count);        try std.testing.expectEqual(types.TurnPresentationOutcome.interrupted, hooks.finalized_outcome.?);        try std.testing.expectEqual(@as(usize, 1), hooks.finish_event_count);        try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_history_count);        try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_event_count);        try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);        try std.testing.expect(hooks.history_turns.items[0] == .interrupted);        try std.testing.expectEqualStrings(case.raw_assistant, hooks.history_turns.items[0].interrupted.assistant.?);        try std.testing.expect(hooks.history_turns.items[0].interrupted.tool_call == null);         if (case.rendered_text) |text| {            try std.testing.expectEqual(@as(usize, 1), hooks.texts.items.len);            try std.testing.expectEqualStrings(text, hooks.texts.items[0]);        } else {            try std.testing.expectEqual(@as(usize, 0), hooks.texts.items.len);        }    }} test "processQueuedPrompt cancellation after valid tool finish settles streamed calls" {    const alloc = std.testing.allocator;    const calls = [_]ToolCall{        .{            .id = "call_search",            .name = "web_search",            .arguments_json = "{\"query\":\"zig\"}",            .provider_result = "{\"results\":[]}",            .provenance = .provider_executed,        },        toolCall("call_read", "read_file", "{\"path\":\"README.md\"}"),    };    const completions = [_]FakeCompletion{.{        .tool_calls = &calls,        .streamed_tool_starts = &calls,        .finish_reason = .tool_calls,        .cancel_after_chunks = true,    }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};     try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());     try std.testing.expectEqual(@as(usize, 0), hooks.permission_names.items.len);    try std.testing.expectEqual(@as(usize, 0), hooks.executed_names.items.len);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_history_count);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.finish_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);    try std.testing.expect(hooks.history_turns.items[0] == .interrupted);    try expectToolTerminalsBeforeTurnFinished(&hooks, &.{        .{ .call_id = "call_search", .kind = .completed },        .{ .call_id = "call_read", .kind = .cancelled },    });} test "processQueuedPrompt in-stream cancellation settles every provisional start" {    const alloc = std.testing.allocator;    const calls = [_]ToolCall{        toolCall("call_command", "terminal", "{}"),        toolCall("call_read", "read_file", "{}"),    };    const completions = [_]FakeCompletion{.{        .streamed_tool_starts = &calls,        .cancel_during_tool_stream = true,    }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};     try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());     try std.testing.expectEqual(@as(usize, 0), hooks.permission_names.items.len);    try std.testing.expectEqual(@as(usize, 0), hooks.executed_names.items.len);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_history_count);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.finish_event_count);    try expectToolTerminalsBeforeTurnFinished(&hooks, &.{        .{ .call_id = "call_command", .kind = .cancelled },        .{ .call_id = "call_read", .kind = .cancelled },    });} test "processQueuedPrompt persists completed tool names when cancelled during next gateway wait" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(root);    const trace_path = try std.fs.path.join(alloc, &.{ root, "completed-tools-interrupt.log" });    defer alloc.free(trace_path);     debug_trace.resetForTest();    defer debug_trace.resetForTest();    try debug_trace.configureForTestWithScopes(alloc, trace_path, "interrupt");     const calls = [_]ToolCall{        toolCall("call_list", "list_files", "{\"path\":\".\"}"),        toolCall("call_glob", "glob_files", "{\"pattern\":\"*\"}"),    };    const completions = [_]FakeCompletion{        .{ .tool_calls = &calls },        .{ .cancel_before_output = true },    };    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};    var job = fixture.job();    job.prompt = @constCast("use all ur tools 1 by 1 - I need u to test them");     try runFakePrompt(&gateway, &hooks, fixture.config(), job);    debug_trace.shutdown();     try std.testing.expectEqual(@as(usize, 2), hooks.executed_names.items.len);    try std.testing.expectEqualStrings("list_files", hooks.executed_names.items[0]);    try std.testing.expectEqualStrings("glob_files", hooks.executed_names.items[1]);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_history_count);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);    try std.testing.expect(hooks.history_turns.items[0] == .interrupted);    try std.testing.expect(hooks.history_turns.items[0].interrupted.tool_call == null);    try std.testing.expectEqual(@as(usize, 2), hooks.history_turns.items[0].interrupted.completed_tool_names.len);    try std.testing.expectEqualStrings("list_files", hooks.history_turns.items[0].interrupted.completed_tool_names[0]);    try std.testing.expectEqualStrings("glob_files", hooks.history_turns.items[0].interrupted.completed_tool_names[1]);     const trace = try readTraceFile(alloc, trace_path, 65536);    defer alloc.free(trace);    try std.testing.expect(std.mem.find(u8, trace, "active_tool=false completed_tool_count=2 completed_tool_names=list_files,glob_files") != null);     const follow_completions = [_]FakeCompletion{.{ .content = "I ran 2 tools: list_files and glob_files." }};    var follow_gateway = FakeGateway.init(alloc, &follow_completions);    defer follow_gateway.deinit();    var follow_hooks = FakeAgentRuntimeDeps.init(alloc);    defer follow_hooks.deinit();    var follow_fixture = PromptFixture{};    var follow_job = follow_fixture.job();    follow_job.prompt = @constCast("how many did u run up until now ?");    follow_job.history = hooks.history_turns.items;     try runFakePrompt(&follow_gateway, &follow_hooks, follow_fixture.config(), follow_job);     const expected_order = [_][]const u8{        "use all ur tools 1 by 1 - I need u to test them",        "Interrupted by user after completing 2 tool calls: list_files, glob_files.",        "<turn_aborted>",        "how many did u run up until now ?",    };    try expectBodyContainsInOrder(&follow_gateway, 0, &expected_order);    try std.testing.expectEqual(@as(usize, 0), follow_hooks.executed_names.items.len);} test "processQueuedPrompt projects no-output interrupted turn as closed before follow-up" {    const alloc = std.testing.allocator;    const interrupted_prompt = "create another file in the folder called test.md and add 200 lines in that - anything";    var history = [_]HistoryTurn{.{ .interrupted = .{        .user = .{ .text = @constCast(interrupted_prompt) },    } }};    const completions = [_]FakeCompletion{.{ .content = "The previous request was interrupted before I completed it." }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};    var job = fixture.job();    job.prompt = @constCast("what happened?");    job.history = history[0..];     try runFakePrompt(&gateway, &hooks, fixture.config(), job);     const expected_order = [_][]const u8{        interrupted_prompt,        session_runtime.interrupted_before_completion_output,        "<turn_aborted>",        "what happened?",    };    try expectBodyContainsInOrder(&gateway, 0, &expected_order);    try expectBodyNotContains(&gateway, 0, session_runtime.aborted_tool_output);    try expectBodyNotContains(&gateway, 0, "\"toolCallId\"");    try expectBodyNotContains(&gateway, 0, removed_direct_question_guidance);    try expectBodyNotContains(&gateway, 0, removed_resume_guidance);     const body = gateway.request_bodies.items[0];    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, interrupted_prompt));    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, session_runtime.interrupted_before_completion_output));    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, "<turn_aborted>"));    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, "what happened?"));} test "processQueuedPrompt projects partial-text interrupted turn as closed before meta follow-up" {    const alloc = std.testing.allocator;    const interrupted_prompt = "tell me a slow story in 1200 words, stream it gradually if possible";    const partial_story = "Once the keeper reached the lighthouse, she lifted the lantern";    var history = [_]HistoryTurn{.{ .interrupted = .{        .user = .{ .text = @constCast(interrupted_prompt) },        .assistant = @constCast(partial_story),    } }};    const completions = [_]FakeCompletion{.{ .content = "The previous story turn was interrupted by the user." }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};    var job = fixture.job();    job.prompt = @constCast("what happened?");    job.history = history[0..];     try runFakePrompt(&gateway, &hooks, fixture.config(), job);     const expected_order = [_][]const u8{        interrupted_prompt,        partial_story,        session_runtime.interrupted_before_completion_output,        "<turn_aborted>",        "what happened?",    };    try expectBodyContainsInOrder(&gateway, 0, &expected_order);    try expectBodyNotContains(&gateway, 0, session_runtime.aborted_tool_output);    try expectBodyNotContains(&gateway, 0, removed_direct_question_guidance);    try expectBodyNotContains(&gateway, 0, removed_resume_guidance);    try expectBodyNotContains(&gateway, 0, "gradually if possiblewhat happened?");    try expectBodyNotContains(&gateway, 0, "lanternwhat happened?");     const body = gateway.request_bodies.items[0];    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, interrupted_prompt));    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, partial_story));    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, session_runtime.interrupted_before_completion_output));    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, "<turn_aborted>"));    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, body, "what happened?"));} test "processQueuedPrompt persists interrupted turn with aborted tool output for follow-up" {    const alloc = std.testing.allocator;    const calls = [_]ToolCall{toolCall("call_browser", "browser_snapshot", "{}")};    const completions = [_]FakeCompletion{.{ .tool_calls = &calls }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};    hooks.exec_plans = &.{.{ .err = error.Cancelled }};    hooks.cancel_on_execute = &fixture.cancel_flag;     try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());     try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_history_count);    try std.testing.expectEqual(@as(usize, 1), hooks.interrupted_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.finish_event_count);    try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);    try std.testing.expect(hooks.history_turns.items[0] == .interrupted);    try std.testing.expectEqualStrings("browser_snapshot", hooks.interrupted_tool_name.?);    try std.testing.expectEqualStrings("browser_snapshot", hooks.history_turns.items[0].interrupted.tool_call.?.name);     const follow_completions = [_]FakeCompletion{.{ .content = "The previous turn was interrupted." }};    var follow_gateway = FakeGateway.init(alloc, &follow_completions);    defer follow_gateway.deinit();    var follow_hooks = FakeAgentRuntimeDeps.init(alloc);    defer follow_hooks.deinit();    var follow_fixture = PromptFixture{};    var follow_job = follow_fixture.job();    follow_job.prompt = @constCast("what happened?");    follow_job.history = hooks.history_turns.items;     try runFakePrompt(&follow_gateway, &follow_hooks, follow_fixture.config(), follow_job);     try expectBodyContains(&follow_gateway, 0, "<turn_aborted>");    try expectBodyContains(&follow_gateway, 0, session_runtime.aborted_tool_output);    try expectBodyContains(&follow_gateway, 0, "\"toolName\":\"browser_snapshot\"");    try expectBodyContains(&follow_gateway, 0, "\"toolCallId\":\"call_browser\"");    try expectBodyNotContains(&follow_gateway, 0, "Continue from where you left off.");    try expectBodyNotContains(&follow_gateway, 0, removed_direct_question_guidance);    try expectBodyNotContains(&follow_gateway, 0, removed_resume_guidance);    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, follow_gateway.request_bodies.items[0], "<turn_aborted>"));    try expectGatewayPromptRoleContentKinds(&follow_gateway, 0);} test "processQueuedPrompt retains cancelled command artifact for presentation only" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(root);    const trace_path = try std.fs.path.join(alloc, &.{ root, "cancelled-command.log" });    defer alloc.free(trace_path);     debug_trace.resetForTest();    defer debug_trace.resetForTest();    try debug_trace.configureForTestWithScopes(alloc, trace_path, "interrupt");     const artifact_path = "/tmp/session/logs/commands/fx-command-cancelled.log";    const artifact_handle = "fx-command-cancelled.log";    const result_output = "RESULT-ONLY-OUTPUT-SENTINEL\nTERM-TAIL-SENTINEL\n";

Lines 1–500 of 796.