| 1 | import { assert, it, vi } from "@effect/vitest"; |
| 2 | import { |
| 3 | NodeId, |
| 4 | ProviderSessionId, |
| 5 | RuntimeRequestId, |
| 6 | ThreadId, |
| 7 | type OrchestrationV2RuntimeRequest, |
| 8 | type OrchestrationV2ThreadProjection, |
| 9 | } from "@t3tools/contracts"; |
| 10 | import * as DateTime from "effect/DateTime"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as Layer from "effect/Layer"; |
| 13 | import * as Option from "effect/Option"; |
| 14 | |
| 15 | import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; |
| 16 | import * as ProjectionStore from "./ProjectionStore.ts"; |
| 17 | import * as ProviderSessionManager from "./ProviderSessionManager.ts"; |
| 18 | import * as RuntimeRequestService from "./RuntimeRequestService.ts"; |
| 19 | |
| 20 | function projectionWithRuntimeRequest( |
| 21 | runtimeRequest?: OrchestrationV2RuntimeRequest, |
| 22 | ): OrchestrationV2ThreadProjection { |
| 23 | return { |
| 24 | runtimeRequests: runtimeRequest === undefined ? [] : [runtimeRequest], |
| 25 | } as unknown as OrchestrationV2ThreadProjection; |
| 26 | } |
| 27 | |
| 28 | function resolvedRuntimeRequest( |
| 29 | requestId: RuntimeRequestId, |
| 30 | providerSessionId: ProviderSessionId, |
| 31 | ): OrchestrationV2RuntimeRequest { |
| 32 | return { |
| 33 | id: requestId, |
| 34 | nodeId: NodeId.make(`node-${requestId}`), |
| 35 | providerTurnId: null, |
| 36 | nativeRequestRef: null, |
| 37 | kind: "command", |
| 38 | status: "resolved", |
| 39 | responseCapability: { |
| 40 | type: "live", |
| 41 | providerSessionId, |
| 42 | }, |
| 43 | createdAt: DateTime.makeUnsafe("2026-07-29T00:00:00.000Z"), |
| 44 | resolvedAt: DateTime.makeUnsafe("2026-07-29T00:00:01.000Z"), |
| 45 | }; |
| 46 | } |
| 47 | |
| 48 | function layerRuntimeRequestTest( |
| 49 | projection: OrchestrationV2ThreadProjection, |
| 50 | getSession: ProviderSessionManager.ProviderSessionManagerV2Shape["get"], |
| 51 | ) { |
| 52 | return RuntimeRequestService.layer.pipe( |
| 53 | Layer.provide( |
| 54 | Layer.mergeAll( |
| 55 | Layer.mock(ProjectionStore.ProjectionStoreV2)({ |
| 56 | getRuntimeRequest: (_threadId, requestId) => |
| 57 | Effect.succeed(projection.runtimeRequests.find((request) => request.id === requestId)), |
| 58 | }), |
| 59 | Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({ |
| 60 | get: getSession, |
| 61 | }), |
| 62 | ), |
| 63 | ), |
| 64 | ); |
| 65 | } |
| 66 | |
| 67 | it.effect("forwards orchestrator-resolved runtime requests to the live adapter", () => { |
| 68 | const threadId = ThreadId.make("thread-runtime-request-resolved"); |
| 69 | const providerSessionId = ProviderSessionId.make("provider-session-runtime-request-resolved"); |
| 70 | const requestId = RuntimeRequestId.make("request-resolved"); |
| 71 | const respondToRuntimeRequest = vi.fn( |
| 72 | (_input: ProviderAdapter.ProviderAdapterV2RuntimeRequestResponseInput) => Effect.void, |
| 73 | ); |
| 74 | const getSession = vi.fn(() => |
| 75 | Effect.succeed( |
| 76 | Option.some({ |
| 77 | respondToRuntimeRequest, |
| 78 | } as never), |
| 79 | ), |
| 80 | ); |
| 81 | const projection = { |
| 82 | runtimeRequests: [ |
| 83 | { |
| 84 | id: requestId, |
| 85 | nodeId: NodeId.make("node-runtime-request-resolved"), |
| 86 | providerTurnId: null, |
| 87 | nativeRequestRef: null, |
| 88 | kind: "command", |
| 89 | status: "resolved", |
| 90 | responseCapability: { |
| 91 | type: "live", |
| 92 | providerSessionId, |
| 93 | }, |
| 94 | createdAt: DateTime.makeUnsafe("2026-07-29T00:00:00.000Z"), |
| 95 | resolvedAt: DateTime.makeUnsafe("2026-07-29T00:00:01.000Z"), |
| 96 | }, |
| 97 | ], |
| 98 | } as unknown as OrchestrationV2ThreadProjection; |
| 99 | const layerTest = RuntimeRequestService.layer.pipe( |
| 100 | Layer.provide( |
| 101 | Layer.mergeAll( |
| 102 | Layer.mock(ProjectionStore.ProjectionStoreV2)({ |
| 103 | getRuntimeRequest: (_threadId, requestId) => |
| 104 | Effect.succeed(projection.runtimeRequests.find((request) => request.id === requestId)), |
| 105 | }), |
| 106 | Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({ |
| 107 | get: getSession, |
| 108 | }), |
| 109 | ), |
| 110 | ), |
| 111 | ); |
| 112 | |
| 113 | return Effect.gen(function* () { |
| 114 | const service = yield* RuntimeRequestService.RuntimeRequestServiceV2; |
| 115 | yield* service.respond({ |
| 116 | threadId, |
| 117 | providerSessionId, |
| 118 | requestId, |
| 119 | decision: "accept", |
| 120 | }); |
| 121 | |
| 122 | assert.equal(getSession.mock.calls.length, 1); |
| 123 | assert.equal(respondToRuntimeRequest.mock.calls.length, 1); |
| 124 | assert.deepEqual(respondToRuntimeRequest.mock.calls[0]?.[0], { |
| 125 | requestId, |
| 126 | decision: "accept", |
| 127 | }); |
| 128 | }).pipe(Effect.provide(layerTest)); |
| 129 | }); |
| 130 | |
| 131 | it.effect("rejects expired runtime requests before invoking the live adapter", () => { |
| 132 | const threadId = ThreadId.make("thread-runtime-request-expired"); |
| 133 | const providerSessionId = ProviderSessionId.make("provider-session-runtime-request-expired"); |
| 134 | const requestId = RuntimeRequestId.make("request-expired"); |
| 135 | const respondToRuntimeRequest = vi.fn( |
| 136 | (_input: ProviderAdapter.ProviderAdapterV2RuntimeRequestResponseInput) => Effect.void, |
| 137 | ); |
| 138 | const getSession = vi.fn(() => |
| 139 | Effect.succeed( |
| 140 | Option.some({ |
| 141 | respondToRuntimeRequest, |
| 142 | } as never), |
| 143 | ), |
| 144 | ); |
| 145 | const projection = { |
| 146 | runtimeRequests: [ |
| 147 | { |
| 148 | id: requestId, |
| 149 | nodeId: NodeId.make("node-runtime-request-expired"), |
| 150 | providerTurnId: null, |
| 151 | nativeRequestRef: null, |
| 152 | kind: "command", |
| 153 | status: "expired", |
| 154 | responseCapability: { |
| 155 | type: "live", |
| 156 | providerSessionId, |
| 157 | }, |
| 158 | createdAt: DateTime.makeUnsafe("2026-07-29T00:00:00.000Z"), |
| 159 | resolvedAt: DateTime.makeUnsafe("2026-07-29T00:00:01.000Z"), |
| 160 | }, |
| 161 | ], |
| 162 | } as unknown as OrchestrationV2ThreadProjection; |
| 163 | const layerTest = RuntimeRequestService.layer.pipe( |
| 164 | Layer.provide( |
| 165 | Layer.mergeAll( |
| 166 | Layer.mock(ProjectionStore.ProjectionStoreV2)({ |
| 167 | getRuntimeRequest: (_threadId, requestId) => |
| 168 | Effect.succeed(projection.runtimeRequests.find((request) => request.id === requestId)), |
| 169 | }), |
| 170 | Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({ |
| 171 | get: getSession, |
| 172 | }), |
| 173 | ), |
| 174 | ), |
| 175 | ); |
| 176 | |
| 177 | return Effect.gen(function* () { |
| 178 | const service = yield* RuntimeRequestService.RuntimeRequestServiceV2; |
| 179 | const error = yield* service |
| 180 | .respond({ |
| 181 | threadId, |
| 182 | providerSessionId, |
| 183 | requestId, |
| 184 | decision: "accept", |
| 185 | }) |
| 186 | .pipe(Effect.flip); |
| 187 | |
| 188 | assert.equal(error.reason, "request-not-ready"); |
| 189 | assert.equal( |
| 190 | error.message, |
| 191 | `Runtime request ${requestId} on thread ${threadId} is not ready for response execution.`, |
| 192 | ); |
| 193 | assert.isUndefined(error.cause); |
| 194 | assert.equal(getSession.mock.calls.length, 0); |
| 195 | assert.equal(respondToRuntimeRequest.mock.calls.length, 0); |
| 196 | }).pipe(Effect.provide(layerTest)); |
| 197 | }); |
| 198 | |
| 199 | it.effect("classifies runtime request response validation failures", () => { |
| 200 | const threadId = ThreadId.make("thread-runtime-request-validation"); |
| 201 | const providerSessionId = ProviderSessionId.make("provider-session-runtime-request-validation"); |
| 202 | const otherProviderSessionId = ProviderSessionId.make( |
| 203 | "provider-session-runtime-request-validation-other", |
| 204 | ); |
| 205 | const requestId = RuntimeRequestId.make("request-validation"); |
| 206 | const unexpectedGetSession = vi.fn(() => Effect.succeed(Option.none())); |
| 207 | const layerMissing = layerRuntimeRequestTest( |
| 208 | projectionWithRuntimeRequest(), |
| 209 | unexpectedGetSession, |
| 210 | ); |
| 211 | const layerNotResumable = layerRuntimeRequestTest( |
| 212 | projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, otherProviderSessionId)), |
| 213 | unexpectedGetSession, |
| 214 | ); |
| 215 | const layerInactive = layerRuntimeRequestTest( |
| 216 | projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, providerSessionId)), |
| 217 | () => Effect.succeed(Option.none()), |
| 218 | ); |
| 219 | const respond = Effect.gen(function* () { |
| 220 | const service = yield* RuntimeRequestService.RuntimeRequestServiceV2; |
| 221 | return yield* service |
| 222 | .respond({ |
| 223 | threadId, |
| 224 | providerSessionId, |
| 225 | requestId, |
| 226 | decision: "accept", |
| 227 | }) |
| 228 | .pipe(Effect.flip); |
| 229 | }); |
| 230 | |
| 231 | return Effect.gen(function* () { |
| 232 | const missing = yield* respond.pipe(Effect.provide(layerMissing)); |
| 233 | assert.equal(missing.reason, "request-missing"); |
| 234 | assert.equal( |
| 235 | missing.message, |
| 236 | `Runtime request ${requestId} no longer exists on thread ${threadId}.`, |
| 237 | ); |
| 238 | assert.isUndefined(missing.cause); |
| 239 | |
| 240 | const notResumable = yield* respond.pipe(Effect.provide(layerNotResumable)); |
| 241 | assert.equal(notResumable.reason, "request-not-resumable"); |
| 242 | assert.equal( |
| 243 | notResumable.message, |
| 244 | `Runtime request ${requestId} on thread ${threadId} is not resumable on provider session ${providerSessionId}.`, |
| 245 | ); |
| 246 | assert.isUndefined(notResumable.cause); |
| 247 | |
| 248 | const inactive = yield* respond.pipe(Effect.provide(layerInactive)); |
| 249 | assert.equal(inactive.reason, "provider-session-not-active"); |
| 250 | assert.equal( |
| 251 | inactive.message, |
| 252 | `Provider session ${providerSessionId} is not active for runtime request ${requestId} on thread ${threadId}.`, |
| 253 | ); |
| 254 | assert.isUndefined(inactive.cause); |
| 255 | assert.equal(unexpectedGetSession.mock.calls.length, 0); |
| 256 | }); |
| 257 | }); |
| 258 | |
| 259 | it.effect("preserves genuine provider session lookup failures as the cause", () => { |
| 260 | const threadId = ThreadId.make("thread-runtime-request-lookup-failure"); |
| 261 | const providerSessionId = ProviderSessionId.make( |
| 262 | "provider-session-runtime-request-lookup-failure", |
| 263 | ); |
| 264 | const requestId = RuntimeRequestId.make("request-lookup-failure"); |
| 265 | const lookupFailure = new ProviderSessionManager.ProviderSessionLookupError({ |
| 266 | providerSessionId, |
| 267 | cause: "lookup failed", |
| 268 | }); |
| 269 | const layerTest = layerRuntimeRequestTest( |
| 270 | projectionWithRuntimeRequest(resolvedRuntimeRequest(requestId, providerSessionId)), |
| 271 | () => Effect.fail(lookupFailure), |
| 272 | ); |
| 273 | |
| 274 | return Effect.gen(function* () { |
| 275 | const service = yield* RuntimeRequestService.RuntimeRequestServiceV2; |
| 276 | const error = yield* service |
| 277 | .respond({ |
| 278 | threadId, |
| 279 | providerSessionId, |
| 280 | requestId, |
| 281 | decision: "accept", |
| 282 | }) |
| 283 | .pipe(Effect.flip); |
| 284 | |
| 285 | assert.instanceOf(error, RuntimeRequestService.RuntimeRequestResponseExecutionError); |
| 286 | assert.equal(error.reason, "unexpected-failure"); |
| 287 | assert.equal( |
| 288 | error.message, |
| 289 | `Failed to respond to runtime request ${requestId} on thread ${threadId} via provider session ${providerSessionId}.`, |
| 290 | ); |
| 291 | assert.strictEqual(error.cause, lookupFailure); |
| 292 | }).pipe(Effect.provide(layerTest)); |
| 293 | }); |