Skip to content
Source
Sign inSign up

ProjectStore.test.ts

164 lines6.0 KB

1import { assert, it } from "@effect/vitest";
2import { EventId, ProjectId, ProviderInstanceId } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as SqlClient from "effect/sql/SqlClient";
7​
8import * as SqlitePersistence from "../persistence/Sqlite.ts";
9import * as ProjectStore from "./ProjectStore.ts";
10​
11it.layer(ProjectStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)))(
12 "ProjectStoreV2",
13 (it) => {
14 it.effect("stores a model selection without options as JSON without an options key", () =>
15 Effect.gen(function* () {
16 const projects = yield* ProjectStore.ProjectStoreV2;
17 const sql = yield* SqlClient.SqlClient;
18 const projectId = ProjectId.make("project-null-options");
19 const modelSelection = { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" };
20 yield* projects.apply({
21 sequence: 1,
22 eventId: EventId.make("event-null-options"),
23 aggregateKind: "project",
24 aggregateId: projectId,
25 occurredAt: "2026-03-24T00:00:00.000Z",
26 commandId: null,
27 causationEventId: null,
28 correlationId: null,
29 metadata: {},
30 type: "project.created",
31 payload: {
32 projectId,
33 title: "Null options project",
34 workspaceRoot: "/tmp/project-null-options",
35 defaultModelSelection: modelSelection,
36 scripts: [],
37 createdAt: "2026-03-24T00:00:00.000Z",
38 updatedAt: "2026-03-24T00:00:00.000Z",
39 },
40 });
41​
42 const rows = yield* sql<{ readonly defaultModelSelection: string | null }>`
43 SELECT default_model_selection_json AS "defaultModelSelection"
44 FROM projection_projects
45 WHERE project_id = ${projectId}
46 `;
47 assert.strictEqual(rows[0]?.defaultModelSelection, JSON.stringify(modelSelection));
48 assert.deepStrictEqual(
49 Option.getOrNull(yield* projects.get(projectId))?.defaultModelSelection,
50 modelSelection,
51 );
52 }),
53 );
54​
55 it.effect("keeps pin and archive times across later updates", () =>
56 Effect.gen(function* () {
57 const projects = yield* ProjectStore.ProjectStoreV2;
58 const projectId = ProjectId.make("project-organized");
59 const at = "2026-03-24T00:00:00.000Z";
60 const event = { aggregateKind: "project", aggregateId: projectId, occurredAt: at } as const;
61 const meta = { commandId: null, causationEventId: null, correlationId: null, metadata: {} };
62 yield* projects.apply({
63 ...event,
64 ...meta,
65 sequence: 1,
66 eventId: EventId.make("event-organized-created"),
67 type: "project.created",
68 payload: {
69 projectId,
70 title: "Organized",
71 workspaceRoot: "/tmp/project-organized",
72 defaultModelSelection: null,
73 scripts: [],
74 createdAt: at,
75 updatedAt: at,
76 },
77 });
78 yield* projects.apply({
79 ...event,
80 ...meta,
81 sequence: 2,
82 eventId: EventId.make("event-organized-pinned"),
83 type: "project.meta-updated",
84 payload: { projectId, pinnedAt: at, archivedAt: at, updatedAt: at },
85 });
86 yield* projects.apply({
87 ...event,
88 ...meta,
89 sequence: 3,
90 eventId: EventId.make("event-organized-renamed"),
91 type: "project.meta-updated",
92 payload: { projectId, title: "Renamed", updatedAt: at },
93 });
94 const project = Option.getOrNull(yield* projects.get(projectId));
95 assert.strictEqual(project?.pinnedAt, at);
96 assert.strictEqual(project?.archivedAt, at);
97 }),
98 );
99​
100 it.effect("marks a project's title as custom only once it is renamed", () =>
101 Effect.gen(function* () {
102 const projects = yield* ProjectStore.ProjectStoreV2;
103 const projectId = ProjectId.make("project-renamed");
104 const event = {
105 aggregateKind: "project",
106 aggregateId: projectId,
107 occurredAt: "2026-03-24T00:00:00.000Z",
108 commandId: null,
109 causationEventId: null,
110 correlationId: null,
111 metadata: {},
112 } as const;
113 const hasCustomTitle = () =>
114 projects
115 .getShell(projectId)
116 .pipe(Effect.map((shell) => Option.getOrNull(shell)?.hasCustomTitle));
117 yield* projects.apply({
118 ...event,
119 sequence: 1,
120 eventId: EventId.make("event-renamed-created"),
121 type: "project.created",
122 payload: {
123 projectId,
124 title: "repo",
125 workspaceRoot: "/tmp/repo",
126 defaultModelSelection: null,
127 scripts: [],
128 createdAt: "2026-03-24T00:00:00.000Z",
129 updatedAt: "2026-03-24T00:00:00.000Z",
130 },
131 });
132 assert.strictEqual(yield* hasCustomTitle(), false);
133 yield* projects.apply({
134 ...event,
135 sequence: 2,
136 eventId: EventId.make("event-renamed-title"),
137 type: "project.meta-updated",
138 payload: { projectId, title: "custom repo", updatedAt: "2026-03-24T00:00:02.000Z" },
139 });
140 assert.strictEqual(yield* hasCustomTitle(), true);
141 yield* projects.apply({
142 ...event,
143 sequence: 3,
144 eventId: EventId.make("migration:39:project:project-renamed:baseline"),
145 type: "project.created",
146 payload: {
147 projectId,
148 title: "repo",
149 workspaceRoot: "/tmp/repo",
150 defaultModelSelection: null,
151 scripts: [],
152 createdAt: "2026-03-24T00:00:00.000Z",
153 updatedAt: "2026-03-24T00:00:03.000Z",
154 },
155 });
156 assert.strictEqual(yield* hasCustomTitle(), true);
157 assert.strictEqual(
158 Option.getOrNull(yield* projects.getShell(projectId))?.title,
159 "custom repo",
160 );
161 }),
162 );
163 },
164);