| ... | ... | @@ -1153,6 +1153,73 @@ describe("ClaudeAdapterLive", () => { |
| 1153 | 1153 | ); |
| 1154 | 1154 | }); |
| 1155 | 1155 | |
| 1156 | it.effect("stopSession does not throw into the SDK prompt consumer", () => { |
| 1157 | // The SDK consumes user messages via `for await (... of prompt)`. |
| 1158 | // Stopping a session must end that loop cleanly — not throw an error. |
| 1159 | // |
| 1160 | // FakeClaudeQuery.close() masks this by resolving pending iterators |
| 1161 | // before the shutdown propagates. Override it to match real SDK behavior |
| 1162 | // where close() does not resolve the prompt consumer. |
| 1163 | const query = new FakeClaudeQuery(); |
| 1164 | (query as { close: () => void }).close = () => { |
| 1165 | query.closeCalls += 1; |
| 1166 | }; |
| 1167 | |
| 1168 | let promptConsumerError: unknown = undefined; |
| 1169 | |
| 1170 | const layer = makeClaudeAdapterLive({ |
| 1171 | createQuery: (input) => { |
| 1172 | // Simulate the SDK consuming the prompt iterable |
| 1173 | (async () => { |
| 1174 | try { |
| 1175 | for await (const _message of input.prompt) { |
| 1176 | /* SDK processes user messages */ |
| 1177 | } |
| 1178 | } catch (error) { |
| 1179 | promptConsumerError = error; |
| 1180 | } |
| 1181 | })(); |
| 1182 | return query; |
| 1183 | }, |
| 1184 | }).pipe( |
| 1185 | Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")), |
| 1186 | Layer.provideMerge(NodeServices.layer), |
| 1187 | ); |
| 1188 | |
| 1189 | return Effect.gen(function* () { |
| 1190 | const adapter = yield* ClaudeAdapter; |
| 1191 | |
| 1192 | const runtimeEventsFiber = Effect.runFork( |
| 1193 | Stream.runForEach(adapter.streamEvents, () => Effect.void), |
| 1194 | ); |
| 1195 | |
| 1196 | yield* adapter.startSession({ |
| 1197 | threadId: THREAD_ID, |
| 1198 | provider: "claudeAgent", |
| 1199 | runtimeMode: "full-access", |
| 1200 | }); |
| 1201 | |
| 1202 | yield* adapter.stopSession(THREAD_ID); |
| 1203 | |
| 1204 | yield* Effect.yieldNow; |
| 1205 | yield* Effect.yieldNow; |
| 1206 | yield* Effect.yieldNow; |
| 1207 | yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 50))); |
| 1208 | |
| 1209 | runtimeEventsFiber.interruptUnsafe(); |
| 1210 | |
| 1211 | assert.equal( |
| 1212 | promptConsumerError, |
| 1213 | undefined, |
| 1214 | `Prompt consumer should not receive a thrown error on session stop, ` + |
| 1215 | `but got: "${promptConsumerError instanceof Error ? promptConsumerError.message : String(promptConsumerError)}"`, |
| 1216 | ); |
| 1217 | }).pipe( |
| 1218 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 1219 | Effect.provide(layer), |
| 1220 | ); |
| 1221 | }); |
| 1222 | |
| 1156 | 1223 | it.effect("forwards Claude task progress summaries for subagent updates", () => { |
| 1157 | 1224 | const harness = makeHarness(); |
| 1158 | 1225 | return Effect.gen(function* () { |