1import { MAX_SCRIPT_ID_LENGTH } from "@t3tools/contracts";
2import { shortcutLabelForCommand } from "./keybindings";
3import { describe, expect, it } from "vite-plus/test";
4import {
5 projectScriptCwd,
6 projectScriptRuntimeEnv,
7 projectScriptMenuLabel,
8 settleProjectScript,
9 setupProjectScript,
10} from "@t3tools/shared/projectScripts";
11
12import {
13 buildProjectScript,
14 commandForProjectScript,
15 nextProjectScriptId,
16 primaryProjectScript,
17 projectScriptIdFromCommand,
18 releaseClaimedRoles,
19} from "./projectScripts";
20
21describe("projectScripts helpers", () => {
22 it("builds scripts with preview settings", () => {
23 expect(
24 buildProjectScript("dev", {
25 name: "Dev server",
26 command: "pnpm dev",
27 icon: "debug",
28 runOnWorktreeCreate: false,
29 waitForSetup: false,
30 runOnSettle: false,
31 previewUrl: "http://localhost:5733",
32 autoOpenPreview: true,
33 }),
34 ).toEqual({
35 id: "dev",
36 name: "Dev server",
37 command: "pnpm dev",
38 icon: "debug",
39 runOnWorktreeCreate: false,
40 previewUrl: "http://localhost:5733",
41 autoOpenPreview: true,
42 });
43 });
44
45 it("omits preview settings when no preview URL is configured", () => {
46 expect(
47 buildProjectScript("test", {
48 name: "Test",
49 command: "pnpm test",
50 icon: "test",
51 runOnWorktreeCreate: false,
52 waitForSetup: false,
53 runOnSettle: false,
54 previewUrl: null,
55 autoOpenPreview: false,
56 }),
57 ).toEqual({
58 id: "test",
59 name: "Test",
60 command: "pnpm test",
61 icon: "test",
62 runOnWorktreeCreate: false,
63 });
64 });
65
66 it("only records async: false for setup scripts that should block the agent", () => {
67 const input = {
68 name: "Setup",
69 command: "pnpm i",
70 icon: "configure",
71 previewUrl: null,
72 autoOpenPreview: false,
73 runOnSettle: false,
74 } as const;
75 expect(
76 buildProjectScript("setup", { ...input, runOnWorktreeCreate: true, waitForSetup: true }),
77 ).toMatchObject({ runOnWorktreeCreate: true, async: false });
78 expect(
79 buildProjectScript("setup", { ...input, runOnWorktreeCreate: true, waitForSetup: false }),
80 ).not.toHaveProperty("async");
81 expect(
82 buildProjectScript("setup", { ...input, runOnWorktreeCreate: false, waitForSetup: true }),
83 ).not.toHaveProperty("async");
84 });
85
86 it("builds and parses script run commands", () => {
87 const command = commandForProjectScript("lint");
88 expect(command).toBe("script.lint.run");
89 expect(projectScriptIdFromCommand(command ?? "")).toBe("lint");
90 expect(projectScriptIdFromCommand("terminal.toggle")).toBeNull();
91 });
92
93 it.each(["install-javascript-dependencies", "A", "a.b", "a b", "-a", "", "a".repeat(25)])(
94 "omits the shortcut for legacy script ID %j without crashing script menus",
95 (id) => {
96 const commands = ["lint", id, "test"].map(commandForProjectScript);
97 expect(commands).toEqual(["script.lint.run", null, "script.test.run"]);
98 expect(commands.map((command) => shortcutLabelForCommand([], command))).toEqual([
99 null,
100 null,
101 null,
102 ]);
103 },
104 );
105
106 it("preserves the exact ID at the shortcut length limit", () => {
107 const id = "a".repeat(MAX_SCRIPT_ID_LENGTH);
108 expect(projectScriptIdFromCommand(commandForProjectScript(id) ?? "")).toBe(id);
109 });
110
111 it("slugifies and dedupes project script ids", () => {
112 expect(nextProjectScriptId("Run Tests", [])).toBe("run-tests");
113 expect(nextProjectScriptId("Run Tests", ["run-tests"])).toBe("run-tests-2");
114 expect(nextProjectScriptId("!!!", [])).toBe("script");
115 });
116
117 it("resolves primary, setup, and settle scripts", () => {
118 const scripts = [
119 {
120 id: "setup",
121 name: "Setup",
122 command: "bun install",
123 icon: "configure" as const,
124 runOnWorktreeCreate: true,
125 },
126 {
127 id: "clean",
128 name: "Clean",
129 command: "cargo clean",
130 icon: "build" as const,
131 runOnWorktreeCreate: false,
132 runOnSettle: true,
133 },
134 {
135 id: "test",
136 name: "Test",
137 command: "bun test",
138 icon: "test" as const,
139 runOnWorktreeCreate: false,
140 },
141 ];
142
143 expect(primaryProjectScript(scripts)?.id).toBe("test");
144 expect(setupProjectScript(scripts)?.id).toBe("setup");
145 expect(settleProjectScript(scripts)?.id).toBe("clean");
146 // Cleanup is never the one-click run button, even when it is all there is.
147 expect(primaryProjectScript(scripts.filter((script) => script.id === "clean"))).toBeNull();
148 });
149
150 it("labels every lifecycle role a script runs in", () => {
151 const script = {
152 id: "both",
153 name: "Both",
154 command: "true",
155 icon: "play" as const,
156 runOnWorktreeCreate: true,
157 };
158 expect(projectScriptMenuLabel(script)).toBe("Both (setup)");
159 expect(projectScriptMenuLabel({ ...script, runOnSettle: true })).toBe(
160 "Both (setup, on settle)",
161 );
162 expect(projectScriptMenuLabel({ ...script, runOnWorktreeCreate: false })).toBe("Both");
163 });
164
165 it("moves the setup and settle roles to the script that claims them", () => {
166 const input = {
167 name: "Next",
168 command: "true",
169 icon: "play",
170 waitForSetup: false,
171 previewUrl: null,
172 autoOpenPreview: false,
173 } as const;
174 const holder = {
175 id: "holder",
176 name: "Holder",
177 command: "true",
178 icon: "play" as const,
179 runOnWorktreeCreate: true,
180 runOnSettle: true,
181 };
182
183 expect(
184 releaseClaimedRoles(holder, { ...input, runOnWorktreeCreate: false, runOnSettle: true }),
185 ).toMatchObject({ runOnWorktreeCreate: true, runOnSettle: false });
186 expect(
187 releaseClaimedRoles(holder, { ...input, runOnWorktreeCreate: true, runOnSettle: false }),
188 ).toMatchObject({ runOnWorktreeCreate: false, runOnSettle: true });
189 expect(
190 releaseClaimedRoles(holder, { ...input, runOnWorktreeCreate: false, runOnSettle: false }),
191 ).toBe(holder);
192 });
193
194 it("builds default runtime env for scripts", () => {
195 const env = projectScriptRuntimeEnv({
196 project: { cwd: "/repo" },
197 worktreePath: "/repo/worktree-a",
198 });
199
200 expect(env).toMatchObject({
201 T3CODE_PROJECT_ROOT: "/repo",
202 T3CODE_WORKTREE_PATH: "/repo/worktree-a",
203 });
204 });
205
206 it("allows overriding runtime env values", () => {
207 const env = projectScriptRuntimeEnv({
208 project: { cwd: "/repo" },
209 extraEnv: {
210 T3CODE_PROJECT_ROOT: "/custom-root",
211 CUSTOM_FLAG: "1",
212 },
213 });
214
215 expect(env.T3CODE_PROJECT_ROOT).toBe("/custom-root");
216 expect(env.CUSTOM_FLAG).toBe("1");
217 expect(env.T3CODE_WORKTREE_PATH).toBeUndefined();
218 });
219
220 it("prefers the worktree path for script cwd resolution", () => {
221 expect(
222 projectScriptCwd({
223 project: { cwd: "/repo" },
224 worktreePath: "/repo/worktree-a",
225 }),
226 ).toBe("/repo/worktree-a");
227 expect(
228 projectScriptCwd({
229 project: { cwd: "/repo" },
230 worktreePath: null,
231 }),
232 ).toBe("/repo");
233 });
234});