import { EnvironmentId, EventId, ORCHESTRATION_WS_METHODS, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationThread, type OrchestrationThreadDetailSnapshot, type OrchestrationThreadStreamItem,} from "@t3tools/contracts";import { describe, expect, it } from "@effect/vitest";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Option from "effect/Option";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as SubscriptionRef from "effect/SubscriptionRef";import * as TestClock from "effect/testing/TestClock"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts";import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget, type PreparedConnection, type SupervisorConnectionState,} from "../connection/model.ts";import * as ConnectionWakeups from "../connection/wakeups.ts";import * as EnvironmentSupervisor from "../connection/supervisor.ts";import * as Persistence from "../platform/persistence.ts";import * as RpcSession from "../rpc/session.ts";import { EMPTY_ENVIRONMENT_THREAD_STATE, makeEnvironmentThreadState, ThreadSnapshotLoader, type EnvironmentThreadState,} from "./threads.ts"; const TARGET = new PrimaryConnectionTarget({ environmentId: EnvironmentId.make("environment-1"), label: "Test environment", httpBaseUrl: "https://environment.example.test", wsBaseUrl: "wss://environment.example.test",});const THREAD_ID = ThreadId.make("thread-1");const CACHED_SNAPSHOT_SEQUENCE = 7;const PREPARED: PreparedConnection = { environmentId: TARGET.environmentId, label: TARGET.label, httpBaseUrl: TARGET.httpBaseUrl, socketUrl: TARGET.wsBaseUrl, httpAuthorization: null, target: TARGET,};const BASE_THREAD: OrchestrationThread = { id: THREAD_ID, projectId: ProjectId.make("project-1"), title: "Cached thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4", }, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, pullRequests: [], deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null,};const ACTIVE_THREAD: OrchestrationThread = { ...BASE_THREAD, latestTurn: { turnId: TurnId.make("turn-1"), state: "running", requestedAt: "2026-04-01T00:01:00.000Z", startedAt: "2026-04-01T00:01:00.000Z", completedAt: null, assistantMessageId: null, }, session: { threadId: THREAD_ID, status: "running", providerName: "codex", runtimeMode: "full-access", activeTurnId: TurnId.make("turn-1"), lastError: null, updatedAt: "2026-04-01T00:01:00.000Z", },}; type TestThreadInput = OrchestrationThreadStreamItem | Error; function testSession( client: WsRpcProtocolClient, options?: { readonly completionMarker?: boolean },): RpcSession.RpcSession { return { client, initialConfig: Effect.succeed( options?.completionMarker === true ? ({ threadResumeCompletionMarker: true } as never) : ({} as never), ), subscribeServerConfig: (input) => client.subscribeServerConfig(input), ready: Effect.void, probe: Effect.void, closed: Effect.never, };} function awaitThreadState( observed: Queue.Queue<EnvironmentThreadState>, predicate: (state: EnvironmentThreadState) => boolean,) { return Queue.take(observed).pipe( Effect.repeat({ until: predicate, }), );} const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (options?: { readonly cached?: OrchestrationThread; readonly httpSnapshot?: Option.Option<OrchestrationThreadDetailSnapshot>; readonly completionMarker?: boolean; readonly resumeCache?: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]>; readonly loadCached?: Effect.Effect<Option.Option<OrchestrationThreadDetailSnapshot>>; readonly saveThread?: Persistence.EnvironmentCacheStore["Service"]["saveThread"];}) { const inputs = yield* Queue.unbounded<TestThreadInput>(); const observed = yield* Queue.unbounded<EnvironmentThreadState>(); const latest = yield* Ref.make<EnvironmentThreadState>(EMPTY_ENVIRONMENT_THREAD_STATE); const retryCount = yield* Ref.make(0); const subscriptionCount = yield* Ref.make(0); const loaderCalls = yield* Ref.make(0); const lastSubscribeAfterSequence = yield* Ref.make<number | undefined>(undefined); const lastRequestCompletionMarker = yield* Ref.make<boolean | undefined>(undefined); const savedThreads = yield* Ref.make<ReadonlyArray<OrchestrationThreadDetailSnapshot>>([]); const removedThreads = yield* Ref.make<ReadonlyArray<ThreadId>>([]); const wakeups = yield* Queue.unbounded<ConnectionWakeups.ConnectionWakeup>(); const supervisorState = yield* SubscriptionRef.make<SupervisorConnectionState>( AVAILABLE_CONNECTION_STATE, ); const streamFrom = (queue: Queue.Queue<TestThreadInput>) => Stream.fromQueue(queue).pipe( Stream.mapEffect((input) => input instanceof Error ? Effect.fail(input) : Effect.succeed(input), ), ); const client = { [ORCHESTRATION_WS_METHODS.subscribeThread]: (input: { readonly afterSequence?: number; readonly requestCompletionMarker?: boolean; }) => Stream.unwrap( Ref.updateAndGet(subscriptionCount, (count) => count + 1).pipe( Effect.andThen(Ref.set(lastSubscribeAfterSequence, input.afterSequence)), Effect.andThen(Ref.set(lastRequestCompletionMarker, input.requestCompletionMarker)), Effect.as(streamFrom(inputs)), ), ), } as unknown as WsRpcProtocolClient; const supervisorSession = yield* SubscriptionRef.make<Option.Option<RpcSession.RpcSession>>( Option.some( testSession( client, options?.completionMarker === true ? { completionMarker: true } : undefined, ), ), ); const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>( Option.some(PREPARED), ); const snapshotLoader = ThreadSnapshotLoader.of({ load: (_prepared, threadId) => Ref.update(loaderCalls, (count) => count + 1).pipe( Effect.as( threadId === THREAD_ID ? (options?.httpSnapshot ?? Option.none<OrchestrationThreadDetailSnapshot>()) : Option.none<OrchestrationThreadDetailSnapshot>(), ), ), }); const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ target: TARGET, state: supervisorState, session: supervisorSession, prepared, connect: Effect.void, disconnect: Effect.void, retryNow: Ref.update(retryCount, (count) => count + 1), } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); const cache = Persistence.EnvironmentCacheStore.of({ loadShell: () => Effect.succeed(Option.none()), saveShell: () => Effect.void, loadThread: (_environmentId, threadId) => options?.loadCached ?? Effect.succeed( threadId === THREAD_ID && options?.cached !== undefined ? Option.some({ snapshotSequence: CACHED_SNAPSHOT_SEQUENCE, thread: options.cached, }) : Option.none(), ), saveThread: (environmentId, thread) => Ref.update(savedThreads, (current) => [...current, thread]).pipe( Effect.andThen(options?.saveThread?.(environmentId, thread) ?? Effect.void), ), removeThread: (_environmentId, threadId) => Ref.update(removedThreads, (current) => [...current, threadId]), loadServerConfig: () => Effect.succeed(Option.none()), saveServerConfig: () => Effect.void, loadVcsRefs: () => Effect.succeed(Option.none()), saveVcsRefs: () => Effect.void, removeVcsRefs: () => Effect.void, clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); const threadState = yield* makeEnvironmentThreadState(THREAD_ID, options?.resumeCache).pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), Effect.provideService(ThreadSnapshotLoader, snapshotLoader), Effect.provideService( ConnectionWakeups.ConnectionWakeups, ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.fromQueue(wakeups) }), ), ); yield* SubscriptionRef.changes(threadState).pipe( Stream.runForEach((state) => Ref.set(latest, state).pipe(Effect.andThen(Queue.offer(observed, state))), ), Effect.forkScoped, ); return { threadState, inputs, observed, latest, retryCount, subscriptionCount, loaderCalls, lastSubscribeAfterSequence, lastRequestCompletionMarker, supervisorState, supervisorSession, savedThreads, removedThreads, wakeups, replaceSession: SubscriptionRef.set( supervisorSession, Option.some( testSession( client, options?.completionMarker === true ? { completionMarker: true } : undefined, ), ), ), };}); const snapshot = (thread: OrchestrationThread): OrchestrationThreadStreamItem => ({ kind: "snapshot", snapshot: { snapshotSequence: 1, thread, },}); const synchronized = (): OrchestrationThreadStreamItem => ({ kind: "synchronized" }); const titleUpdated = (title: string, sequence = 2): OrchestrationThreadStreamItem => ({ kind: "event", event: { eventId: EventId.make("event-title"), sequence, occurredAt: "2026-04-01T01:00:00.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, aggregateKind: "thread", aggregateId: THREAD_ID, type: "thread.meta-updated", payload: { threadId: THREAD_ID, title, updatedAt: "2026-04-01T01:00:00.000Z", }, },}); const deleted = (): OrchestrationThreadStreamItem => ({ kind: "event", event: { eventId: EventId.make("event-deleted"), sequence: 3, occurredAt: "2026-04-01T02:00:00.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, aggregateKind: "thread", aggregateId: THREAD_ID, type: "thread.deleted", payload: { threadId: THREAD_ID, deletedAt: "2026-04-01T02:00:00.000Z", }, },}); describe("EnvironmentThreads", () => { for (const source of ["disk", "HTTP"] as const) { it.effect(`does not rewrite an unchanged ${source} snapshot on navigation or warm return`, () => Effect.gen(function* () { const resumeCache: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]> = { snapshot: undefined, owner: undefined, }; const firstSaved = yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ resumeCache, ...(source === "disk" ? { cached: BASE_THREAD } : { httpSnapshot: Option.some({ snapshotSequence: 7, thread: BASE_THREAD }) }), }); yield* awaitThreadState(h.observed, (value) => value.status === "live"); if (source === "HTTP") yield* TestClock.adjust("500 millis"); return h.savedThreads; }), ); expect(yield* Ref.get(firstSaved)).toHaveLength(source === "disk" ? 0 : 1); const nextSaved = yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ resumeCache }); yield* awaitThreadState(h.observed, (value) => value.status === "live"); return h.savedThreads; }), ); expect(yield* Ref.get(nextSaved)).toEqual([]); }), ); } it.effect("retries a failed background cache write when the scope closes", () => Effect.gen(function* () { let attempts = 0; yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ httpSnapshot: Option.some({ snapshotSequence: 7, thread: BASE_THREAD }), saveThread: () => Effect.suspend(() => { attempts += 1; return attempts === 1 ? Effect.fail( new Persistence.ConnectionPersistenceError({ operation: "save-thread", message: "Test storage failure", }), ) : Effect.void; }), }); yield* awaitThreadState(h.observed, (value) => value.status === "live"); yield* TestClock.adjust("500 millis"); expect(attempts).toBe(1); }), ); expect(attempts).toBe(2); }), ); it.effect("flushes newer data after an older background write completes", () => Effect.gen(function* () { const writing = yield* Deferred.make<void>(); const written = yield* Deferred.make<void>(); const release = yield* Deferred.make<void>(); const saved = yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ cached: BASE_THREAD, saveThread: (_environmentId, snapshot) => snapshot.snapshotSequence === 8 ? Deferred.succeed(writing, undefined).pipe( Effect.andThen(Deferred.await(release)), Effect.andThen(Deferred.succeed(written, undefined)), ) : Effect.void, }); yield* Queue.offer(h.inputs, titleUpdated("First update", 8)); yield* awaitThreadState( h.observed, (value) => Option.getOrNull(value.data)?.title === "First update", ); yield* TestClock.adjust("500 millis"); yield* Deferred.await(writing); yield* Queue.offer(h.inputs, titleUpdated("Newer update", 9)); yield* awaitThreadState( h.observed, (value) => Option.getOrNull(value.data)?.title === "Newer update", ); yield* Deferred.succeed(release, undefined); yield* Deferred.await(written); return h.savedThreads; }), ); expect( (yield* Ref.get(saved)).map((snapshot) => [ snapshot.snapshotSequence, snapshot.thread.title, ]), ).toEqual([ [8, "First update"], [9, "Newer update"], ]); }), ); it.effect("does not resume past a canceled event whose data was not applied", () => Effect.gen(function* () { const resumeCache: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]> = { snapshot: undefined, owner: undefined, }; const scope = yield* Effect.acquireRelease(Scope.make(), (scope) => Scope.close(scope, Exit.void), ); const first = yield* makeHarness({ httpSnapshot: Option.some({ thread: BASE_THREAD, snapshotSequence: CACHED_SNAPSHOT_SEQUENCE, }), resumeCache, }).pipe(Effect.provideService(Scope.Scope, scope)); yield* awaitThreadState(first.observed, (value) => value.status === "live"); const applying = yield* Deferred.make<void>(); const update = titleUpdated("Not applied", 8); if (update.kind !== "event") return yield* Effect.die("Expected an event"); Object.defineProperty(update.event.payload, "title", { get: () => { Deferred.doneUnsafe(applying, Exit.void); return "Not applied"; }, }); // The reducer reads the title after it advances the cursor. Hold only // the data write so closing the scope interrupts that exact interval. yield* first.threadState.semaphore.take(1); yield* Queue.offer(first.inputs, update); yield* Deferred.await(applying); yield* Scope.close(scope, Exit.void); yield* first.threadState.semaphore.release(1); const resumed = yield* makeHarness({ resumeCache }); const state = yield* awaitThreadState(resumed.observed, (value) => value.status === "live"); expect(Option.getOrThrow(state.data).title).toBe(BASE_THREAD.title); expect(yield* Ref.get(resumed.lastSubscribeAfterSequence)).toBe(CACHED_SNAPSHOT_SEQUENCE); expect(yield* Ref.get(first.savedThreads)).toEqual([ { thread: BASE_THREAD, snapshotSequence: CACHED_SNAPSHOT_SEQUENCE, }, ]); }), ); it.effect("prevents an old scope from replacing its successor's cached data", () => Effect.gen(function* () { const resumeCache: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]> = { snapshot: undefined, owner: undefined, }; const oldScope = yield* Effect.acquireRelease(Scope.make(), (scope) => Scope.close(scope, Exit.void), ); const old = yield* makeHarness({ cached: BASE_THREAD, resumeCache }).pipe( Effect.provideService(Scope.Scope, oldScope), ); yield* awaitThreadState(old.observed, (value) => value.status === "live"); const successor = yield* makeHarness({ resumeCache }); yield* Queue.offer(successor.inputs, titleUpdated("Successor title", 9));Lines 1–500 of 992. Load more
import { EnvironmentId, EventId, ORCHESTRATION_WS_METHODS, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationThread, type OrchestrationThreadDetailSnapshot, type OrchestrationThreadStreamItem, } from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import * as TestClock from "effect/testing/TestClock"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget, type PreparedConnection, type SupervisorConnectionState, } from "../connection/model.ts"; import * as ConnectionWakeups from "../connection/wakeups.ts"; import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import * as Persistence from "../platform/persistence.ts"; import * as RpcSession from "../rpc/session.ts"; import { EMPTY_ENVIRONMENT_THREAD_STATE, makeEnvironmentThreadState, ThreadSnapshotLoader, type EnvironmentThreadState, } from "./threads.ts"; const TARGET = new PrimaryConnectionTarget({ environmentId: EnvironmentId.make("environment-1"), label: "Test environment", httpBaseUrl: "https://environment.example.test", wsBaseUrl: "wss://environment.example.test", }); const THREAD_ID = ThreadId.make("thread-1"); const CACHED_SNAPSHOT_SEQUENCE = 7; const PREPARED: PreparedConnection = { environmentId: TARGET.environmentId, label: TARGET.label, httpBaseUrl: TARGET.httpBaseUrl, socketUrl: TARGET.wsBaseUrl, httpAuthorization: null, target: TARGET, }; const BASE_THREAD: OrchestrationThread = { id: THREAD_ID, projectId: ProjectId.make("project-1"), title: "Cached thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4", }, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, pullRequests: [], deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null, }; const ACTIVE_THREAD: OrchestrationThread = { ...BASE_THREAD, latestTurn: { turnId: TurnId.make("turn-1"), state: "running", requestedAt: "2026-04-01T00:01:00.000Z", startedAt: "2026-04-01T00:01:00.000Z", completedAt: null, assistantMessageId: null, }, session: { threadId: THREAD_ID, status: "running", providerName: "codex", runtimeMode: "full-access", activeTurnId: TurnId.make("turn-1"), lastError: null, updatedAt: "2026-04-01T00:01:00.000Z", }, }; type TestThreadInput = OrchestrationThreadStreamItem | Error; function testSession( client: WsRpcProtocolClient, options?: { readonly completionMarker?: boolean }, ): RpcSession.RpcSession { return { client, initialConfig: Effect.succeed( options?.completionMarker === true ? ({ threadResumeCompletionMarker: true } as never) : ({} as never), ), subscribeServerConfig: (input) => client.subscribeServerConfig(input), ready: Effect.void, probe: Effect.void, closed: Effect.never, }; } function awaitThreadState( observed: Queue.Queue<EnvironmentThreadState>, predicate: (state: EnvironmentThreadState) => boolean, ) { return Queue.take(observed).pipe( Effect.repeat({ until: predicate, }), ); } const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (options?: { readonly cached?: OrchestrationThread; readonly httpSnapshot?: Option.Option<OrchestrationThreadDetailSnapshot>; readonly completionMarker?: boolean; readonly resumeCache?: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]>; readonly loadCached?: Effect.Effect<Option.Option<OrchestrationThreadDetailSnapshot>>; readonly saveThread?: Persistence.EnvironmentCacheStore["Service"]["saveThread"]; }) { const inputs = yield* Queue.unbounded<TestThreadInput>(); const observed = yield* Queue.unbounded<EnvironmentThreadState>(); const latest = yield* Ref.make<EnvironmentThreadState>(EMPTY_ENVIRONMENT_THREAD_STATE); const retryCount = yield* Ref.make(0); const subscriptionCount = yield* Ref.make(0); const loaderCalls = yield* Ref.make(0); const lastSubscribeAfterSequence = yield* Ref.make<number | undefined>(undefined); const lastRequestCompletionMarker = yield* Ref.make<boolean | undefined>(undefined); const savedThreads = yield* Ref.make<ReadonlyArray<OrchestrationThreadDetailSnapshot>>([]); const removedThreads = yield* Ref.make<ReadonlyArray<ThreadId>>([]); const wakeups = yield* Queue.unbounded<ConnectionWakeups.ConnectionWakeup>(); const supervisorState = yield* SubscriptionRef.make<SupervisorConnectionState>( AVAILABLE_CONNECTION_STATE, ); const streamFrom = (queue: Queue.Queue<TestThreadInput>) => Stream.fromQueue(queue).pipe( Stream.mapEffect((input) => input instanceof Error ? Effect.fail(input) : Effect.succeed(input), ), ); const client = { [ORCHESTRATION_WS_METHODS.subscribeThread]: (input: { readonly afterSequence?: number; readonly requestCompletionMarker?: boolean; }) => Stream.unwrap( Ref.updateAndGet(subscriptionCount, (count) => count + 1).pipe( Effect.andThen(Ref.set(lastSubscribeAfterSequence, input.afterSequence)), Effect.andThen(Ref.set(lastRequestCompletionMarker, input.requestCompletionMarker)), Effect.as(streamFrom(inputs)), ), ), } as unknown as WsRpcProtocolClient; const supervisorSession = yield* SubscriptionRef.make<Option.Option<RpcSession.RpcSession>>( Option.some( testSession( client, options?.completionMarker === true ? { completionMarker: true } : undefined, ), ), ); const prepared = yield* SubscriptionRef.make<Option.Option<PreparedConnection>>( Option.some(PREPARED), ); const snapshotLoader = ThreadSnapshotLoader.of({ load: (_prepared, threadId) => Ref.update(loaderCalls, (count) => count + 1).pipe( Effect.as( threadId === THREAD_ID ? (options?.httpSnapshot ?? Option.none<OrchestrationThreadDetailSnapshot>()) : Option.none<OrchestrationThreadDetailSnapshot>(), ), ), }); const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ target: TARGET, state: supervisorState, session: supervisorSession, prepared, connect: Effect.void, disconnect: Effect.void, retryNow: Ref.update(retryCount, (count) => count + 1), } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); const cache = Persistence.EnvironmentCacheStore.of({ loadShell: () => Effect.succeed(Option.none()), saveShell: () => Effect.void, loadThread: (_environmentId, threadId) => options?.loadCached ?? Effect.succeed( threadId === THREAD_ID && options?.cached !== undefined ? Option.some({ snapshotSequence: CACHED_SNAPSHOT_SEQUENCE, thread: options.cached, }) : Option.none(), ), saveThread: (environmentId, thread) => Ref.update(savedThreads, (current) => [...current, thread]).pipe( Effect.andThen(options?.saveThread?.(environmentId, thread) ?? Effect.void), ), removeThread: (_environmentId, threadId) => Ref.update(removedThreads, (current) => [...current, threadId]), loadServerConfig: () => Effect.succeed(Option.none()), saveServerConfig: () => Effect.void, loadVcsRefs: () => Effect.succeed(Option.none()), saveVcsRefs: () => Effect.void, removeVcsRefs: () => Effect.void, clearVcsRefs: () => Effect.void, clear: () => Effect.void, }); const threadState = yield* makeEnvironmentThreadState(THREAD_ID, options?.resumeCache).pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), Effect.provideService(ThreadSnapshotLoader, snapshotLoader), Effect.provideService( ConnectionWakeups.ConnectionWakeups, ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.fromQueue(wakeups) }), ), ); yield* SubscriptionRef.changes(threadState).pipe( Stream.runForEach((state) => Ref.set(latest, state).pipe(Effect.andThen(Queue.offer(observed, state))), ), Effect.forkScoped, ); return { threadState, inputs, observed, latest, retryCount, subscriptionCount, loaderCalls, lastSubscribeAfterSequence, lastRequestCompletionMarker, supervisorState, supervisorSession, savedThreads, removedThreads, wakeups, replaceSession: SubscriptionRef.set( supervisorSession, Option.some( testSession( client, options?.completionMarker === true ? { completionMarker: true } : undefined, ), ), ), }; }); const snapshot = (thread: OrchestrationThread): OrchestrationThreadStreamItem => ({ kind: "snapshot", snapshot: { snapshotSequence: 1, thread, }, }); const synchronized = (): OrchestrationThreadStreamItem => ({ kind: "synchronized" }); const titleUpdated = (title: string, sequence = 2): OrchestrationThreadStreamItem => ({ kind: "event", event: { eventId: EventId.make("event-title"), sequence, occurredAt: "2026-04-01T01:00:00.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, aggregateKind: "thread", aggregateId: THREAD_ID, type: "thread.meta-updated", payload: { threadId: THREAD_ID, title, updatedAt: "2026-04-01T01:00:00.000Z", }, }, }); const deleted = (): OrchestrationThreadStreamItem => ({ kind: "event", event: { eventId: EventId.make("event-deleted"), sequence: 3, occurredAt: "2026-04-01T02:00:00.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, aggregateKind: "thread", aggregateId: THREAD_ID, type: "thread.deleted", payload: { threadId: THREAD_ID, deletedAt: "2026-04-01T02:00:00.000Z", }, }, }); describe("EnvironmentThreads", () => { for (const source of ["disk", "HTTP"] as const) { it.effect(`does not rewrite an unchanged ${source} snapshot on navigation or warm return`, () => Effect.gen(function* () { const resumeCache: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]> = { snapshot: undefined, owner: undefined, }; const firstSaved = yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ resumeCache, ...(source === "disk" ? { cached: BASE_THREAD } : { httpSnapshot: Option.some({ snapshotSequence: 7, thread: BASE_THREAD }) }), }); yield* awaitThreadState(h.observed, (value) => value.status === "live"); if (source === "HTTP") yield* TestClock.adjust("500 millis"); return h.savedThreads; }), ); expect(yield* Ref.get(firstSaved)).toHaveLength(source === "disk" ? 0 : 1); const nextSaved = yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ resumeCache }); yield* awaitThreadState(h.observed, (value) => value.status === "live"); return h.savedThreads; }), ); expect(yield* Ref.get(nextSaved)).toEqual([]); }), ); } it.effect("retries a failed background cache write when the scope closes", () => Effect.gen(function* () { let attempts = 0; yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ httpSnapshot: Option.some({ snapshotSequence: 7, thread: BASE_THREAD }), saveThread: () => Effect.suspend(() => { attempts += 1; return attempts === 1 ? Effect.fail( new Persistence.ConnectionPersistenceError({ operation: "save-thread", message: "Test storage failure", }), ) : Effect.void; }), }); yield* awaitThreadState(h.observed, (value) => value.status === "live"); yield* TestClock.adjust("500 millis"); expect(attempts).toBe(1); }), ); expect(attempts).toBe(2); }), ); it.effect("flushes newer data after an older background write completes", () => Effect.gen(function* () { const writing = yield* Deferred.make<void>(); const written = yield* Deferred.make<void>(); const release = yield* Deferred.make<void>(); const saved = yield* Effect.scoped( Effect.gen(function* () { const h = yield* makeHarness({ cached: BASE_THREAD, saveThread: (_environmentId, snapshot) => snapshot.snapshotSequence === 8 ? Deferred.succeed(writing, undefined).pipe( Effect.andThen(Deferred.await(release)), Effect.andThen(Deferred.succeed(written, undefined)), ) : Effect.void, }); yield* Queue.offer(h.inputs, titleUpdated("First update", 8)); yield* awaitThreadState( h.observed, (value) => Option.getOrNull(value.data)?.title === "First update", ); yield* TestClock.adjust("500 millis"); yield* Deferred.await(writing); yield* Queue.offer(h.inputs, titleUpdated("Newer update", 9)); yield* awaitThreadState( h.observed, (value) => Option.getOrNull(value.data)?.title === "Newer update", ); yield* Deferred.succeed(release, undefined); yield* Deferred.await(written); return h.savedThreads; }), ); expect( (yield* Ref.get(saved)).map((snapshot) => [ snapshot.snapshotSequence, snapshot.thread.title, ]), ).toEqual([ [8, "First update"], [9, "Newer update"], ]); }), ); it.effect("does not resume past a canceled event whose data was not applied", () => Effect.gen(function* () { const resumeCache: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]> = { snapshot: undefined, owner: undefined, }; const scope = yield* Effect.acquireRelease(Scope.make(), (scope) => Scope.close(scope, Exit.void), ); const first = yield* makeHarness({ httpSnapshot: Option.some({ thread: BASE_THREAD, snapshotSequence: CACHED_SNAPSHOT_SEQUENCE, }), resumeCache, }).pipe(Effect.provideService(Scope.Scope, scope)); yield* awaitThreadState(first.observed, (value) => value.status === "live"); const applying = yield* Deferred.make<void>(); const update = titleUpdated("Not applied", 8); if (update.kind !== "event") return yield* Effect.die("Expected an event"); Object.defineProperty(update.event.payload, "title", { get: () => { Deferred.doneUnsafe(applying, Exit.void); return "Not applied"; }, }); // The reducer reads the title after it advances the cursor. Hold only // the data write so closing the scope interrupts that exact interval. yield* first.threadState.semaphore.take(1); yield* Queue.offer(first.inputs, update); yield* Deferred.await(applying); yield* Scope.close(scope, Exit.void); yield* first.threadState.semaphore.release(1); const resumed = yield* makeHarness({ resumeCache }); const state = yield* awaitThreadState(resumed.observed, (value) => value.status === "live"); expect(Option.getOrThrow(state.data).title).toBe(BASE_THREAD.title); expect(yield* Ref.get(resumed.lastSubscribeAfterSequence)).toBe(CACHED_SNAPSHOT_SEQUENCE); expect(yield* Ref.get(first.savedThreads)).toEqual([ { thread: BASE_THREAD, snapshotSequence: CACHED_SNAPSHOT_SEQUENCE, }, ]); }), ); it.effect("prevents an old scope from replacing its successor's cached data", () => Effect.gen(function* () { const resumeCache: NonNullable<Parameters<typeof makeEnvironmentThreadState>[1]> = { snapshot: undefined, owner: undefined, }; const oldScope = yield* Effect.acquireRelease(Scope.make(), (scope) => Scope.close(scope, Exit.void), ); const old = yield* makeHarness({ cached: BASE_THREAD, resumeCache }).pipe( Effect.provideService(Scope.Scope, oldScope), ); yield* awaitThreadState(old.observed, (value) => value.status === "live"); const successor = yield* makeHarness({ resumeCache }); yield* Queue.offer(successor.inputs, titleUpdated("Successor title", 9));