Skip to content
File

Blob: tests/worker/dispatch/shared/run-environment/commit-recording.test.ts

typescript224 lines
1import { describe, expect, it } from "vitest";
2 
3import { CommitSha } from "@/contracts";
4import { prepareExecutionEnvironment } from "@/worker/dispatch/shared/run-environment";
5import { RunOwnershipLostError } from "@/worker/dispatch/shared/run-lease";
6 
7import { createExecResult, createHarness, createLeaseStub } from "../../../../helpers/run-environment";
8 
9const expectBranchCheckoutInitialized = (events: string[]): void => {
10 expect(events).toContain("init");
11 expect(events.indexOf("remote-add")).toBeGreaterThan(events.indexOf("init"));
12 expect(events.indexOf("fetch-depth-1")).toBeGreaterThan(events.indexOf("remote-add"));
13};
14 
15describe("run environment preparation", () => {
16 describe("resolved commit recording", () => {
17 it("records the resolved commit before config loading failures", async () => {
18 const commitSha = CommitSha.assertDecode("0123456789abcdef0123456789abcdef01234567");
19 const { context, events, projectId, recordRunResolvedCommit, replaceSteps, runId, sandbox } = createHarness({
20 projectIdValue: "prj_0000000000000000000000",
21 runIdValue: "run_0000000000000000000000",
22 snapshotCommitSha: null,
23 execImpl: (command) => {
24 if (command.startsWith("git init ")) {
25 events.push("init");
26 return createExecResult();
27 }
28 
29 if (command.includes("remote add origin")) {
30 events.push("remote-add");
31 return createExecResult();
32 }
33 
34 if (command.includes("fetch --depth=1 --update-shallow origin 'main'")) {
35 events.push("fetch-depth-1");
36 return createExecResult();
37 }
38 
39 if (command.includes("checkout -B 'main' FETCH_HEAD")) {
40 events.push("checkout-branch");
41 return createExecResult();
42 }
43 
44 if (command.includes("branch --set-upstream-to='origin/main' 'main'")) {
45 events.push("set-upstream");
46 return createExecResult();
47 }
48 
49 if (command.includes("rev-parse HEAD")) {
50 events.push("rev-parse");
51 return createExecResult({
52 stdout: `${commitSha}\n`,
53 });
54 }
55 
56 throw new Error(`Unexpected exec command: ${command}`);
57 },
58 });
59 
60 await expect(prepareExecutionEnvironment(context, createLeaseStub())).rejects.toThrow(
61 "Repository config is not valid YAML",
62 );
63 
64 expect(recordRunResolvedCommit).toHaveBeenCalledWith({
65 projectId,
66 runId,
67 commitSha,
68 });
69 expect(replaceSteps).not.toHaveBeenCalled();
70 expectBranchCheckoutInitialized(events);
71 expect(events.indexOf("record-run-resolved-commit")).toBeGreaterThanOrEqual(0);
72 expect(events.indexOf("read-file")).toBeGreaterThanOrEqual(0);
73 expect(events.indexOf("record-run-resolved-commit")).toBeLessThan(events.indexOf("read-file"));
74 expect(events.indexOf("checkout-branch")).toBeGreaterThan(events.indexOf("fetch-depth-1"));
75 expect(events.indexOf("set-upstream")).toBeGreaterThan(events.indexOf("checkout-branch"));
76 expect(sandbox.deleteSession).toHaveBeenCalledWith("checkout-session");
77 expect(context.state.session).toBeNull();
78 });
79 
80 it("stops checkout when resolved-commit backfill reports stale ownership", async () => {
81 const commitSha = CommitSha.assertDecode("1111111111111111111111111111111111111111");
82 const { context, events, projectId, recordRunResolvedCommit, runId, sandbox } = createHarness({
83 projectIdValue: "prj_9999999999999999999990",
84 runIdValue: "run_9999999999999999999990",
85 snapshotCommitSha: null,
86 recordRunResolvedCommitResult: {
87 kind: "stale",
88 status: "failed",
89 },
90 execImpl: (command) => {
91 if (command.startsWith("git init ")) {
92 events.push("init");
93 return createExecResult();
94 }
95 
96 if (command.includes("remote add origin")) {
97 events.push("remote-add");
98 return createExecResult();
99 }
100 
101 if (command.includes("fetch --depth=1 --update-shallow origin 'main'")) {
102 events.push("fetch-depth-1");
103 return createExecResult();
104 }
105 
106 if (command.includes("checkout -B 'main' FETCH_HEAD")) {
107 events.push("checkout-branch");
108 return createExecResult();
109 }
110 
111 if (command.includes("branch --set-upstream-to='origin/main' 'main'")) {
112 events.push("set-upstream");
113 return createExecResult();
114 }
115 
116 if (command.includes("rev-parse HEAD")) {
117 events.push("rev-parse");
118 return createExecResult({
119 stdout: `${commitSha}\n`,
120 });
121 }
122 
123 throw new Error(`Unexpected exec command: ${command}`);
124 },
125 });
126 
127 const error = await prepareExecutionEnvironment(
128 context,
129 createLeaseStub({
130 throwIfOwnershipLost: () => {
131 if (context.state.ownershipLost) {
132 throw new RunOwnershipLostError(context.state.ownershipLossStatus);
133 }
134 },
135 }),
136 ).catch((error) => error);
137 
138 expect(error).toBeInstanceOf(RunOwnershipLostError);
139 expect((error as RunOwnershipLostError).observedStatus).toBe("failed");
140 expect(recordRunResolvedCommit).toHaveBeenCalledWith({
141 projectId,
142 runId,
143 commitSha,
144 });
145 expectBranchCheckoutInitialized(events);
146 expect(events).toContain("record-run-resolved-commit");
147 expect(events).toContain("ownership-lost");
148 expect(events.indexOf("ownership-lost")).toBeGreaterThan(events.indexOf("record-run-resolved-commit"));
149 expect(events).not.toContain("read-file");
150 expect(sandbox.deleteSession).toHaveBeenCalledWith("checkout-session");
151 expect(context.state.session).toBeNull();
152 });
153 
154 it("reuses a previously recorded manual-run commit instead of re-resolving branch head", async () => {
155 const commitSha = CommitSha.assertDecode("2222222222222222222222222222222222222222");
156 let catFileChecks = 0;
157 const { context, events, projectId, recordRunResolvedCommit, runId, sandbox } = createHarness({
158 projectIdValue: "prj_7777777777777777777777",
159 runIdValue: "run_7777777777777777777777",
160 snapshotCommitSha: null,
161 runMetaCommitSha: commitSha,
162 execImpl: (command) => {
163 if (command.includes(" origin 'main'") || command.includes("checkout -B 'main' FETCH_HEAD")) {
164 throw new Error(`Replay should not re-resolve branch head: ${command}`);
165 }
166 
167 if (command.startsWith("git init ")) {
168 events.push("init");
169 return createExecResult();
170 }
171 
172 if (command.includes("remote add origin")) {
173 events.push("remote-add");
174 return createExecResult();
175 }
176 
177 if (command.includes("cat-file -e")) {
178 catFileChecks += 1;
179 events.push(`cat-file-${catFileChecks}`);
180 return createExecResult({
181 success: catFileChecks >= 2,
182 stderr: catFileChecks >= 2 ? "" : "fatal: Not a valid object name",
183 });
184 }
185 
186 if (command.includes(`fetch --depth=1 --update-shallow origin '${commitSha}'`)) {
187 events.push("fetch-depth-1");
188 return createExecResult();
189 }
190 
191 if (command.includes("checkout --detach")) {
192 events.push("checkout-detached");
193 return createExecResult();
194 }
195 
196 if (command.includes("rev-parse HEAD")) {
197 events.push("rev-parse");
198 return createExecResult({
199 stdout: `${commitSha}\n`,
200 });
201 }
202 
203 throw new Error(`Unexpected exec command: ${command}`);
204 },
205 });
206 
207 await expect(prepareExecutionEnvironment(context, createLeaseStub())).rejects.toThrow(
208 "Repository config is not valid YAML",
209 );
210 
211 expect(recordRunResolvedCommit).toHaveBeenCalledWith({
212 projectId,
213 runId,
214 commitSha,
215 });
216 expect(events).toContain("fetch-depth-1");
217 expect(events).toContain("checkout-detached");
218 expect(events).not.toContain("checkout-branch");
219 expect(sandbox.deleteSession).toHaveBeenCalledWith("checkout-session");
220 expect(context.state.session).toBeNull();
221 });
222 });
223});