Skip to content
Source
Sign inSign up

ProjectCommands.test.ts

176 lines5.6 KB

1import { assert, describe, it } from "@effect/vitest";
2import { CommandId, EventId, ProjectId, type ProjectScript } from "@t3tools/contracts";
3import * as DateTime from "effect/DateTime";
4import * as Result from "effect/Result";
5​
6import {
7 planProjectCommand,
8 type ProjectCommand,
9 type ProjectCommandState,
10} from "./ProjectCommands.ts";
11import type { ProjectRow } from "./ProjectStore.ts";
12​
13const now = DateTime.makeUnsafe("2026-01-01T00:00:00.000Z");
14const projectId = ProjectId.make("project-scripts");
15​
16const row = (overrides: Partial<ProjectRow> = {}): ProjectRow => ({
17 projectId,
18 title: "Scripts",
19 workspaceRoot: "/tmp/scripts",
20 defaultModelSelection: null,
21 defaultThreadEnvMode: null,
22 autoPull: false,
23 faviconPath: null,
24 projectIcon: null,
25 scripts: [],
26 pinnedAt: null,
27 archivedAt: null,
28 createdAt: "2026-01-01T00:00:00.000Z",
29 updatedAt: "2026-01-01T00:00:00.000Z",
30 deletedAt: null,
31 ...overrides,
32});
33​
34const script = (id: string): ProjectScript => ({
35 id,
36 name: "Install dependencies",
37 command: "vp i",
38 icon: "configure",
39 runOnWorktreeCreate: false,
40});
41​
42const plan = (command: ProjectCommand, state: Partial<ProjectCommandState> = {}) =>
43 planProjectCommand({
44 command,
45 state: { project: undefined, workspaceOwner: undefined, ...state },
46 eventId: EventId.make("event:planned"),
47 now,
48 });
49​
50const payloadOf = (result: ReturnType<typeof plan>) => {
51 assert.isTrue(Result.isSuccess(result));
52 return Result.getOrThrow(result).payload as Record<string, unknown>;
53};
54​
55const failureOf = (result: ReturnType<typeof plan>) => {
56 assert.isTrue(Result.isFailure(result));
57 return Result.isFailure(result) ? result.failure : assert.fail("expected a rejection");
58};
59​
60describe("planProjectCommand", () => {
61 it("keeps legacy scripts editable and removable while rejecting new invalid ones", () => {
62 const legacy = script("install-javascript-dependencies");
63 const withLegacy = (scripts: ReadonlyArray<ProjectScript>) =>
64 plan(
65 {
66 type: "project.meta.update",
67 commandId: CommandId.make("cmd-repair-script"),
68 projectId,
69 scripts,
70 },
71 { project: row({ scripts: [legacy] }) },
72 );
73 for (const scripts of [[{ ...legacy, command: "vp install" }, script("lint")], []]) {
74 assert.deepEqual(payloadOf(withLegacy(scripts)).scripts, scripts);
75 }
76 assert.equal(
77 failureOf(withLegacy([legacy, script("another.invalid.id")]))._tag,
78 "ProjectCommandInvariantError",
79 );
80 });
81​
82 it("keeps the first pin and archive time, and clears both on undo", () => {
83 const earlier = "2025-12-01T00:00:00.000Z";
84 const update = (project: ProjectRow, change: { pinned?: boolean; archived?: boolean }) =>
85 payloadOf(
86 plan(
87 {
88 type: "project.meta.update",
89 commandId: CommandId.make("cmd-organize"),
90 projectId,
91 ...change,
92 },
93 { project },
94 ),
95 );
96 assert.deepInclude(update(row(), { pinned: true, archived: true }), {
97 pinnedAt: "2026-01-01T00:00:00.000Z",
98 archivedAt: "2026-01-01T00:00:00.000Z",
99 });
100 const organized = row({ pinnedAt: earlier, archivedAt: earlier });
101 assert.deepInclude(update(organized, { pinned: true, archived: true }), {
102 pinnedAt: earlier,
103 archivedAt: earlier,
104 });
105 assert.deepInclude(update(organized, { pinned: false, archived: false }), {
106 pinnedAt: null,
107 archivedAt: null,
108 });
109 const untouched = update(organized, {});
110 assert.notProperty(untouched, "pinnedAt");
111 assert.notProperty(untouched, "archivedAt");
112 });
113​
114 it("rejects a workspace root held by another active project", () => {
115 const owner = row({
116 projectId: ProjectId.make("project-existing"),
117 workspaceRoot: "/tmp/project",
118 });
119 const create = failureOf(
120 plan(
121 {
122 type: "project.create",
123 commandId: CommandId.make("cmd-duplicate-root"),
124 projectId: ProjectId.make("project-duplicate-root"),
125 title: "Duplicate",
126 workspaceRoot: "/tmp/project",
127 },
128 { workspaceOwner: owner },
129 ),
130 );
131 assert.equal(create._tag, "ProjectWorkspaceConflictError");
132 assert.equal(
133 create.message,
134 "Active project 'project-existing' already exists for workspace root '/tmp/project'.",
135 );
136 const move = failureOf(
137 plan(
138 {
139 type: "project.meta.update",
140 commandId: CommandId.make("cmd-move-root"),
141 projectId,
142 workspaceRoot: "/tmp/project",
143 },
144 { project: row(), workspaceOwner: owner },
145 ),
146 );
147 assert.equal(move._tag, "ProjectWorkspaceConflictError");
148 });
149​
150 it("requires the project to exist, and to be absent on create", () => {
151 const create: ProjectCommand = {
152 type: "project.create",
153 commandId: CommandId.make("cmd-create-twice"),
154 projectId,
155 title: "Twice",
156 workspaceRoot: "/tmp/twice",
157 };
158 assert.include(failureOf(plan(create, { project: row() })).message, "cannot be created twice");
159 const deleted = row({ deletedAt: "2026-01-01T00:00:00.000Z" });
160 assert.include(
161 failureOf(plan(create, { project: deleted })).message,
162 "cannot be created twice",
163 );
164 for (const command of [
165 { type: "project.meta.update", commandId: CommandId.make("cmd-missing"), projectId },
166 { type: "project.delete", commandId: CommandId.make("cmd-missing"), projectId },
167 ] as const) {
168 for (const project of [undefined, deleted]) {
169 assert.equal(
170 failureOf(plan(command, { project }))._tag,
171 "ProjectCommandMissingProjectError",
172 );
173 }
174 }
175 });
176});