Skip to content
Source
Sign inSign up

firstRun.logic.test.ts

243 lines7.1 KB

1import { describe, expect, it } from "vite-plus/test";
2​
3import {
4 isFirstRunWorkspaceProvenanceAuthoritative,
5 isFreshFirstRunWorkspace,
6 resolveFirstRunDecision,
7 transitionFirstRunGateState,
8} from "./firstRun.logic";
9​
10const freshWorkspace = {
11 enabled: true,
12 hydrated: true,
13 completed: false,
14 bootstrapped: true,
15 authoritative: true,
16 workspaceAuthoritative: true,
17 workspaceProvenanceAuthoritative: true,
18 catalogReady: true,
19 serverConfigAvailable: true,
20 workspaceFresh: true,
21 projectCount: 1,
22 threadCount: 1,
23} as const;
24​
25describe("resolveFirstRunDecision", () => {
26 it("opens the wizard for an authoritative fresh workspace", () => {
27 expect(resolveFirstRunDecision(freshWorkspace)).toEqual({
28 decision: "wizard",
29 persistCompletion: false,
30 });
31 });
32​
33 it("does not permanently complete onboarding from cached project counts", () => {
34 expect(
35 resolveFirstRunDecision({
36 ...freshWorkspace,
37 authoritative: false,
38 projectCount: 3,
39 workspaceFresh: false,
40 }),
41 ).toEqual({
42 decision: "app",
43 persistCompletion: false,
44 });
45 });
46​
47 it("backfills completion once existing projects are confirmed by the server", () => {
48 expect(
49 resolveFirstRunDecision({
50 ...freshWorkspace,
51 projectCount: 3,
52 workspaceFresh: false,
53 }),
54 ).toEqual({
55 decision: "app",
56 persistCompletion: true,
57 });
58 });
59​
60 it("waits when the initial welcome is pending and opens the wizard after completion", () => {
61 const pendingProvenance = isFirstRunWorkspaceProvenanceAuthoritative({
62 welcomeReceived: true,
63 bootstrapStatus: "pending",
64 });
65 expect(
66 resolveFirstRunDecision({
67 ...freshWorkspace,
68 workspaceProvenanceAuthoritative: pendingProvenance,
69 }),
70 ).toEqual({ decision: "pending", persistCompletion: false });
71​
72 const completedProvenance = isFirstRunWorkspaceProvenanceAuthoritative({
73 welcomeReceived: true,
74 bootstrapStatus: "complete",
75 });
76 expect(
77 resolveFirstRunDecision({
78 ...freshWorkspace,
79 workspaceProvenanceAuthoritative: completedProvenance,
80 }),
81 ).toEqual({ decision: "wizard", persistCompletion: false });
82 });
83​
84 it("does not wait for server data after onboarding is already complete", () => {
85 expect(
86 resolveFirstRunDecision({
87 ...freshWorkspace,
88 authoritative: false,
89 bootstrapped: false,
90 completed: true,
91 serverConfigAvailable: false,
92 }),
93 ).toEqual({
94 decision: "app",
95 persistCompletion: false,
96 });
97 });
98});
99​
100describe("isFirstRunWorkspaceProvenanceAuthoritative", () => {
101 it("accepts a legacy welcome without bootstrap status", () => {
102 expect(
103 isFirstRunWorkspaceProvenanceAuthoritative({
104 welcomeReceived: true,
105 bootstrapStatus: null,
106 }),
107 ).toBe(true);
108 });
109});
110​
111describe("transitionFirstRunGateState", () => {
112 it("shows recovery without mounting the app when evidence stalls", () => {
113 expect(
114 transitionFirstRunGateState({ decision: "pending", stalled: false }, { type: "timeout" }),
115 ).toEqual({ decision: "pending", stalled: true });
116 });
117​
118 it.each([
119 { previous: "app", workspaceFresh: true, decision: "wizard" },
120 { previous: "wizard", workspaceFresh: false, decision: "app" },
121 ] as const)(
122 "updates $previous to $decision when authoritative workspace evidence changes",
123 ({ previous, workspaceFresh, decision }) => {
124 const evidence = resolveFirstRunDecision({ ...freshWorkspace, workspaceFresh });
125 expect(
126 transitionFirstRunGateState(
127 { decision: previous, stalled: false },
128 { type: "evidence", decision: evidence.decision },
129 ),
130 ).toEqual({ decision, stalled: false });
131 },
132 );
133​
134 // regression: #19680 unchanged gate evidence must not produce a new state
135 it.each(["pending", "app", "wizard"] as const)(
136 "preserves state identity for unchanged %s evidence",
137 (decision) => {
138 const state = { decision, stalled: false };
139 expect(transitionFirstRunGateState(state, { type: "evidence", decision })).toBe(state);
140 },
141 );
142});
143​
144const primaryEnvironmentId = "primary-environment";
145const bootstrapProject = {
146 id: "bootstrap-project",
147 environmentId: primaryEnvironmentId,
148 workspaceRoot: "/projects/current",
149};
150const bootstrapThread = {
151 id: "bootstrap-thread",
152 projectId: bootstrapProject.id,
153 environmentId: primaryEnvironmentId,
154 latestRun: null,
155 latestUserMessageAt: null,
156 runtime: null,
157};
158​
159const freshBootstrap = {
160 primaryEnvironmentId,
161 serverCwd: "/projects/current",
162 bootstrapProjectId: bootstrapProject.id,
163 bootstrapThreadId: bootstrapThread.id,
164 bootstrapProjectCreated: true,
165 bootstrapThreadCreated: true,
166 projects: [bootstrapProject],
167 threads: [bootstrapThread],
168};
169​
170describe("isFreshFirstRunWorkspace", () => {
171 it("accepts only the unused project and thread created from the server cwd", () => {
172 expect(
173 isFreshFirstRunWorkspace({
174 primaryEnvironmentId,
175 serverCwd: "/projects/current/",
176 bootstrapProjectId: bootstrapProject.id,
177 bootstrapThreadId: bootstrapThread.id,
178 bootstrapProjectCreated: true,
179 bootstrapThreadCreated: true,
180 projects: [bootstrapProject],
181 threads: [bootstrapThread],
182 }),
183 ).toBe(true);
184 });
185​
186 it("rejects an existing unused cwd project and thread reused by startup", () => {
187 expect(
188 isFreshFirstRunWorkspace({
189 primaryEnvironmentId,
190 serverCwd: "/projects/current",
191 bootstrapProjectId: bootstrapProject.id,
192 bootstrapThreadId: bootstrapThread.id,
193 bootstrapProjectCreated: false,
194 bootstrapThreadCreated: false,
195 projects: [bootstrapProject],
196 threads: [bootstrapThread],
197 }),
198 ).toBe(false);
199 });
200​
201 it("rejects a nonempty workspace when an older server omits creation provenance", () => {
202 expect(
203 isFreshFirstRunWorkspace({
204 primaryEnvironmentId,
205 serverCwd: "/projects/current",
206 bootstrapProjectId: bootstrapProject.id,
207 bootstrapThreadId: bootstrapThread.id,
208 projects: [bootstrapProject],
209 threads: [bootstrapThread],
210 }),
211 ).toBe(false);
212 });
213​
214 it("normalizes Windows project paths before checking the bootstrap workspace", () => {
215 expect(
216 isFreshFirstRunWorkspace({
217 primaryEnvironmentId,
218 serverCwd: "C:\\Projects\\Current\\",
219 bootstrapProjectId: bootstrapProject.id,
220 bootstrapThreadId: bootstrapThread.id,
221 bootstrapProjectCreated: true,
222 bootstrapThreadCreated: true,
223 projects: [{ ...bootstrapProject, workspaceRoot: "c:/projects/current" }],
224 threads: [bootstrapThread],
225 }),
226 ).toBe(true);
227 });
228​
229 it("rejects a bootstrap thread that already has a user message", () => {
230 expect(
231 isFreshFirstRunWorkspace({
232 ...freshBootstrap,
233 projects: [bootstrapProject],
234 threads: [
235 {
236 ...bootstrapThread,
237 latestUserMessageAt: "2026-08-23T12:00:00.000Z",
238 },
239 ],
240 }),
241 ).toBe(false);
242 });
243});