1import { assert, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { ProviderInstanceId } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6
7import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
8import * as OpenCode2AdapterV2Testkit from "../orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts";
9import * as OpenCode2Server from "@t3tools/provider-opencode/server/v2/OpenCode2Server";
10import * as OpenCode2TextGeneration from "@t3tools/provider-opencode/server/v2/textGeneration";
11import { OPENCODE2_TITLE_GENERATION } from "./OpenCode2TextGeneration.fixture.ts";
12
13const layer = TestProviderHost.layer().pipe(Layer.provideMerge(NodeServices.layer));
14
15it.layer(layer)("OpenCode2TextGeneration", (it) => {
16 it.effect("generates a title in a temporary session on the free tier and removes it", () =>
17 Effect.gen(function* () {
18 const server = yield* OpenCode2AdapterV2Testkit.replayServer({
19 provider: "opencode",
20 protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
21 version: "2.0.18",
22 scenario: "opencode2_title_generation",
23 entries: [...OPENCODE2_TITLE_GENERATION, { type: "runtime_exit", status: "success" }],
24 });
25 const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
26 Effect.provideService(OpenCode2Server.OpenCode2Server, server),
27 );
28 const title = yield* textGeneration.generateThreadTitle({
29 cwd: process.cwd(),
30 message: "fix the login redirect loop after oauth",
31 modelSelection: {
32 instanceId: ProviderInstanceId.make("opencode"),
33 model: "opencode/big-pickle",
34 },
35 });
36 assert.equal(title.title, "Fix OAuth Login Redirect Loop");
37 }).pipe(Effect.scoped),
38 );
39
40 it.effect("fails at once when the event stream drops before the reply", () =>
41 Effect.gen(function* () {
42 const [subscribe, connected, create, created] = OPENCODE2_TITLE_GENERATION;
43 const sessionId = "ses_f0ec85cf6ffebfalpvV2E9nocu";
44 const server = yield* OpenCode2AdapterV2Testkit.replayServer({
45 provider: "opencode",
46 protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
47 version: "2.0.18",
48 scenario: "opencode2_title_generation_stream_lost",
49 entries: [
50 subscribe!,
51 connected!,
52 create!,
53 created!,
54 {
55 type: "expect_outbound",
56 frame: { type: "session.prompt", input: { sessionID: sessionId, text: "<any>" } },
57 },
58 {
59 type: "emit_inbound",
60 frame: {
61 type: "sdk.response",
62 operation: "session.prompt",
63 data: {
64 data: {
65 id: "msg_1",
66 sessionID: sessionId,
67 time: { created: 1 },
68 type: "user",
69 payload: { text: "t" },
70 delivery: "steer",
71 },
72 },
73 },
74 },
75 // The server goes away before the reply; the temporary session is still removed.
76 { type: "runtime_exit", status: "success" },
77 {
78 type: "expect_outbound",
79 frame: { type: "session.remove", input: { sessionID: sessionId } },
80 },
81 {
82 type: "emit_inbound",
83 frame: { type: "sdk.response", operation: "session.remove", data: null },
84 },
85 ],
86 });
87 const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
88 Effect.provideService(OpenCode2Server.OpenCode2Server, server),
89 );
90 const failure = yield* textGeneration
91 .generateThreadTitle({
92 cwd: process.cwd(),
93 message: "fix the login redirect loop after oauth",
94 modelSelection: {
95 instanceId: ProviderInstanceId.make("opencode"),
96 model: "opencode/big-pickle",
97 },
98 })
99 .pipe(Effect.flip, Effect.timeout("10 seconds"));
100 assert.equal(failure._tag, "TextGenerationError");
101 }).pipe(Effect.scoped),
102 );
103
104 it.effect("keeps a provider's own error message out of the caller-visible detail", () =>
105 Effect.gen(function* () {
106 const [subscribe, connected, create, created] = OPENCODE2_TITLE_GENERATION;
107 const sessionId = "ses_f0ec85cf6ffebfalpvV2E9nocu";
108 // The spike's `text_generation` recording: a deny-all session is refused this way.
109 const refusal = {
110 type: "provider.auth",
111 message:
112 "Error from provider (Console): OpenCode's free tier can only be used from within OpenCode",
113 status: 403,
114 };
115 const server = yield* OpenCode2AdapterV2Testkit.replayServer({
116 provider: "opencode",
117 protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
118 version: "2.0.18",
119 scenario: "opencode2_title_generation_refused",
120 entries: [
121 subscribe!,
122 connected!,
123 create!,
124 created!,
125 {
126 type: "expect_outbound",
127 frame: { type: "session.prompt", input: { sessionID: sessionId, text: "<any>" } },
128 },
129 {
130 type: "emit_inbound",
131 frame: {
132 type: "sdk.response",
133 operation: "session.prompt",
134 data: {
135 data: {
136 id: "msg_1",
137 sessionID: sessionId,
138 time: { created: 1 },
139 type: "user",
140 payload: { text: "t" },
141 delivery: "steer",
142 },
143 },
144 },
145 },
146 {
147 type: "emit_inbound",
148 frame: {
149 type: "sdk.event",
150 event: {
151 id: "evt_0eb7f9ae0001VCNDHVLnqTfoPG",
152 created: 1790657403616,
153 type: "session.execution.failed",
154 data: { sessionID: sessionId, error: refusal },
155 durable: { aggregateID: sessionId, seq: 7, version: 1 },
156 },
157 },
158 },
159 {
160 type: "expect_outbound",
161 frame: { type: "session.remove", input: { sessionID: sessionId } },
162 },
163 {
164 type: "emit_inbound",
165 frame: { type: "sdk.response", operation: "session.remove", data: null },
166 },
167 { type: "runtime_exit", status: "success" },
168 ],
169 });
170 const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
171 Effect.provideService(OpenCode2Server.OpenCode2Server, server),
172 );
173 const failure = yield* textGeneration
174 .generateThreadTitle({
175 cwd: process.cwd(),
176 message: "fix the login redirect loop after oauth",
177 modelSelection: {
178 instanceId: ProviderInstanceId.make("opencode"),
179 model: "opencode/big-pickle",
180 },
181 })
182 .pipe(Effect.flip);
183 assert.equal(failure.detail, "OpenCode could not generate the text.");
184 assert.deepEqual(failure.cause, refusal);
185 }).pipe(Effect.scoped),
186 );
187
188 it.effect("ends at once on an execution end this build cannot decode", () =>
189 Effect.gen(function* () {
190 const [subscribe, connected, create, created] = OPENCODE2_TITLE_GENERATION;
191 const sessionId = "ses_f0ec85cf6ffebfalpvV2E9nocu";
192 const server = yield* OpenCode2AdapterV2Testkit.replayServer({
193 provider: "opencode",
194 protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
195 version: "2.0.18",
196 scenario: "opencode2_title_generation_unreadable_end",
197 entries: [
198 subscribe!,
199 connected!,
200 create!,
201 created!,
202 {
203 type: "expect_outbound",
204 frame: { type: "session.prompt", input: { sessionID: sessionId, text: "<any>" } },
205 },
206 {
207 type: "emit_inbound",
208 frame: {
209 type: "sdk.response",
210 operation: "session.prompt",
211 data: {
212 data: {
213 id: "msg_1",
214 sessionID: sessionId,
215 time: { created: 1 },
216 type: "user",
217 payload: { text: "t" },
218 delivery: "steer",
219 },
220 },
221 },
222 },
223 // A reason added after 2.0.18: the full schema rejects the frame.
224 {
225 type: "emit_inbound",
226 frame: {
227 type: "sdk.event",
228 event: {
229 id: "evt_0eb7f9ae0001VCNDHVLnqTfoPG",
230 created: 1790657403616,
231 type: "session.execution.interrupted",
232 data: { sessionID: sessionId, reason: "budget" },
233 durable: { aggregateID: sessionId, seq: 7, version: 1 },
234 },
235 },
236 },
237 {
238 type: "expect_outbound",
239 frame: { type: "session.remove", input: { sessionID: sessionId } },
240 },
241 {
242 type: "emit_inbound",
243 frame: { type: "sdk.response", operation: "session.remove", data: null },
244 },
245 { type: "runtime_exit", status: "success" },
246 ],
247 });
248 const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
249 Effect.provideService(OpenCode2Server.OpenCode2Server, server),
250 );
251 const failure = yield* textGeneration
252 .generateThreadTitle({
253 cwd: process.cwd(),
254 message: "fix the login redirect loop after oauth",
255 modelSelection: {
256 instanceId: ProviderInstanceId.make("opencode"),
257 model: "opencode/big-pickle",
258 },
259 })
260 .pipe(Effect.flip, Effect.timeout("10 seconds"));
261 assert.equal(failure._tag, "TextGenerationError");
262 }).pipe(Effect.scoped),
263 );
264});