Skip to content
Source
Sign inSign up

ProjectCommands.test.ts

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