Skip to content
Source
Sign inSign up

OpenCode2TextGeneration.test.ts

224 lines8.9 KB

1import { assert, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as NodeURL from "node:url";
4import { ChatAttachmentId, ChatImageAttachment, ProviderInstanceId } from "@t3tools/contracts";
5import * as Deferred from "effect/Deferred";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Fiber from "effect/Fiber";
9import * as Layer from "effect/Layer";
10import { TestClock } from "effect/testing";
11​
12import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
13import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
14import * as OpenCode2AdapterV2Testkit from "../orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts";
15import * as OpenCode2Server from "@t3tools/provider-opencode/server/v2/OpenCode2Server";
16import * as OpenCode2TextGeneration from "@t3tools/provider-opencode/server/v2/textGeneration";
17import { OPENCODE2_TITLE_GENERATION } from "./OpenCode2TextGeneration.fixture.ts";
18​
19const layer = TestProviderHost.layer().pipe(Layer.provideMerge(NodeServices.layer));
20​
21it.layer(layer)("OpenCode2TextGeneration", (it) => {
22 it.effect.each([false, true] as const)(
23 "generates a title in a temporary session on the free tier and removes it (external: %s)",
24 (external) =>
25 Effect.gen(function* () {
26 const host = yield* ProviderHost.ProviderHost;
27 const fileSystem = yield* FileSystem.FileSystem;
28 const image = (id: string, name: string, mimeType: string) =>
29 ChatImageAttachment.make({
30 type: "image",
31 id: ChatAttachmentId.make(id),
32 name,
33 mimeType,
34 sizeBytes: 4,
35 });
36 const screenshot = image("oc2-title-screenshot", "screen.png", "image/png");
37 const missing = image("oc2-title-missing", "missing.png", "image/png");
38 const unsupported = image("oc2-title-diagram", "diagram.svg", "image/svg+xml");
39 const screenshotPath = host.resolveAttachmentPath(screenshot)!;
40 yield* fileSystem.writeFileString(screenshotPath, "data");
41 yield* fileSystem.writeFileString(host.resolveAttachmentPath(unsupported)!, "<svg/>");
42 const prompts: Array<unknown> = [];
43 const server = yield* OpenCode2AdapterV2Testkit.replayServer(
44 {
45 provider: "opencode",
46 protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
47 version: "2.0.18",
48 scenario: "opencode2_title_generation",
49 entries: [...OPENCODE2_TITLE_GENERATION, { type: "runtime_exit", status: "success" }],
50 },
51 { external, onPrompt: (input) => prompts.push(input) },
52 );
53 const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
54 Effect.provideService(OpenCode2Server.OpenCode2Server, server),
55 );
56 const title = yield* textGeneration.generateThreadTitle({
57 cwd: process.cwd(),
58 message: "fix the login redirect loop after oauth",
59 attachments: [screenshot, missing, unsupported],
60 modelSelection: {
61 instanceId: ProviderInstanceId.make("opencode"),
62 model: "opencode/big-pickle",
63 },
64 });
65 assert.equal(title.title, "Fix OAuth Login Redirect Loop");
66 if (external) {
67 assert.notProperty(prompts[0], "files");
68 } else {
69 assert.deepInclude(prompts[0], {
70 files: [{ uri: NodeURL.pathToFileURL(screenshotPath).href, name: "screen.png" }],
71 });
72 }
73 }).pipe(Effect.scoped),
74 );
75​
76 it.effect("keeps a provider's own error message out of the caller-visible detail", () =>
77 Effect.gen(function* () {
78 const [subscribe, connected, create, created] = OPENCODE2_TITLE_GENERATION;
79 const sessionId = "ses_f0ec85cf6ffebfalpvV2E9nocu";
80 const refusal = {
81 type: "provider.auth",
82 message:
83 "Error from provider (Console): OpenCode's free tier can only be used from within OpenCode",
84 status: 403,
85 };
86 const server = yield* OpenCode2AdapterV2Testkit.replayServer({
87 provider: "opencode",
88 protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
89 version: "2.0.18",
90 scenario: "opencode2_title_generation_refused",
91 entries: [
92 subscribe!,
93 connected!,
94 create!,
95 created!,
96 {
97 type: "expect_outbound",
98 frame: { type: "session.prompt", input: { sessionID: sessionId, text: "<any>" } },
99 },
100 {
101 type: "emit_inbound",
102 frame: {
103 type: "sdk.response",
104 operation: "session.prompt",
105 data: {
106 data: {
107 id: "msg_1",
108 sessionID: sessionId,
109 time: { created: 1 },
110 type: "user",
111 payload: { text: "t" },
112 delivery: "steer",
113 },
114 },
115 },
116 },
117 {
118 type: "emit_inbound",
119 frame: {
120 type: "sdk.event",
121 event: {
122 id: "evt_0eb7f9ae0001VCNDHVLnqTfoPG",
123 created: 1790657403616,
124 type: "session.execution.failed",
125 data: { sessionID: sessionId, error: refusal },
126 durable: { aggregateID: sessionId, seq: 7, version: 1 },
127 },
128 },
129 },
130 {
131 type: "expect_outbound",
132 frame: { type: "session.remove", input: { sessionID: sessionId } },
133 },
134 {
135 type: "emit_inbound",
136 frame: { type: "sdk.response", operation: "session.remove", data: null },
137 },
138 { type: "runtime_exit", status: "success" },
139 ],
140 });
141 const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
142 Effect.provideService(OpenCode2Server.OpenCode2Server, server),
143 );
144 const failure = yield* textGeneration
145 .generateThreadTitle({
146 cwd: process.cwd(),
147 message: "fix the login redirect loop after oauth",
148 modelSelection: {
149 instanceId: ProviderInstanceId.make("opencode"),
150 model: "opencode/big-pickle",
151 },
152 })
153 .pipe(Effect.flip);
154 assert.equal(failure.detail, "OpenCode could not generate the text.");
155 assert.deepEqual(failure.cause, refusal);
156 }).pipe(Effect.scoped),
157 );
158​
159 it.effect("times out when the prompt request and its interrupt never answer", () =>
160 Effect.gen(function* () {
161 const [subscribe, connected, create, created] = OPENCODE2_TITLE_GENERATION;
162 const sessionId = "ses_f0ec85cf6ffebfalpvV2E9nocu";
163 const prompted = yield* Deferred.make<void>();
164 const server = yield* OpenCode2AdapterV2Testkit.replayServer(
165 {
166 provider: "opencode",
167 protocol: OpenCode2AdapterV2Testkit.OPENCODE2_HTTP_PROTOCOL,
168 version: "2.0.18",
169 scenario: "opencode2_title_generation_stalled",
170 entries: [
171 subscribe!,
172 connected!,
173 create!,
174 created!,
175 {
176 type: "expect_outbound",
177 frame: { type: "session.prompt", input: { sessionID: sessionId, text: "<any>" } },
178 },
179 {
180 type: "emit_inbound",
181 frame: { type: "sdk.response", operation: "session.prompt", data: "<hang>" },
182 },
183 {
184 type: "expect_outbound",
185 frame: { type: "session.interrupt", input: { sessionID: sessionId } },
186 },
187 {
188 type: "emit_inbound",
189 frame: { type: "sdk.response", operation: "session.interrupt", data: "<hang>" },
190 },
191 {
192 type: "expect_outbound",
193 frame: { type: "session.remove", input: { sessionID: sessionId } },
194 },
195 {
196 type: "emit_inbound",
197 frame: { type: "sdk.response", operation: "session.remove", data: null },
198 },
199 { type: "runtime_exit", status: "success" },
200 ],
201 },
202 { onPrompt: () => Deferred.doneUnsafe(prompted, Effect.void) },
203 );
204 const textGeneration = yield* OpenCode2TextGeneration.make().pipe(
205 Effect.provideService(OpenCode2Server.OpenCode2Server, server),
206 );
207 const generation = yield* textGeneration
208 .generateThreadTitle({
209 cwd: process.cwd(),
210 message: "fix the login redirect loop after oauth",
211 modelSelection: {
212 instanceId: ProviderInstanceId.make("opencode"),
213 model: "opencode/big-pickle",
214 },
215 })
216 .pipe(Effect.flip, Effect.forkScoped);
217 yield* Deferred.await(prompted);
218 yield* TestClock.adjust("3 minutes");
219 yield* TestClock.adjust("5 seconds");
220 const failure = yield* Fiber.join(generation);
221 assert.equal(failure.detail, "OpenCode did not finish generating in time.");
222 }).pipe(Effect.scoped),
223 );
224});