1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import { expect, it } from "@effect/vitest";
3import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6
7import { OrchestratorProjectionError } from "../orchestration-v2/Orchestrator.ts";
8import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts";
9import type * as McpInvocationContext from "./McpInvocationContext.ts";
10import * as ThreadMetadataMcp from "./ThreadMetadataMcpService.ts";
11
12const threadId = ThreadId.make("thread:metadata-caller");
13const scope: McpInvocationContext.McpInvocationScope = {
14 environmentId: EnvironmentId.make("environment:metadata-test"),
15 requestNamespace: "provider-session:metadata-test",
16 thread: {
17 threadId,
18 providerSessionId: "provider-session:metadata-test",
19 providerInstanceId: ProviderInstanceId.make("codex"),
20 },
21 client: undefined,
22 capabilities: new Set(["orchestration"]),
23 issuedAt: 1,
24};
25
26function layerService(
27 getThreadShell: ThreadManagement.ThreadManagementService["Service"]["getThreadShell"],
28) {
29 return ThreadMetadataMcp.layer.pipe(
30 Layer.provide(
31 Layer.merge(
32 Layer.mock(ThreadManagement.ThreadManagementService)({
33 getThreadShell,
34 getThreadRecords: () => Effect.die("projection must not load after shell failure"),
35 } satisfies Partial<ThreadManagement.ThreadManagementService["Service"]>),
36 NodeCrypto.layer,
37 ),
38 ),
39 );
40}
41
42const updateCallingThread = Effect.gen(function* () {
43 const service = yield* ThreadMetadataMcp.ThreadMetadataMcpService;
44 return yield* service.update(scope, {
45 action: "rename",
46 title: "Renamed thread",
47 clientRequestId: "metadata-caller-classification",
48 });
49});
50
51it.effect("reports an absent calling thread as thread_not_found", () =>
52 Effect.gen(function* () {
53 const error = yield* updateCallingThread.pipe(
54 Effect.provide(layerService(() => Effect.succeed(null))),
55 Effect.flip,
56 );
57
58 expect(error.code).toBe("thread_not_found");
59 }),
60);
61
62it.effect("keeps calling-thread storage failures as orchestration errors", () =>
63 Effect.gen(function* () {
64 const error = yield* updateCallingThread.pipe(
65 Effect.provide(
66 layerService(() =>
67 Effect.fail(
68 new OrchestratorProjectionError({
69 threadId,
70 cause: new Error("storage unavailable"),
71 }),
72 ),
73 ),
74 ),
75 Effect.flip,
76 );
77
78 expect(error.code).toBe("orchestration_error");
79 }),
80);