| ... | ... | @@ -58,6 +58,7 @@ import type * as EffectAcpSchema from "effect-acp/compat"; |
| 58 | 58 | import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost"; |
| 59 | 59 | import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; |
| 60 | 60 | import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; |
| 61 | import { ACP_SESSION_MODE_OPTION_ID } from "@t3tools/provider-acp/server/sessionConfig"; |
| 61 | 62 | import { |
| 62 | 63 | extractXAiAcpSubagentEndNotice, |
| 63 | 64 | extractXAiAcpSubagentUpdate, |
| ... | ... | @@ -880,6 +881,95 @@ describe("AcpAdapterV2", () => { |
| 880 | 881 | }).pipe(Effect.provide(layerTest), Effect.scoped), |
| 881 | 882 | ); |
| 882 | 883 | |
| 884 | it.live("reapplies a selection after a switch away from it failed partway", () => |
| 885 | Effect.gen(function* () { |
| 886 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 887 | const path = yield* Path.Path; |
| 888 | const mockAgentPath = yield* path.fromFileUrl( |
| 889 | new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), |
| 890 | ); |
| 891 | const protocolEvents = yield* Queue.unbounded<EffectAcpProtocol.AcpProtocolLogEvent>(); |
| 892 | const instanceId = ProviderInstanceId.make("acp-test-partial-selection-reapply"); |
| 893 | const threadId = ThreadId.make("thread-acp-partial-selection-reapply"); |
| 894 | const adapter = yield* makeAcpAdapterV2({ |
| 895 | instanceId, |
| 896 | flavor: { |
| 897 | driver: ACP_TEST_DRIVER, |
| 898 | capabilities: AcpProviderCapabilitiesV2, |
| 899 | makeRuntime: makeMockRuntime({ |
| 900 | childProcessSpawner, |
| 901 | mockAgentPath, |
| 902 | protocolEvents, |
| 903 | // The model switch lands, then the agent rejects the mode step. |
| 904 | environment: { T3_ACP_FAIL_SET_MODE_CONFIG_OPTION: "1" }, |
| 905 | }), |
| 906 | }, |
| 907 | selfInvocation: yield* resolveSelfInvocation(), |
| 908 | }); |
| 909 | const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ |
| 910 | runtimeMode: "full-access", |
| 911 | interactionMode: "default", |
| 912 | cwd: process.cwd(), |
| 913 | }); |
| 914 | const selectionA = { instanceId, model: "composer-2" } as const; |
| 915 | const selectionB = { |
| 916 | instanceId, |
| 917 | model: "composer-2[fast=true]", |
| 918 | options: [{ id: ACP_SESSION_MODE_OPTION_ID, value: "code" }], |
| 919 | } as const; |
| 920 | const runtime = yield* adapter.openSession({ |
| 921 | threadId, |
| 922 | providerSessionId: ProviderSessionId.make("provider-session-acp-partial-selection"), |
| 923 | modelSelection: selectionA, |
| 924 | runtimePolicy, |
| 925 | }); |
| 926 | const providerThread = yield* runtime.ensureThread({ |
| 927 | threadId, |
| 928 | modelSelection: selectionA, |
| 929 | runtimePolicy, |
| 930 | }); |
| 931 | const now = yield* DateTime.now; |
| 932 | const turnInput = (ordinal: number, modelSelection: ModelSelection) => |
| 933 | makeTurnInput({ |
| 934 | threadId, |
| 935 | providerThread, |
| 936 | instanceId, |
| 937 | runtimePolicy, |
| 938 | now, |
| 939 | ordinal, |
| 940 | modelSelection, |
| 941 | }); |
| 942 | |
| 943 | const switchToB = yield* Effect.exit(runtime.startTurn(turnInput(1, selectionB))); |
| 944 | assert.isTrue(Exit.isFailure(switchToB), "the agent rejected B's mode step"); |
| 945 | yield* Queue.clear(protocolEvents); |
| 946 | |
| 947 | yield* runtime.startTurn(turnInput(2, selectionA)); |
| 948 | yield* runtime.events.pipe( |
| 949 | Stream.takeUntil((event) => event.type === "turn.terminal"), |
| 950 | Stream.runDrain, |
| 951 | ); |
| 952 | const modelSetsBeforePrompt: Array<unknown> = []; |
| 953 | while (true) { |
| 954 | const event = yield* Queue.take(protocolEvents); |
| 955 | if (event.direction !== "outgoing") continue; |
| 956 | const method = rawProtocolMethod(event); |
| 957 | if (method === "session/prompt") break; |
| 958 | if ( |
| 959 | method === "session/set_config_option" && |
| 960 | rawProtocolRequestParam(event, "configId") === "model" |
| 961 | ) { |
| 962 | modelSetsBeforePrompt.push(rawProtocolRequestParam(event, "value")); |
| 963 | } |
| 964 | } |
| 965 | assert.deepEqual( |
| 966 | modelSetsBeforePrompt, |
| 967 | ["composer-2"], |
| 968 | "the native model was left on B, so the turn on A must set it back first", |
| 969 | ); |
| 970 | }).pipe(Effect.provide(layerTest), Effect.scoped), |
| 971 | ); |
| 972 | |
| 883 | 973 | it.effect("starts a new replay message after ACP v2 plan boundaries", () => |
| 884 | 974 | Effect.gen(function* () { |
| 885 | 975 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |