import { CommandId, EventId, MessageId, ProjectId, ProviderInstanceId, ThreadId, type OrchestrationReadModel, type OrchestrationThread,} from "@t3tools/contracts";import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import * as Effect from "effect/Effect"; import { decideOrchestrationCommand } from "./decider.ts"; const NOW = "2026-01-01T00:00:00.000Z";// The decider's clock is the Effect test clock, pinned to the epoch, so// "future" wake times are relative to 1970-01-01T00:00:00.000Z.const FUTURE_WAKE = "1970-01-02T09:00:00.000Z";const PAST_WAKE = "1969-12-31T09:00:00.000Z";const SNOOZED_AT = "1969-12-30T00:00:00.000Z"; function makeReadModel(input: { readonly snoozedUntil?: string | null; readonly snoozedAt?: string | null; readonly archivedAt?: string | null; readonly activities?: OrchestrationThread["activities"]; readonly messages?: OrchestrationThread["messages"];}): OrchestrationReadModel { return { snapshotSequence: 0, projects: [], threads: [ { id: ThreadId.make("thread-1"), projectId: ProjectId.make("project-1"), title: "Thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: NOW, updatedAt: NOW, archivedAt: input.archivedAt ?? null, settledOverride: null, settledAt: null, snoozedUntil: input.snoozedUntil ?? null, snoozedAt: input.snoozedAt ?? (input.snoozedUntil != null ? SNOOZED_AT : null), deletedAt: null, messages: input.messages ?? [], proposedPlans: [], activities: input.activities ?? [], checkpoints: [], session: null, }, ], updatedAt: NOW, };} it.layer(NodeServices.layer)("snoozed thread decider", (it) => { it.effect("snoozes a thread to a future wake time", () => Effect.gen(function* () { const event = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze"), threadId: ThreadId.make("thread-1"), snoozedUntil: FUTURE_WAKE, }, readModel: makeReadModel({}), }); const events = Array.isArray(event) ? event : [event]; expect(events).toHaveLength(1); expect(events[0]?.type).toBe("thread.snoozed"); if (events[0]?.type === "thread.snoozed") { expect(events[0].payload.snoozedUntil).toBe(FUTURE_WAKE); expect(events[0].payload.snoozedAt).toBe(events[0].payload.updatedAt); } }), ); it.effect("rejects a wake time that is not in the future", () => Effect.gen(function* () { const error = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze-past"), threadId: ThreadId.make("thread-1"), snoozedUntil: PAST_WAKE, }, readModel: makeReadModel({}), }).pipe(Effect.flip); expect(error._tag).toBe("OrchestrationCommandInvariantError"); }), ); it.effect("rejects an unparseable wake time", () => Effect.gen(function* () { // IsoDateTime is structurally a string, so garbage can reach the // decider; a NaN wake time must never persist as snooze state. const error = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze-garbage"), threadId: ThreadId.make("thread-1"), snoozedUntil: "not-a-date", }, readModel: makeReadModel({}), }).pipe(Effect.flip); expect(error._tag).toBe("OrchestrationCommandInvariantError"); }), ); it.effect("rejects snoozing blocked-on-you work", () => Effect.gen(function* () { const requestActivity = { id: EventId.make("activity-req-1"), tone: "approval" as const, kind: "approval.requested", summary: "approval.requested", payload: { requestId: "req-1" }, turnId: null, createdAt: NOW, } as OrchestrationThread["activities"][number]; const error = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze-blocked"), threadId: ThreadId.make("thread-1"), snoozedUntil: FUTURE_WAKE, }, readModel: makeReadModel({ activities: [requestActivity] }), }).pipe(Effect.flip); expect(error._tag).toBe("OrchestrationCommandInvariantError"); }), ); it.effect("re-emits idempotently for a duplicate snooze to the same wake time", () => Effect.gen(function* () { const reEmit = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze-again"), threadId: ThreadId.make("thread-1"), snoozedUntil: FUTURE_WAKE, }, readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }), }); const events = Array.isArray(reEmit) ? reEmit : [reEmit]; expect(events).toHaveLength(1); if (events[0]?.type === "thread.snoozed") { // Original snoozedAt preserved; updatedAt must not churn. expect(events[0].payload.snoozedAt).toBe(SNOOZED_AT); expect(events[0].payload.updatedAt).toBe(NOW); } }), ); it.effect("re-snoozing to a DIFFERENT wake time stamps fresh", () => Effect.gen(function* () { const event = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze-extend"), threadId: ThreadId.make("thread-1"), snoozedUntil: "1970-01-03T09:00:00.000Z", }, readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }), }); const events = Array.isArray(event) ? event : [event]; if (events[0]?.type === "thread.snoozed") { expect(events[0].payload.snoozedUntil).toBe("1970-01-03T09:00:00.000Z"); expect(events[0].payload.updatedAt).not.toBe(NOW); } }), ); it.effect("unsnoozes with reason user and re-emits idempotently when awake", () => Effect.gen(function* () { const event = yield* decideOrchestrationCommand({ command: { type: "thread.unsnooze", commandId: CommandId.make("cmd-unsnooze"), threadId: ThreadId.make("thread-1"), reason: "user", }, readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }), }); const events = Array.isArray(event) ? event : [event]; expect(events[0]?.type).toBe("thread.unsnoozed"); if (events[0]?.type === "thread.unsnoozed") { expect(events[0].payload.reason).toBe("user"); expect(events[0].payload.updatedAt).not.toBe(NOW); } const awake = yield* decideOrchestrationCommand({ command: { type: "thread.unsnooze", commandId: CommandId.make("cmd-unsnooze-awake"), threadId: ThreadId.make("thread-1"), reason: "user", }, readModel: makeReadModel({}), }); const awakeEvents = Array.isArray(awake) ? awake : [awake]; expect(awakeEvents[0]?.type).toBe("thread.unsnoozed"); if (awakeEvents[0]?.type === "thread.unsnoozed") { // No state change — keep the existing updatedAt. expect(awakeEvents[0].payload.updatedAt).toBe(NOW); } }), ); it.effect("rejects snoozing a thread with a queued turn start", () => Effect.gen(function* () { // The decider clock is the Effect test clock pinned to the epoch: a // user message 30s before it with no adopting turn is queued work. const queuedMessage = { id: MessageId.make("message-queued"), role: "user", text: "Continue", turnId: null, streaming: false, createdAt: "1969-12-31T23:59:30.000Z", updatedAt: "1969-12-31T23:59:30.000Z", } as OrchestrationThread["messages"][number]; const error = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze-queued"), threadId: ThreadId.make("thread-1"), snoozedUntil: FUTURE_WAKE, }, readModel: makeReadModel({ messages: [queuedMessage] }), }).pipe(Effect.flip); expect(error._tag).toBe("OrchestrationCommandInvariantError"); }), ); it.effect("rejects snoozing an archived thread", () => Effect.gen(function* () { const error = yield* decideOrchestrationCommand({ command: { type: "thread.snooze", commandId: CommandId.make("cmd-snooze-archived"), threadId: ThreadId.make("thread-1"), snoozedUntil: FUTURE_WAKE, }, readModel: makeReadModel({ archivedAt: NOW }), }).pipe(Effect.flip); expect(error._tag).toBe("OrchestrationCommandInvariantError"); }), ); it.effect("a user message spends the snooze return ticket (activity wake)", () => Effect.gen(function* () { const result = yield* decideOrchestrationCommand({ command: { type: "thread.turn.start", commandId: CommandId.make("cmd-turn-start"), threadId: ThreadId.make("thread-1"), message: { messageId: MessageId.make("message-1"), role: "user", text: "Continue", attachments: [], }, runtimeMode: "full-access", interactionMode: "default", createdAt: NOW, }, readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }), }); const events = Array.isArray(result) ? result : [result]; const unsnoozed = events.find((entry) => entry.type === "thread.unsnoozed"); expect(unsnoozed).toBeDefined(); if (unsnoozed?.type === "thread.unsnoozed") { expect(unsnoozed.payload.reason).toBe("activity"); } }), );});
import {
CommandId,
EventId,
MessageId,
ProjectId,
ProviderInstanceId,
ThreadId,
type OrchestrationReadModel,
type OrchestrationThread,
} from "@t3tools/contracts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import { decideOrchestrationCommand } from "./decider.ts";
const NOW = "2026-01-01T00:00:00.000Z";
// The decider's clock is the Effect test clock, pinned to the epoch, so
// "future" wake times are relative to 1970-01-01T00:00:00.000Z.
const FUTURE_WAKE = "1970-01-02T09:00:00.000Z";
const PAST_WAKE = "1969-12-31T09:00:00.000Z";
const SNOOZED_AT = "1969-12-30T00:00:00.000Z";
function makeReadModel(input: {
readonly snoozedUntil?: string | null;
readonly snoozedAt?: string | null;
readonly archivedAt?: string | null;
readonly activities?: OrchestrationThread["activities"];
readonly messages?: OrchestrationThread["messages"];
}): OrchestrationReadModel {
return {
snapshotSequence: 0,
projects: [],
threads: [
{
id: ThreadId.make("thread-1"),
projectId: ProjectId.make("project-1"),
title: "Thread",
modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
latestTurn: null,
createdAt: NOW,
updatedAt: NOW,
archivedAt: input.archivedAt ?? null,
settledOverride: null,
settledAt: null,
snoozedUntil: input.snoozedUntil ?? null,
snoozedAt: input.snoozedAt ?? (input.snoozedUntil != null ? SNOOZED_AT : null),
deletedAt: null,
messages: input.messages ?? [],
proposedPlans: [],
activities: input.activities ?? [],
checkpoints: [],
session: null,
},
],
updatedAt: NOW,
};
}
it.layer(NodeServices.layer)("snoozed thread decider", (it) => {
it.effect("snoozes a thread to a future wake time", () =>
Effect.gen(function* () {
const event = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: FUTURE_WAKE,
},
readModel: makeReadModel({}),
});
const events = Array.isArray(event) ? event : [event];
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("thread.snoozed");
if (events[0]?.type === "thread.snoozed") {
expect(events[0].payload.snoozedUntil).toBe(FUTURE_WAKE);
expect(events[0].payload.snoozedAt).toBe(events[0].payload.updatedAt);
}
}),
);
it.effect("rejects a wake time that is not in the future", () =>
Effect.gen(function* () {
const error = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze-past"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: PAST_WAKE,
},
readModel: makeReadModel({}),
}).pipe(Effect.flip);
expect(error._tag).toBe("OrchestrationCommandInvariantError");
}),
);
it.effect("rejects an unparseable wake time", () =>
Effect.gen(function* () {
// IsoDateTime is structurally a string, so garbage can reach the
// decider; a NaN wake time must never persist as snooze state.
const error = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze-garbage"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: "not-a-date",
},
readModel: makeReadModel({}),
}).pipe(Effect.flip);
expect(error._tag).toBe("OrchestrationCommandInvariantError");
}),
);
it.effect("rejects snoozing blocked-on-you work", () =>
Effect.gen(function* () {
const requestActivity = {
id: EventId.make("activity-req-1"),
tone: "approval" as const,
kind: "approval.requested",
summary: "approval.requested",
payload: { requestId: "req-1" },
turnId: null,
createdAt: NOW,
} as OrchestrationThread["activities"][number];
const error = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze-blocked"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: FUTURE_WAKE,
},
readModel: makeReadModel({ activities: [requestActivity] }),
}).pipe(Effect.flip);
expect(error._tag).toBe("OrchestrationCommandInvariantError");
}),
);
it.effect("re-emits idempotently for a duplicate snooze to the same wake time", () =>
Effect.gen(function* () {
const reEmit = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze-again"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: FUTURE_WAKE,
},
readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }),
});
const events = Array.isArray(reEmit) ? reEmit : [reEmit];
expect(events).toHaveLength(1);
if (events[0]?.type === "thread.snoozed") {
// Original snoozedAt preserved; updatedAt must not churn.
expect(events[0].payload.snoozedAt).toBe(SNOOZED_AT);
expect(events[0].payload.updatedAt).toBe(NOW);
}
}),
);
it.effect("re-snoozing to a DIFFERENT wake time stamps fresh", () =>
Effect.gen(function* () {
const event = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze-extend"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: "1970-01-03T09:00:00.000Z",
},
readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }),
});
const events = Array.isArray(event) ? event : [event];
if (events[0]?.type === "thread.snoozed") {
expect(events[0].payload.snoozedUntil).toBe("1970-01-03T09:00:00.000Z");
expect(events[0].payload.updatedAt).not.toBe(NOW);
}
}),
);
it.effect("unsnoozes with reason user and re-emits idempotently when awake", () =>
Effect.gen(function* () {
const event = yield* decideOrchestrationCommand({
command: {
type: "thread.unsnooze",
commandId: CommandId.make("cmd-unsnooze"),
threadId: ThreadId.make("thread-1"),
reason: "user",
},
readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }),
});
const events = Array.isArray(event) ? event : [event];
expect(events[0]?.type).toBe("thread.unsnoozed");
if (events[0]?.type === "thread.unsnoozed") {
expect(events[0].payload.reason).toBe("user");
expect(events[0].payload.updatedAt).not.toBe(NOW);
}
const awake = yield* decideOrchestrationCommand({
command: {
type: "thread.unsnooze",
commandId: CommandId.make("cmd-unsnooze-awake"),
threadId: ThreadId.make("thread-1"),
reason: "user",
},
readModel: makeReadModel({}),
});
const awakeEvents = Array.isArray(awake) ? awake : [awake];
expect(awakeEvents[0]?.type).toBe("thread.unsnoozed");
if (awakeEvents[0]?.type === "thread.unsnoozed") {
// No state change — keep the existing updatedAt.
expect(awakeEvents[0].payload.updatedAt).toBe(NOW);
}
}),
);
it.effect("rejects snoozing a thread with a queued turn start", () =>
Effect.gen(function* () {
// The decider clock is the Effect test clock pinned to the epoch: a
// user message 30s before it with no adopting turn is queued work.
const queuedMessage = {
id: MessageId.make("message-queued"),
role: "user",
text: "Continue",
turnId: null,
streaming: false,
createdAt: "1969-12-31T23:59:30.000Z",
updatedAt: "1969-12-31T23:59:30.000Z",
} as OrchestrationThread["messages"][number];
const error = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze-queued"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: FUTURE_WAKE,
},
readModel: makeReadModel({ messages: [queuedMessage] }),
}).pipe(Effect.flip);
expect(error._tag).toBe("OrchestrationCommandInvariantError");
}),
);
it.effect("rejects snoozing an archived thread", () =>
Effect.gen(function* () {
const error = yield* decideOrchestrationCommand({
command: {
type: "thread.snooze",
commandId: CommandId.make("cmd-snooze-archived"),
threadId: ThreadId.make("thread-1"),
snoozedUntil: FUTURE_WAKE,
},
readModel: makeReadModel({ archivedAt: NOW }),
}).pipe(Effect.flip);
expect(error._tag).toBe("OrchestrationCommandInvariantError");
}),
);
it.effect("a user message spends the snooze return ticket (activity wake)", () =>
Effect.gen(function* () {
const result = yield* decideOrchestrationCommand({
command: {
type: "thread.turn.start",
commandId: CommandId.make("cmd-turn-start"),
threadId: ThreadId.make("thread-1"),
message: {
messageId: MessageId.make("message-1"),
role: "user",
text: "Continue",
attachments: [],
},
runtimeMode: "full-access",
interactionMode: "default",
createdAt: NOW,
},
readModel: makeReadModel({ snoozedUntil: FUTURE_WAKE }),
});
const events = Array.isArray(result) ? result : [result];
const unsnoozed = events.find((entry) => entry.type === "thread.unsnoozed");
expect(unsnoozed).toBeDefined();
if (unsnoozed?.type === "thread.unsnoozed") {
expect(unsnoozed.payload.reason).toBe("activity");
}
}),
);
});