import { CommandId, EventId, ProjectId, ProviderInstanceId, type ModelSelection, type OrchestrationEvent,} from "@t3tools/contracts";import { expect, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as NodeServices from "@effect/platform-node/NodeServices"; import { decideOrchestrationCommand } from "./decider.ts";import { createEmptyReadModel, projectEvent } from "./projector.ts"; const now = "2026-01-01T00:00:00.000Z";const projectId = ProjectId.make("project-env-mode"); const seedProjectCreated = (sequence: number): OrchestrationEvent => ({ sequence, eventId: EventId.make(`evt-project-env-mode-${sequence}`), aggregateKind: "project", aggregateId: projectId, type: "project.created", occurredAt: now, commandId: CommandId.make(`cmd-project-env-mode-${sequence}`), causationEventId: null, correlationId: CommandId.make(`cmd-project-env-mode-${sequence}`), metadata: {}, payload: { projectId, title: "Env mode", workspaceRoot: "/tmp/env-mode", defaultModelSelection: null, scripts: [], createdAt: now, updatedAt: now, },}); it.layer(NodeServices.layer)("decider project defaults", (it) => { it.effect("only treats metadata updates as explicit model defaults", () => Effect.gen(function* () { const selection: ModelSelection = { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol", options: [{ id: "reasoningEffort", value: "high" }], }; const created = yield* decideOrchestrationCommand({ command: { type: "project.create", commandId: CommandId.make("cmd-project-model-create"), projectId, title: "Model default", workspaceRoot: "/tmp/model-default", defaultModelSelection: selection, createdAt: now, }, readModel: createEmptyReadModel(now), }); const createdEvent = Array.isArray(created) ? created[0] : created; expect(createdEvent.type).toBe("project.created"); expect( (createdEvent.payload as { defaultModelSelection?: unknown }).defaultModelSelection, ).toBeNull(); const withProject = yield* projectEvent(createEmptyReadModel(now), { ...createdEvent, sequence: 1, }); const updated = yield* decideOrchestrationCommand({ command: { type: "project.meta.update", commandId: CommandId.make("cmd-project-model-update"), projectId, defaultModelSelection: selection, }, readModel: withProject, }); const updatedEvent = Array.isArray(updated) ? updated[0] : updated; expect(updatedEvent.type).toBe("project.meta-updated"); expect( (updatedEvent.payload as { defaultModelSelection?: unknown }).defaultModelSelection, ).toEqual(selection); }), ); it.effect("propagates defaultThreadEnvMode through meta.update into the read model", () => Effect.gen(function* () { const readModel = yield* projectEvent(createEmptyReadModel(now), seedProjectCreated(1)); expect(readModel.projects[0]?.defaultThreadEnvMode).toBeNull(); const result = yield* decideOrchestrationCommand({ command: { type: "project.meta.update", commandId: CommandId.make("cmd-project-env-mode-set"), projectId, defaultThreadEnvMode: "worktree", }, readModel, }); const event = Array.isArray(result) ? result[0] : result; expect(event.type).toBe("project.meta-updated"); expect((event.payload as { defaultThreadEnvMode?: unknown }).defaultThreadEnvMode).toBe( "worktree", ); const updated = yield* projectEvent(readModel, { ...event, sequence: 2 }); expect(updated.projects[0]?.defaultThreadEnvMode).toBe("worktree"); }), ); it.effect("omits the field when unset and clears it on explicit null", () => Effect.gen(function* () { const readModel = yield* projectEvent(createEmptyReadModel(now), seedProjectCreated(1)); const unrelated = yield* decideOrchestrationCommand({ command: { type: "project.meta.update", commandId: CommandId.make("cmd-project-env-mode-title"), projectId, title: "Renamed", }, readModel, }); const unrelatedEvent = Array.isArray(unrelated) ? unrelated[0] : unrelated; expect("defaultThreadEnvMode" in (unrelatedEvent.payload as object)).toBe(false); const set = yield* decideOrchestrationCommand({ command: { type: "project.meta.update", commandId: CommandId.make("cmd-project-env-mode-set"), projectId, defaultThreadEnvMode: "worktree", }, readModel, }); const setEvent = Array.isArray(set) ? set[0] : set; const afterSet = yield* projectEvent(readModel, { ...setEvent, sequence: 2 }); const clear = yield* decideOrchestrationCommand({ command: { type: "project.meta.update", commandId: CommandId.make("cmd-project-env-mode-clear"), projectId, defaultThreadEnvMode: null, }, readModel: afterSet, }); const clearEvent = Array.isArray(clear) ? clear[0] : clear; const afterClear = yield* projectEvent(afterSet, { ...clearEvent, sequence: 3 }); expect(afterClear.projects[0]?.defaultThreadEnvMode).toBeNull(); }), ); it.effect("propagates autoPull through meta.update into the read model", () => Effect.gen(function* () { const readModel = yield* projectEvent(createEmptyReadModel(now), seedProjectCreated(1)); expect(readModel.projects[0]?.autoPull).toBe(false); const result = yield* decideOrchestrationCommand({ command: { type: "project.meta.update", commandId: CommandId.make("cmd-project-auto-pull"), projectId, autoPull: true, }, readModel, }); const event = Array.isArray(result) ? result[0] : result; expect((event.payload as { autoPull?: unknown }).autoPull).toBe(true); const updated = yield* projectEvent(readModel, { ...event, sequence: 2 }); expect(updated.projects[0]?.autoPull).toBe(true); }), );});
import {
CommandId,
EventId,
ProjectId,
ProviderInstanceId,
type ModelSelection,
type OrchestrationEvent,
} from "@t3tools/contracts";
import { expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { decideOrchestrationCommand } from "./decider.ts";
import { createEmptyReadModel, projectEvent } from "./projector.ts";
const now = "2026-01-01T00:00:00.000Z";
const projectId = ProjectId.make("project-env-mode");
const seedProjectCreated = (sequence: number): OrchestrationEvent => ({
sequence,
eventId: EventId.make(`evt-project-env-mode-${sequence}`),
aggregateKind: "project",
aggregateId: projectId,
type: "project.created",
occurredAt: now,
commandId: CommandId.make(`cmd-project-env-mode-${sequence}`),
causationEventId: null,
correlationId: CommandId.make(`cmd-project-env-mode-${sequence}`),
metadata: {},
payload: {
projectId,
title: "Env mode",
workspaceRoot: "/tmp/env-mode",
defaultModelSelection: null,
scripts: [],
createdAt: now,
updatedAt: now,
},
});
it.layer(NodeServices.layer)("decider project defaults", (it) => {
it.effect("only treats metadata updates as explicit model defaults", () =>
Effect.gen(function* () {
const selection: ModelSelection = {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.6-sol",
options: [{ id: "reasoningEffort", value: "high" }],
};
const created = yield* decideOrchestrationCommand({
command: {
type: "project.create",
commandId: CommandId.make("cmd-project-model-create"),
projectId,
title: "Model default",
workspaceRoot: "/tmp/model-default",
defaultModelSelection: selection,
createdAt: now,
},
readModel: createEmptyReadModel(now),
});
const createdEvent = Array.isArray(created) ? created[0] : created;
expect(createdEvent.type).toBe("project.created");
expect(
(createdEvent.payload as { defaultModelSelection?: unknown }).defaultModelSelection,
).toBeNull();
const withProject = yield* projectEvent(createEmptyReadModel(now), {
...createdEvent,
sequence: 1,
});
const updated = yield* decideOrchestrationCommand({
command: {
type: "project.meta.update",
commandId: CommandId.make("cmd-project-model-update"),
projectId,
defaultModelSelection: selection,
},
readModel: withProject,
});
const updatedEvent = Array.isArray(updated) ? updated[0] : updated;
expect(updatedEvent.type).toBe("project.meta-updated");
expect(
(updatedEvent.payload as { defaultModelSelection?: unknown }).defaultModelSelection,
).toEqual(selection);
}),
);
it.effect("propagates defaultThreadEnvMode through meta.update into the read model", () =>
Effect.gen(function* () {
const readModel = yield* projectEvent(createEmptyReadModel(now), seedProjectCreated(1));
expect(readModel.projects[0]?.defaultThreadEnvMode).toBeNull();
const result = yield* decideOrchestrationCommand({
command: {
type: "project.meta.update",
commandId: CommandId.make("cmd-project-env-mode-set"),
projectId,
defaultThreadEnvMode: "worktree",
},
readModel,
});
const event = Array.isArray(result) ? result[0] : result;
expect(event.type).toBe("project.meta-updated");
expect((event.payload as { defaultThreadEnvMode?: unknown }).defaultThreadEnvMode).toBe(
"worktree",
);
const updated = yield* projectEvent(readModel, { ...event, sequence: 2 });
expect(updated.projects[0]?.defaultThreadEnvMode).toBe("worktree");
}),
);
it.effect("omits the field when unset and clears it on explicit null", () =>
Effect.gen(function* () {
const readModel = yield* projectEvent(createEmptyReadModel(now), seedProjectCreated(1));
const unrelated = yield* decideOrchestrationCommand({
command: {
type: "project.meta.update",
commandId: CommandId.make("cmd-project-env-mode-title"),
projectId,
title: "Renamed",
},
readModel,
});
const unrelatedEvent = Array.isArray(unrelated) ? unrelated[0] : unrelated;
expect("defaultThreadEnvMode" in (unrelatedEvent.payload as object)).toBe(false);
const set = yield* decideOrchestrationCommand({
command: {
type: "project.meta.update",
commandId: CommandId.make("cmd-project-env-mode-set"),
projectId,
defaultThreadEnvMode: "worktree",
},
readModel,
});
const setEvent = Array.isArray(set) ? set[0] : set;
const afterSet = yield* projectEvent(readModel, { ...setEvent, sequence: 2 });
const clear = yield* decideOrchestrationCommand({
command: {
type: "project.meta.update",
commandId: CommandId.make("cmd-project-env-mode-clear"),
projectId,
defaultThreadEnvMode: null,
},
readModel: afterSet,
});
const clearEvent = Array.isArray(clear) ? clear[0] : clear;
const afterClear = yield* projectEvent(afterSet, { ...clearEvent, sequence: 3 });
expect(afterClear.projects[0]?.defaultThreadEnvMode).toBeNull();
}),
);
it.effect("propagates autoPull through meta.update into the read model", () =>
Effect.gen(function* () {
const readModel = yield* projectEvent(createEmptyReadModel(now), seedProjectCreated(1));
expect(readModel.projects[0]?.autoPull).toBe(false);
const result = yield* decideOrchestrationCommand({
command: {
type: "project.meta.update",
commandId: CommandId.make("cmd-project-auto-pull"),
projectId,
autoPull: true,
},
readModel,
});
const event = Array.isArray(result) ? result[0] : result;
expect((event.payload as { autoPull?: unknown }).autoPull).toBe(true);
const updated = yield* projectEvent(readModel, { ...event, sequence: 2 });
expect(updated.projects[0]?.autoPull).toBe(true);
}),
);
});