1786 lines · 69.0 KB · Zig
RawDownload
const std = @import("std");const types = @import("../../../shared/types.zig");const debug_trace = @import("../../../shared/debug_trace.zig");const io_mod = @import("../../../shared/io.zig");const tool_result_errors = @import("../../../tooling/tool_result_errors.zig");const lifecycle_hooks = @import("../../../hooks/hooks.zig");const session_runtime = @import("../../../session/session.zig"); const test_support = @import("support.zig");const runtime_finalization = @import("../finalization.zig");const runtime_orchestrator = @import("../orchestrator.zig");const runtime_telemetry = @import("../telemetry.zig"); const ChatMessage = types.ChatMessage;const ToolCall = types.ToolCall;const TurnFinalizationGuard = runtime_finalization.TurnFinalizationGuard;const PromptFinishTrace = runtime_finalization.PromptFinishTrace;const TurnSummaryAccumulator = runtime_telemetry.TurnSummaryAccumulator;const CommonStopState = runtime_orchestrator.CommonStopState;const copyLatestStopPartial = runtime_orchestrator.copyLatestStopPartial; const FakeCompletion = test_support.FakeCompletion;const FakeGateway = test_support.FakeGateway;const FakeAgentRuntimeDeps = test_support.FakeAgentRuntimeDeps;const FakeExecPlan = test_support.FakeExecPlan;const PromptFixture = test_support.PromptFixture;const StopTestHandler = test_support.StopTestHandler; const runFakePrompt = test_support.runFakePrompt;const runFakePromptWithLifecycle = test_support.runFakePromptWithLifecycle;const registerStopTestHandler = test_support.registerStopTestHandler;const testLifecycleContext = test_support.testLifecycleContext;const finishCommonAssistantTerminal = runtime_orchestrator.finishCommonAssistantTerminal;const expectBodyContains = test_support.expectBodyContains;const expectBodyNotContains = test_support.expectBodyNotContains;const countText = test_support.countText;const countNeedle = test_support.countNeedle;const readTraceFile = test_support.readTraceFile;const textContains = test_support.textContains;const logIndex = test_support.logIndex;const toolCall = test_support.toolCall; const PostTurnEndFinalizationCapture = struct {    calls: usize = 0,    turn_ids: [8]u64 = undefined,    scope_kinds: [8]lifecycle_hooks.ScopeKind = undefined,    workspace_matches: [8]bool = undefined,    outcomes: [8]types.TurnPresentationOutcome = undefined,    dispositions: [8]?types.ProviderCompletionDisposition = undefined,    finish_event_attempts: [8]usize = undefined,    deps: ?*FakeAgentRuntimeDeps = null,     fn run(raw: *anyopaque, input: lifecycle_hooks.PostTurnEndInput) lifecycle_hooks.HandlerError!void {        const self: *PostTurnEndFinalizationCapture = @ptrCast(@alignCast(raw));        const index = self.calls;        self.calls += 1;        self.turn_ids[index] = input.invocation.turn_id orelse 0;        self.scope_kinds[index] = input.invocation.scope.kind;        self.workspace_matches[index] = std.mem.eql(            u8,            input.invocation.scope.workspace_root,            "/tmp/workspace",        );        self.outcomes[index] = input.outcome;        self.dispositions[index] = input.provider_disposition;        self.finish_event_attempts[index] = if (self.deps) |deps|            deps.finish_event_attempt_count        else            0;    }}; test "processQueuedPrompt normal final completion propagates normalized history before finish event" {    const alloc = std.testing.allocator;    const chunks = [_][]const u8{" **Hello** "};    const completions = [_]FakeCompletion{.{        .chunks = &chunks,        .content = " **Hello** ",    }};    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.expectEqualStrings("Hello", hooks.history_assistant_text.?);    try std.testing.expectEqualStrings("Hello", hooks.finish_assistant_text.?);    try std.testing.expectEqual(@as(?types.TurnPresentationOutcome, .completed), hooks.finish_terminal_outcome);    const newline_idx = logIndex(&hooks, "text:newline").?;    const finish_idx = logIndex(&hooks, "event:finish_prompt").?;    try std.testing.expect(newline_idx < finish_idx);    try expectBodyNotContains(&gateway, 0, "<turn_aborted>");} test "processQueuedPrompt pauses missing finish without synthesizing output" {    const alloc = std.testing.allocator;    const completions = [_]FakeCompletion{.{ .omit_finish = true }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};     var config = fixture.config();    config.max_provider_attempts = 1;    try runFakePrompt(&gateway, &hooks, config, fixture.job());     try std.testing.expectEqual(@as(usize, 0), hooks.finish_event_count);    try std.testing.expectEqual(@as(usize, 0), hooks.history_turns.items.len);    try std.testing.expectEqual(@as(usize, 0), hooks.system_notices.items.len);    try std.testing.expect(!textContains(&hooks, "Done."));    try std.testing.expectEqual(types.TurnPresentationOutcome.paused, hooks.finalized_outcome.?);} test "processQueuedPrompt pauses partial text without finish proof" {    const alloc = std.testing.allocator;    const chunks = [_][]const u8{"Partial answer"};    const completions = [_]FakeCompletion{.{        .chunks = &chunks,        .omit_finish = true,    }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};     var config = fixture.config();    config.max_provider_attempts = 1;    try runFakePrompt(&gateway, &hooks, config, fixture.job());     try std.testing.expect(textContains(&hooks, "Partial answer"));    try std.testing.expect(!textContains(&hooks, "Done."));    try std.testing.expectEqual(@as(usize, 0), hooks.finish_event_count);    try std.testing.expectEqual(@as(usize, 0), hooks.history_turns.items.len);    try std.testing.expectEqual(@as(usize, 0), hooks.system_notices.items.len);    try std.testing.expectEqual(types.TurnPresentationOutcome.paused, hooks.finalized_outcome.?);} test "processQueuedPrompt pauses tool calls without finish proof" {    const alloc = std.testing.allocator;    const calls = [_]ToolCall{toolCall("call_write", "write_file", "{\"path\":\"a.txt\",\"content\":\"x\"}")};    const completions = [_]FakeCompletion{.{        .tool_calls = &calls,        .omit_finish = true,    }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};    var config = fixture.config();    config.agent_step_limit = 1;    config.max_provider_attempts = 1;     try runFakePrompt(&gateway, &hooks, 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, 0), hooks.finish_event_count);    try std.testing.expectEqual(@as(usize, 0), hooks.history_turns.items.len);    try std.testing.expectEqual(types.TurnPresentationOutcome.paused, hooks.finalized_outcome.?);} test "processQueuedPrompt preserves finish precedence over malformed argument recovery" {    const alloc = std.testing.allocator;    const calls = [_]ToolCall{.{        .id = "call_bad",        .name = "read_file",        .arguments_json = "{}",        .argument_integrity = .malformed_json,    }};    const Expected = enum {        paused,        model_error,        length_limited,    };    const Case = struct {        finish_reason: ?types.ProviderFinishReason = null,        omit_finish: bool = false,        expected: Expected,    };    const cases = [_]Case{        .{ .omit_finish = true, .expected = .paused },        .{ .finish_reason = .provider_error, .expected = .paused },        .{ .finish_reason = .stop, .expected = .model_error },        .{ .finish_reason = .length, .expected = .length_limited },    };     for (cases) |case| {        const completions = [_]FakeCompletion{.{            .tool_calls = &calls,            .finish_reason = case.finish_reason,            .omit_finish = case.omit_finish,        }};        var gateway = FakeGateway.init(alloc, &completions);        defer gateway.deinit();        var hooks = FakeAgentRuntimeDeps.init(alloc);        if (case.finish_reason == .provider_error) hooks.enable_route_recovery = false;        defer hooks.deinit();        var fixture = PromptFixture{};         var config = fixture.config();        config.max_provider_attempts = 1;        switch (case.expected) {            .paused => try runFakePrompt(&gateway, &hooks, config, fixture.job()),            .model_error => try std.testing.expectError(                error.ModelError,                runFakePrompt(&gateway, &hooks, config, fixture.job()),            ),            .length_limited => try runFakePrompt(                &gateway,                &hooks,                config,                fixture.job(),            ),        }         try std.testing.expectEqual(@as(usize, 1), gateway.request_bodies.items.len);        try std.testing.expectEqual(@as(usize, 0), hooks.validated_names.items.len);        try std.testing.expectEqual(@as(usize, 0), hooks.availability_checked_names.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, 0), hooks.rejected_names.items.len);         if (case.expected == .length_limited) {            try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);            try std.testing.expectEqual(                @as(usize, 0),                hooks.history_turns.items[0].assistant.execution.tool_steps.len,            );        } else {            try std.testing.expectEqual(@as(usize, 0), hooks.history_turns.items.len);        }        if (case.expected == .paused) {            try std.testing.expectEqual(types.TurnPresentationOutcome.paused, hooks.finalized_outcome.?);        }    }} test "processQueuedPrompt returns a final response after repeated tool failures" {    const alloc = std.testing.allocator;    const call_one = [_]ToolCall{toolCall("call_1", "read_file", "{\"path\":\"a\"}")};    const call_two = [_]ToolCall{toolCall("call_2", "read_file", "{\"path\":\"b\"}")};    const call_three = [_]ToolCall{toolCall("call_3", "read_file", "{\"path\":\"c\"}")};    const completions = [_]FakeCompletion{        .{ .tool_calls = &call_one },        .{ .tool_calls = &call_two },        .{ .tool_calls = &call_three },        .{ .content = "Recovered after errors." },    };    const error_result = FakeExecPlan{ .result = .{ .model_output = "Tool read_file failed: bad" } };    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    hooks.permission_decisions = &.{ .once, .once, .once };    hooks.exec_plans = &.{ error_result, error_result, error_result };    defer hooks.deinit();    var fixture = PromptFixture{};     try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());     try std.testing.expect(!textContains(&hooks, "Agent stopped: 3 consecutive steps with all-error tool calls."));    try std.testing.expectEqualStrings("Recovered after errors.", hooks.history_assistant_text.?);    try std.testing.expectEqual(@as(usize, 3), hooks.permission_names.items.len);    try std.testing.expectEqual(@as(usize, 3), hooks.executed_names.items.len);} test "processQueuedPrompt returns a final response after repeated malformed calls" {    const alloc = std.testing.allocator;    const call_one = [_]ToolCall{.{        .id = "call_1",        .name = "read_file",        .arguments_json = "{}",        .argument_integrity = .malformed_json,    }};    const call_two = [_]ToolCall{.{        .id = "call_2",        .name = "read_file",        .arguments_json = "{}",        .argument_integrity = .malformed_json,    }};    const call_three = [_]ToolCall{.{        .id = "call_3",        .name = "read_file",        .arguments_json = "{}",        .argument_integrity = .malformed_json,    }};    const completions = [_]FakeCompletion{        .{ .tool_calls = &call_one },        .{ .tool_calls = &call_two },        .{ .tool_calls = &call_three },        .{ .content = "Recovered after malformed arguments." },    };    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(!textContains(&hooks, "Agent stopped: 3 consecutive steps with all-error tool calls."));    try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);    try std.testing.expectEqual(@as(usize, 1), hooks.finalization_count);    try std.testing.expectEqual(@as(usize, 1), hooks.finish_event_count);    try std.testing.expectEqual(types.TurnPresentationOutcome.completed, hooks.finalized_outcome.?);    try std.testing.expectEqualStrings("Recovered after malformed arguments.", hooks.history_assistant_text.?);    try std.testing.expectEqualStrings("Recovered after malformed arguments.", hooks.finish_assistant_text.?);    try std.testing.expect(        logIndex(&hooks, "event:turn_finished").? <            logIndex(&hooks, "event:finish_prompt").?,    );     const execution = hooks.history_turns.items[0].assistant.execution;    try std.testing.expectEqual(@as(usize, 3), execution.tool_steps.len);    for (execution.tool_steps) |step| {        try std.testing.expectEqual(@as(usize, 1), step.tool_calls.len);        try std.testing.expectEqualStrings("{}", step.tool_calls[0].arguments_json);        try std.testing.expectEqual(types.ToolArgumentIntegrity.valid, step.tool_calls[0].argument_integrity);        try std.testing.expectEqual(@as(usize, 1), step.tool_results.len);        try std.testing.expectEqual(types.PersistedToolStatus.failure, step.tool_results[0].status);        try std.testing.expect(tool_result_errors.isToolExecutionFailedOutput(step.tool_results[0].output));        try std.testing.expectEqualStrings(step.tool_calls[0].id, step.tool_results[0].tool_call_id);    }    try std.testing.expectEqual(@as(usize, 0), hooks.permission_names.items.len);    try std.testing.expectEqual(@as(usize, 0), hooks.executed_names.items.len);} test "processQueuedPrompt returns a final response after a repeated tool-name cycle" {    const alloc = std.testing.allocator;    const c1 = [_]ToolCall{toolCall("call_1", "read_file", "{\"path\":\"a\"}")};    const c2 = [_]ToolCall{toolCall("call_2", "edit_file", "{\"path\":\"a\",\"old_string\":\"old\",\"new_string\":\"new\"}")};    const c3 = [_]ToolCall{toolCall("call_3", "read_file", "{\"path\":\"b\"}")};    const c4 = [_]ToolCall{toolCall("call_4", "edit_file", "{\"path\":\"b\",\"old_string\":\"old\",\"new_string\":\"new\"}")};    const c5 = [_]ToolCall{toolCall("call_5", "read_file", "{\"path\":\"c\"}")};    const c6 = [_]ToolCall{toolCall("call_6", "edit_file", "{\"path\":\"c\",\"old_string\":\"old\",\"new_string\":\"new\"}")};    const completions = [_]FakeCompletion{        .{ .tool_calls = &c1 },        .{ .tool_calls = &c2 },        .{ .tool_calls = &c3 },        .{ .tool_calls = &c4 },        .{ .tool_calls = &c5 },        .{ .tool_calls = &c6 },        .{ .content = "Recovered after cycle." },    };    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    hooks.permission_decisions = &.{ .once, .once, .once, .once, .once, .once };    defer hooks.deinit();    var fixture = PromptFixture{};     try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());     try std.testing.expect(!textContains(&hooks, "Agent stopped: detected repeating tool call cycle."));    try std.testing.expectEqualStrings("Recovered after cycle.", hooks.history_assistant_text.?);    try std.testing.expectEqual(@as(usize, 6), hooks.permission_names.items.len);} test "processQueuedPrompt step limit pushes configured notice and finish event" {    const alloc = std.testing.allocator;    const calls = [_]ToolCall{toolCall("call_1", "read_file", "{\"path\":\"a\"}")};    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{};    var config = fixture.config();    config.agent_step_limit = 1;    config.step_limit_notice = "custom limit";     try runFakePrompt(&gateway, &hooks, config, fixture.job());     try std.testing.expect(textContains(&hooks, "custom limit"));    try std.testing.expectEqualStrings("custom limit", hooks.history_assistant_text.?);    try std.testing.expectEqualStrings("custom limit", hooks.finish_assistant_text.?);    try std.testing.expect(hooks.finalized_disposition == null);} test "processQueuedPrompt step limit persists malformed argument failure execution" {    const alloc = std.testing.allocator;    const calls = [_]ToolCall{.{        .id = "call_bad",        .name = "read_file",        .arguments_json = "{}",        .argument_integrity = .malformed_json,    }};    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{};    var config = fixture.config();    config.agent_step_limit = 1;    config.step_limit_notice = "custom limit";     try runFakePrompt(&gateway, &hooks, config, fixture.job());     try std.testing.expectEqual(@as(usize, 1), hooks.history_turns.items.len);    const execution = hooks.history_turns.items[0].assistant.execution;    try std.testing.expectEqual(@as(usize, 1), execution.tool_steps.len);    try std.testing.expectEqualStrings("{}", execution.tool_steps[0].tool_calls[0].arguments_json);    try std.testing.expectEqual(types.ToolArgumentIntegrity.valid, execution.tool_steps[0].tool_calls[0].argument_integrity);    try std.testing.expectEqual(@as(usize, 1), execution.tool_steps[0].tool_results.len);    try std.testing.expectEqual(types.PersistedToolStatus.failure, execution.tool_steps[0].tool_results[0].status);    try std.testing.expect(tool_result_errors.isToolExecutionFailedOutput(execution.tool_steps[0].tool_results[0].output));    try std.testing.expectEqual(@as(usize, 0), hooks.permission_names.items.len);    try std.testing.expectEqual(@as(usize, 0), hooks.executed_names.items.len);} test "finalization sink failure makes guard fatal and suppresses finish payload" {    const alloc = std.testing.allocator;    const completions = [_]FakeCompletion{.{ .content = "Final" }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    hooks.finalization_error = error.TurnFinalizationDeliveryFailed;    defer hooks.deinit();    var fixture = PromptFixture{};     try std.testing.expectError(        error.TurnFinalizationDeliveryFailed,        runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job()),    );     try std.testing.expectEqual(@as(usize, 1), hooks.finalization_count);    try std.testing.expectEqual(@as(usize, 0), hooks.finish_event_count);} test "processQueuedPrompt terminal outcomes finalize exactly once before optional finish" {    const alloc = std.testing.allocator;    const cases = [_]struct {        completion: FakeCompletion,        outcome: types.TurnPresentationOutcome,        finish_count: usize,        propagates_error: bool = false,    }{        .{ .completion = .{ .content = "Final" }, .outcome = .completed, .finish_count = 1 },        .{ .completion = .{ .cancel_before_output = true }, .outcome = .interrupted, .finish_count = 1 },        .{ .completion = .{ .status = .bad_request, .err_body = "bad" }, .outcome = .failed, .finish_count = 0 },        .{ .completion = .{ .stream_error = error.TestProviderFailure }, .outcome = .failed, .finish_count = 0, .propagates_error = true },    };    for (cases) |case| {        const completions = [_]FakeCompletion{case.completion};        var gateway = FakeGateway.init(alloc, &completions);        defer gateway.deinit();        var hooks = FakeAgentRuntimeDeps.init(alloc);        defer hooks.deinit();        var fixture = PromptFixture{};        if (case.propagates_error) {            try std.testing.expectError(                error.TestProviderFailure,                runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job()),            );        } else {            try runFakePrompt(&gateway, &hooks, fixture.config(), fixture.job());        }        try std.testing.expectEqual(@as(usize, 1), hooks.finalization_count);        try std.testing.expectEqual(case.outcome, hooks.finalized_outcome.?);        try std.testing.expectEqual(case.finish_count, hooks.finish_event_count);        if (case.finish_count > 0) {            try std.testing.expect(                logIndex(&hooks, "event:turn_finished").? <                    logIndex(&hooks, "event:finish_prompt").?,            );        }    }} test "partial stream failure persists failed terminal reason" {    const alloc = std.testing.allocator;    const completions = [_]FakeCompletion{.{        .chunks = &.{"partial response"},        .stream_error_after_chunks = error.TestProviderFailure,    }};    var gateway = FakeGateway.init(alloc, &completions);    defer gateway.deinit();    var hooks = FakeAgentRuntimeDeps.init(alloc);    defer hooks.deinit();    var fixture = PromptFixture{};    var config = fixture.config();    config.gateway_retry_count = 0;    config.max_provider_attempts = 1;     try std.testing.expectError(        error.TestProviderFailure,        runFakePrompt(&gateway, &hooks, config, fixture.job()),    );     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.expectEqual(        types.InterruptedTerminalReason.failed,        hooks.history_turns.items[0].interrupted.terminal_reason,    );

Lines 1–500 of 1786.