import { expect, it } from "@effect/vitest";import { CommandId, MessageId, NodeId, type OrchestrationV2Command, type OrchestrationV2ThreadProjection, ProjectId, ProviderInstanceId, RunId, ThreadId,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Ref from "effect/Ref"; import { LegacyV1ThreadImporter, LegacyV1ThreadImportError } from "./LegacyV1ThreadImporter.ts";import { OrchestratorProjectionError, OrchestratorV2 } from "./Orchestrator.ts";import { existingThreadIdsForCommand, layer, layerWithLegacyImporter, ThreadManagementDurableRunProjectionError, ThreadManagementProjectThreadsListError, ThreadManagementPostDispatchProjectionError, ThreadManagementProjectionLoadError, ThreadManagementRunNotFoundError, ThreadManagementService, ThreadManagementThreadNotFoundError, ThreadManagementThreadNotInterruptibleError, ThreadManagementThreadArchivedError, ThreadManagementNoSteerableRunError, withCreationProvenance,} from "./ThreadManagementService.ts"; it("stamps authoritative provenance on commands that create threads or messages", () => { const command: OrchestrationV2Command = { type: "thread.create", createdBy: "agent", creationSource: "mcp", commandId: CommandId.make("command:thread-management:create"), threadId: ThreadId.make("thread:thread-management:create"), projectId: ProjectId.make("project:thread-management"), title: "Thread management", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex", }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, }; expect( withCreationProvenance(command, { createdBy: "user", creationSource: "web", }), ).toMatchObject({ createdBy: "user", creationSource: "web", });}); it.effect("marks projection failures that occur after an accepted message dispatch", () => Effect.gen(function* () { const projectId = ProjectId.make("project:thread-management:post-dispatch"); const threadId = ThreadId.make("thread:thread-management:post-dispatch"); const messageId = MessageId.make("message:thread-management:post-dispatch"); const projection = { thread: { id: threadId, projectId, deletedAt: null, archivedAt: null, }, runs: [], } as unknown as OrchestrationV2ThreadProjection; const reads = yield* Ref.make(0); const projectionError = new OrchestratorProjectionError({ threadId, cause: new Error("projection unavailable after commit"), }); const testLayer = layer.pipe( Layer.provide( Layer.mock(OrchestratorV2)({ getThreadProjection: () => Ref.updateAndGet(reads, (count) => count + 1).pipe( Effect.flatMap((count) => count === 1 ? Effect.succeed(projection) : Effect.fail(projectionError), ), ), dispatch: () => Effect.succeed({ sequence: 1, storedEvents: [], replayed: false }), }), ), ); yield* Effect.gen(function* () { const service = yield* ThreadManagementService; const error = yield* service .sendToThread({ projectId, commandId: CommandId.make("command:thread-management:post-dispatch"), threadId, messageId, text: "hello", attachments: [], mode: "auto", createdBy: "agent", creationSource: "mcp", }) .pipe(Effect.flip); expect(error).toBeInstanceOf(ThreadManagementPostDispatchProjectionError); expect(error).toMatchObject({ projectId, threadId, messageId, dispatchReplayed: false, }); }).pipe(Effect.provide(testLayer)); }),); it("leaves commands that do not create durable authored content unchanged", () => { const command: OrchestrationV2Command = { type: "run.interrupt", commandId: CommandId.make("command:thread-management:interrupt"), threadId: ThreadId.make("thread:thread-management:interrupt"), runId: RunId.make("run:thread-management:interrupt"), }; expect( withCreationProvenance(command, { createdBy: "user", creationSource: "web", }), ).toBe(command);}); it("identifies every existing thread that must be hydrated before dispatch", () => { const sourceThreadId = ThreadId.make("thread:thread-management:source"); const targetThreadId = ThreadId.make("thread:thread-management:target"); const parentThreadId = ThreadId.make("thread:thread-management:parent"); expect( existingThreadIdsForCommand({ type: "thread.create", createdBy: "user", creationSource: "web", commandId: CommandId.make("command:thread-management:create"), threadId: targetThreadId, projectId: ProjectId.make("project:thread-management"), title: "Created thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex", }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, }), ).toEqual([]); expect( existingThreadIdsForCommand({ type: "thread.archive", commandId: CommandId.make("command:thread-management:archive"), threadId: targetThreadId, }), ).toEqual([targetThreadId]); // Read-state commands skip transcript hydration entirely: they fire on // every activity bump while a thread is open and never touch messages. expect( existingThreadIdsForCommand({ type: "thread.visit", commandId: CommandId.make("command:thread-management:visit"), threadId: targetThreadId, visitedAt: "2026-07-30T00:00:00.000Z", }), ).toEqual([]); expect( existingThreadIdsForCommand({ type: "thread.mark-unread", commandId: CommandId.make("command:thread-management:mark-unread"), threadId: targetThreadId, }), ).toEqual([]); expect( existingThreadIdsForCommand({ type: "thread.fork", createdBy: "user", creationSource: "web", commandId: CommandId.make("command:thread-management:fork"), sourceThreadId, targetThreadId, sourcePoint: { type: "run", runId: RunId.make("run:thread-management:source"), }, }), ).toEqual([sourceThreadId]); expect( existingThreadIdsForCommand({ type: "thread.merge_back", createdBy: "user", creationSource: "web", commandId: CommandId.make("command:thread-management:merge"), sourceThreadId, targetThreadId, sourcePoint: { type: "run", runId: RunId.make("run:thread-management:source"), }, }), ).toEqual([sourceThreadId, targetThreadId]); expect( existingThreadIdsForCommand({ type: "delegated_task.request", createdBy: "agent", creationSource: "provider", commandId: CommandId.make("command:thread-management:delegate"), parentThreadId, parentRunId: RunId.make("run:thread-management:parent"), parentNodeId: NodeId.make("node:thread-management:parent"), task: "Inspect the migration", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex", }, runtimeMode: "full-access", interactionMode: "default", }), ).toEqual([parentThreadId]); expect( existingThreadIdsForCommand({ type: "delegated_task.wake-policy", commandId: CommandId.make("command:thread-management:wake-policy"), parentThreadId, taskId: NodeId.make("node:thread-management:delegated"), completionWake: "always", }), ).toEqual([parentThreadId]); expect( existingThreadIdsForCommand({ type: "thread.created.record", commandId: CommandId.make("command:thread-management:record"), parentThreadId, parentRunId: RunId.make("run:thread-management:parent"), parentNodeId: NodeId.make("node:thread-management:parent"), targetThreadId, targetRunId: null, }), ).toEqual([parentThreadId, targetThreadId]);}); it("derives thread management messages from structural error attributes", () => { const projectId = ProjectId.make("project:thread-management:errors"); const threadId = ThreadId.make("thread:thread-management:errors"); const runId = RunId.make("run:thread-management:errors"); const messageId = MessageId.make("message:thread-management:errors"); const infrastructureCause = new Error("private sqlite detail"); const threadNotFound = new ThreadManagementThreadNotFoundError({ projectId, threadId, }); expect(threadNotFound).toMatchObject({ projectId, threadId }); expect(threadNotFound.message).toBe(`Thread ${threadId} was not found in project ${projectId}.`); const runNotFound = new ThreadManagementRunNotFoundError({ threadId, runId }); expect(runNotFound).toMatchObject({ threadId, runId }); expect(runNotFound.message).toBe(`Run ${runId} does not belong to thread ${threadId}.`); const archived = new ThreadManagementThreadArchivedError({ threadId, }); expect(archived).toMatchObject({ threadId }); expect(archived.message).toBe(`Thread ${threadId} is archived and cannot receive messages.`); const notSteerable = new ThreadManagementNoSteerableRunError({ threadId, mode: "restart", }); expect(notSteerable).toMatchObject({ threadId, mode: "restart", }); expect(notSteerable.message).toBe( `Thread ${threadId} has no running turn that can be restarted.`, ); const notInterruptible = new ThreadManagementThreadNotInterruptibleError({ threadId, runId, }); expect(notInterruptible).toMatchObject({ threadId, runId }); expect(notInterruptible.message).toBe(`Run ${runId} is not currently interruptible.`); const listFailure = new ThreadManagementProjectThreadsListError({ projectId, cause: infrastructureCause, }); expect(listFailure).toMatchObject({ projectId, cause: infrastructureCause }); expect(listFailure.message).toBe(`Unable to list threads in project ${projectId}.`); expect(listFailure.message).not.toContain(infrastructureCause.message); const durableProjectionFailure = new ThreadManagementDurableRunProjectionError({ threadId, messageId, dispatchReplayed: false, }); expect(durableProjectionFailure).toMatchObject({ threadId, messageId }); expect(durableProjectionFailure.message).toBe( `Message ${messageId} was accepted on thread ${threadId} without a durable run projection.`, );}); it.effect("classifies projection infrastructure failures separately from a missing thread", () => { const projectId = ProjectId.make("project:thread-management:projection-failure"); const threadId = ThreadId.make("thread:thread-management:projection-failure"); const infrastructureCause = new Error("sqlite read failed"); const projectionError = new OrchestratorProjectionError({ threadId, cause: infrastructureCause, }); const testLayer = layer.pipe( Layer.provide( Layer.mock(OrchestratorV2)({ getThreadProjection: () => Effect.fail(projectionError), }), ), ); return Effect.gen(function* () { const service = yield* ThreadManagementService; const error = yield* Effect.flip(service.getProjectThread({ projectId, threadId })); expect(error).toBeInstanceOf(ThreadManagementProjectionLoadError); expect(error).toMatchObject({ projectId, threadId, cause: projectionError, }); expect(error.message).toBe(`Unable to load thread ${threadId} in project ${projectId}.`); }).pipe(Effect.provide(testLayer));}); it.effect("uses thread-not-found only after a projection loads outside the project", () => { const projectId = ProjectId.make("project:thread-management:requested"); const otherProjectId = ProjectId.make("project:thread-management:other"); const threadId = ThreadId.make("thread:thread-management:wrong-project"); const projection = { thread: { id: threadId, projectId: otherProjectId, deletedAt: null, }, } as OrchestrationV2ThreadProjection; const testLayer = layer.pipe( Layer.provide( Layer.mock(OrchestratorV2)({ getThreadProjection: () => Effect.succeed(projection), }), ), ); return Effect.gen(function* () { const service = yield* ThreadManagementService; const error = yield* Effect.flip(service.getProjectThread({ projectId, threadId })); expect(error).toBeInstanceOf(ThreadManagementThreadNotFoundError); expect(error).toMatchObject({ projectId, threadId }); expect("cause" in error).toBe(false); }).pipe(Effect.provide(testLayer));}); it.effect("preserves failed legacy materialization when reading checkpoint context", () => { const threadId = ThreadId.make("thread:thread-management:checkpoint-import-failure"); const importError = new LegacyV1ThreadImportError({ threadId, operation: "hydrate transcript for", cause: new Error("checkpoint import failed"), }); const testLayer = layerWithLegacyImporter.pipe( Layer.provide( Layer.mergeAll( Layer.mock(OrchestratorV2)({ getCheckpointContext: () => Effect.succeed({ runs: [], checkpointScopes: [], checkpoints: [] }), }), Layer.mock(LegacyV1ThreadImporter)({ ensureTranscript: () => Effect.fail(importError), }), ), ), ); return Effect.gen(function* () { const service = yield* ThreadManagementService; const error = yield* service.getCheckpointContext(threadId).pipe(Effect.flip); expect(error).toBeInstanceOf(OrchestratorProjectionError); expect(error).toMatchObject({ threadId, cause: importError }); }).pipe(Effect.provide(testLayer));});
import { expect, it } from "@effect/vitest";
import {
CommandId,
MessageId,
NodeId,
type OrchestrationV2Command,
type OrchestrationV2ThreadProjection,
ProjectId,
ProviderInstanceId,
RunId,
ThreadId,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Ref from "effect/Ref";
import { LegacyV1ThreadImporter, LegacyV1ThreadImportError } from "./LegacyV1ThreadImporter.ts";
import { OrchestratorProjectionError, OrchestratorV2 } from "./Orchestrator.ts";
import {
existingThreadIdsForCommand,
layer,
layerWithLegacyImporter,
ThreadManagementDurableRunProjectionError,
ThreadManagementProjectThreadsListError,
ThreadManagementPostDispatchProjectionError,
ThreadManagementProjectionLoadError,
ThreadManagementRunNotFoundError,
ThreadManagementService,
ThreadManagementThreadNotFoundError,
ThreadManagementThreadNotInterruptibleError,
ThreadManagementThreadArchivedError,
ThreadManagementNoSteerableRunError,
withCreationProvenance,
} from "./ThreadManagementService.ts";
it("stamps authoritative provenance on commands that create threads or messages", () => {
const command: OrchestrationV2Command = {
type: "thread.create",
createdBy: "agent",
creationSource: "mcp",
commandId: CommandId.make("command:thread-management:create"),
threadId: ThreadId.make("thread:thread-management:create"),
projectId: ProjectId.make("project:thread-management"),
title: "Thread management",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
};
expect(
withCreationProvenance(command, {
createdBy: "user",
creationSource: "web",
}),
).toMatchObject({
createdBy: "user",
creationSource: "web",
});
});
it.effect("marks projection failures that occur after an accepted message dispatch", () =>
Effect.gen(function* () {
const projectId = ProjectId.make("project:thread-management:post-dispatch");
const threadId = ThreadId.make("thread:thread-management:post-dispatch");
const messageId = MessageId.make("message:thread-management:post-dispatch");
const projection = {
thread: {
id: threadId,
projectId,
deletedAt: null,
archivedAt: null,
},
runs: [],
} as unknown as OrchestrationV2ThreadProjection;
const reads = yield* Ref.make(0);
const projectionError = new OrchestratorProjectionError({
threadId,
cause: new Error("projection unavailable after commit"),
});
const testLayer = layer.pipe(
Layer.provide(
Layer.mock(OrchestratorV2)({
getThreadProjection: () =>
Ref.updateAndGet(reads, (count) => count + 1).pipe(
Effect.flatMap((count) =>
count === 1 ? Effect.succeed(projection) : Effect.fail(projectionError),
),
),
dispatch: () => Effect.succeed({ sequence: 1, storedEvents: [], replayed: false }),
}),
),
);
yield* Effect.gen(function* () {
const service = yield* ThreadManagementService;
const error = yield* service
.sendToThread({
projectId,
commandId: CommandId.make("command:thread-management:post-dispatch"),
threadId,
messageId,
text: "hello",
attachments: [],
mode: "auto",
createdBy: "agent",
creationSource: "mcp",
})
.pipe(Effect.flip);
expect(error).toBeInstanceOf(ThreadManagementPostDispatchProjectionError);
expect(error).toMatchObject({
projectId,
threadId,
messageId,
dispatchReplayed: false,
});
}).pipe(Effect.provide(testLayer));
}),
);
it("leaves commands that do not create durable authored content unchanged", () => {
const command: OrchestrationV2Command = {
type: "run.interrupt",
commandId: CommandId.make("command:thread-management:interrupt"),
threadId: ThreadId.make("thread:thread-management:interrupt"),
runId: RunId.make("run:thread-management:interrupt"),
};
expect(
withCreationProvenance(command, {
createdBy: "user",
creationSource: "web",
}),
).toBe(command);
});
it("identifies every existing thread that must be hydrated before dispatch", () => {
const sourceThreadId = ThreadId.make("thread:thread-management:source");
const targetThreadId = ThreadId.make("thread:thread-management:target");
const parentThreadId = ThreadId.make("thread:thread-management:parent");
expect(
existingThreadIdsForCommand({
type: "thread.create",
createdBy: "user",
creationSource: "web",
commandId: CommandId.make("command:thread-management:create"),
threadId: targetThreadId,
projectId: ProjectId.make("project:thread-management"),
title: "Created thread",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
}),
).toEqual([]);
expect(
existingThreadIdsForCommand({
type: "thread.archive",
commandId: CommandId.make("command:thread-management:archive"),
threadId: targetThreadId,
}),
).toEqual([targetThreadId]);
// Read-state commands skip transcript hydration entirely: they fire on
// every activity bump while a thread is open and never touch messages.
expect(
existingThreadIdsForCommand({
type: "thread.visit",
commandId: CommandId.make("command:thread-management:visit"),
threadId: targetThreadId,
visitedAt: "2026-07-30T00:00:00.000Z",
}),
).toEqual([]);
expect(
existingThreadIdsForCommand({
type: "thread.mark-unread",
commandId: CommandId.make("command:thread-management:mark-unread"),
threadId: targetThreadId,
}),
).toEqual([]);
expect(
existingThreadIdsForCommand({
type: "thread.fork",
createdBy: "user",
creationSource: "web",
commandId: CommandId.make("command:thread-management:fork"),
sourceThreadId,
targetThreadId,
sourcePoint: {
type: "run",
runId: RunId.make("run:thread-management:source"),
},
}),
).toEqual([sourceThreadId]);
expect(
existingThreadIdsForCommand({
type: "thread.merge_back",
createdBy: "user",
creationSource: "web",
commandId: CommandId.make("command:thread-management:merge"),
sourceThreadId,
targetThreadId,
sourcePoint: {
type: "run",
runId: RunId.make("run:thread-management:source"),
},
}),
).toEqual([sourceThreadId, targetThreadId]);
expect(
existingThreadIdsForCommand({
type: "delegated_task.request",
createdBy: "agent",
creationSource: "provider",
commandId: CommandId.make("command:thread-management:delegate"),
parentThreadId,
parentRunId: RunId.make("run:thread-management:parent"),
parentNodeId: NodeId.make("node:thread-management:parent"),
task: "Inspect the migration",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
},
runtimeMode: "full-access",
interactionMode: "default",
}),
).toEqual([parentThreadId]);
expect(
existingThreadIdsForCommand({
type: "delegated_task.wake-policy",
commandId: CommandId.make("command:thread-management:wake-policy"),
parentThreadId,
taskId: NodeId.make("node:thread-management:delegated"),
completionWake: "always",
}),
).toEqual([parentThreadId]);
expect(
existingThreadIdsForCommand({
type: "thread.created.record",
commandId: CommandId.make("command:thread-management:record"),
parentThreadId,
parentRunId: RunId.make("run:thread-management:parent"),
parentNodeId: NodeId.make("node:thread-management:parent"),
targetThreadId,
targetRunId: null,
}),
).toEqual([parentThreadId, targetThreadId]);
});
it("derives thread management messages from structural error attributes", () => {
const projectId = ProjectId.make("project:thread-management:errors");
const threadId = ThreadId.make("thread:thread-management:errors");
const runId = RunId.make("run:thread-management:errors");
const messageId = MessageId.make("message:thread-management:errors");
const infrastructureCause = new Error("private sqlite detail");
const threadNotFound = new ThreadManagementThreadNotFoundError({
projectId,
threadId,
});
expect(threadNotFound).toMatchObject({ projectId, threadId });
expect(threadNotFound.message).toBe(`Thread ${threadId} was not found in project ${projectId}.`);
const runNotFound = new ThreadManagementRunNotFoundError({ threadId, runId });
expect(runNotFound).toMatchObject({ threadId, runId });
expect(runNotFound.message).toBe(`Run ${runId} does not belong to thread ${threadId}.`);
const archived = new ThreadManagementThreadArchivedError({
threadId,
});
expect(archived).toMatchObject({ threadId });
expect(archived.message).toBe(`Thread ${threadId} is archived and cannot receive messages.`);
const notSteerable = new ThreadManagementNoSteerableRunError({
threadId,
mode: "restart",
});
expect(notSteerable).toMatchObject({
threadId,
mode: "restart",
});
expect(notSteerable.message).toBe(
`Thread ${threadId} has no running turn that can be restarted.`,
);
const notInterruptible = new ThreadManagementThreadNotInterruptibleError({
threadId,
runId,
});
expect(notInterruptible).toMatchObject({ threadId, runId });
expect(notInterruptible.message).toBe(`Run ${runId} is not currently interruptible.`);
const listFailure = new ThreadManagementProjectThreadsListError({
projectId,
cause: infrastructureCause,
});
expect(listFailure).toMatchObject({ projectId, cause: infrastructureCause });
expect(listFailure.message).toBe(`Unable to list threads in project ${projectId}.`);
expect(listFailure.message).not.toContain(infrastructureCause.message);
const durableProjectionFailure = new ThreadManagementDurableRunProjectionError({
threadId,
messageId,
dispatchReplayed: false,
});
expect(durableProjectionFailure).toMatchObject({ threadId, messageId });
expect(durableProjectionFailure.message).toBe(
`Message ${messageId} was accepted on thread ${threadId} without a durable run projection.`,
);
});
it.effect("classifies projection infrastructure failures separately from a missing thread", () => {
const projectId = ProjectId.make("project:thread-management:projection-failure");
const threadId = ThreadId.make("thread:thread-management:projection-failure");
const infrastructureCause = new Error("sqlite read failed");
const projectionError = new OrchestratorProjectionError({
threadId,
cause: infrastructureCause,
});
const testLayer = layer.pipe(
Layer.provide(
Layer.mock(OrchestratorV2)({
getThreadProjection: () => Effect.fail(projectionError),
}),
),
);
return Effect.gen(function* () {
const service = yield* ThreadManagementService;
const error = yield* Effect.flip(service.getProjectThread({ projectId, threadId }));
expect(error).toBeInstanceOf(ThreadManagementProjectionLoadError);
expect(error).toMatchObject({
projectId,
threadId,
cause: projectionError,
});
expect(error.message).toBe(`Unable to load thread ${threadId} in project ${projectId}.`);
}).pipe(Effect.provide(testLayer));
});
it.effect("uses thread-not-found only after a projection loads outside the project", () => {
const projectId = ProjectId.make("project:thread-management:requested");
const otherProjectId = ProjectId.make("project:thread-management:other");
const threadId = ThreadId.make("thread:thread-management:wrong-project");
const projection = {
thread: {
id: threadId,
projectId: otherProjectId,
deletedAt: null,
},
} as OrchestrationV2ThreadProjection;
const testLayer = layer.pipe(
Layer.provide(
Layer.mock(OrchestratorV2)({
getThreadProjection: () => Effect.succeed(projection),
}),
),
);
return Effect.gen(function* () {
const service = yield* ThreadManagementService;
const error = yield* Effect.flip(service.getProjectThread({ projectId, threadId }));
expect(error).toBeInstanceOf(ThreadManagementThreadNotFoundError);
expect(error).toMatchObject({ projectId, threadId });
expect("cause" in error).toBe(false);
}).pipe(Effect.provide(testLayer));
});
it.effect("preserves failed legacy materialization when reading checkpoint context", () => {
const threadId = ThreadId.make("thread:thread-management:checkpoint-import-failure");
const importError = new LegacyV1ThreadImportError({
threadId,
operation: "hydrate transcript for",
cause: new Error("checkpoint import failed"),
});
const testLayer = layerWithLegacyImporter.pipe(
Layer.provide(
Layer.mergeAll(
Layer.mock(OrchestratorV2)({
getCheckpointContext: () =>
Effect.succeed({ runs: [], checkpointScopes: [], checkpoints: [] }),
}),
Layer.mock(LegacyV1ThreadImporter)({
ensureTranscript: () => Effect.fail(importError),
}),
),
),
);
return Effect.gen(function* () {
const service = yield* ThreadManagementService;
const error = yield* service.getCheckpointContext(threadId).pipe(Effect.flip);
expect(error).toBeInstanceOf(OrchestratorProjectionError);
expect(error).toMatchObject({ threadId, cause: importError });
}).pipe(Effect.provide(testLayer));
});