1import { assert, it } from "@effect/vitest";
2import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";
3
4import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
5import { decideProviderSessionTransition } from "./ProviderSessionTransitionPolicy.ts";
6
7const driver = ProviderDriverKind.make("codex");
8const instanceId = ProviderInstanceId.make("codex");
9const base = {
10 driver,
11 continuationIdentity: { driverKind: driver, continuationKey: "codex:account:one" },
12 modelSelection: { instanceId, model: "gpt-5.1-codex" },
13 runtimeMode: "full-access" as const,
14 interactionMode: "default" as const,
15 workspace: "/repo",
16 capabilities: CodexProviderCapabilitiesV2,
17};
18
19it("reuses compatible sessions and treats interaction mode as turn-scoped", () => {
20 assert.deepEqual(
21 decideProviderSessionTransition({
22 current: base,
23 target: { ...base, interactionMode: "plan", available: true },
24 }),
25 { type: "reuse" },
26 );
27});
28
29it("uses the adapter's selection transition classification", () => {
30 assert.deepEqual(
31 decideProviderSessionTransition({
32 current: base,
33 target: {
34 ...base,
35 modelSelection: { ...base.modelSelection, model: "gpt-5.2-codex" },
36 available: true,
37 },
38 selectionTransition: { type: "apply_on_next_turn" },
39 }),
40 { type: "switch_model_in_session" },
41 );
42 assert.deepEqual(
43 decideProviderSessionTransition({
44 current: {
45 ...base,
46 capabilities: {
47 ...base.capabilities,
48 sessions: { ...base.capabilities.sessions, supportsModelSwitchInSession: false },
49 },
50 },
51 target: {
52 ...base,
53 capabilities: {
54 ...base.capabilities,
55 sessions: { ...base.capabilities.sessions, supportsModelSwitchInSession: false },
56 },
57 modelSelection: { ...base.modelSelection, model: "gpt-5.2-codex" },
58 available: true,
59 },
60 selectionTransition: { type: "restart_session" },
61 }),
62 { type: "restart_and_resume" },
63 );
64});
65
66it("treats provider option changes as selection changes", () => {
67 assert.deepEqual(
68 decideProviderSessionTransition({
69 current: {
70 ...base,
71 modelSelection: {
72 ...base.modelSelection,
73 options: [{ id: "reasoningEffort", value: "medium" }],
74 },
75 },
76 target: {
77 ...base,
78 modelSelection: {
79 ...base.modelSelection,
80 options: [{ id: "reasoningEffort", value: "high" }],
81 },
82 available: true,
83 },
84 selectionTransition: { type: "apply_on_next_turn" },
85 }),
86 { type: "switch_model_in_session" },
87 );
88});
89
90it("restarts compatible instances for workspace or runtime changes", () => {
91 assert.deepEqual(
92 decideProviderSessionTransition({
93 current: base,
94 target: { ...base, workspace: "/other", available: true },
95 }),
96 { type: "restart_and_resume" },
97 );
98});
99
100it("preserves a rejected selection when the workspace also changes", () => {
101 assert.deepEqual(
102 decideProviderSessionTransition({
103 current: base,
104 target: {
105 ...base,
106 modelSelection: { ...base.modelSelection, model: "gpt-5.2-codex" },
107 workspace: "/other",
108 available: true,
109 },
110 selectionTransition: {
111 type: "reject",
112 reason: "The active session cannot apply that model.",
113 },
114 }),
115 { type: "reject", reason: "The active session cannot apply that model." },
116 );
117});
118
119it("preserves handoff and missing-classification outcomes across workspace changes", () => {
120 const target = {
121 ...base,
122 modelSelection: { ...base.modelSelection, model: "gpt-5.2-codex" },
123 workspace: "/other",
124 available: true,
125 };
126 assert.deepEqual(
127 decideProviderSessionTransition({
128 current: base,
129 target,
130 selectionTransition: { type: "create_with_handoff" },
131 }),
132 { type: "create_with_handoff" },
133 );
134 assert.deepEqual(decideProviderSessionTransition({ current: base, target }), {
135 type: "reject",
136 reason: "The provider adapter did not classify the selection change.",
137 });
138});
139
140it("uses portable handoff for incompatible continuation identities", () => {
141 assert.deepEqual(
142 decideProviderSessionTransition({
143 current: base,
144 target: {
145 ...base,
146 modelSelection: {
147 ...base.modelSelection,
148 instanceId: ProviderInstanceId.make("codex_other"),
149 },
150 continuationIdentity: { driverKind: driver, continuationKey: "codex:account:other" },
151 available: true,
152 },
153 }),
154 { type: "create_with_handoff" },
155 );
156});
157
158it("uses portable handoff for cross-driver transitions", () => {
159 const claudeDriver = ProviderDriverKind.make("claude");
160 assert.deepEqual(
161 decideProviderSessionTransition({
162 current: base,
163 target: {
164 ...base,
165 driver: claudeDriver,
166 continuationIdentity: { driverKind: claudeDriver, continuationKey: "claude:account:one" },
167 modelSelection: {
168 instanceId: ProviderInstanceId.make("claude"),
169 model: "claude-opus-4-1",
170 },
171 available: true,
172 },
173 }),
174 { type: "create_with_handoff" },
175 );
176});