Skip to content
File

Blob: tests/integration/queue-runner/scenarios.ts

typescript223 lines
1import type { RunDetail, RunStatus } from "@/contracts";
2 
3import {
4 EXPECTED_LOG_MARKERS,
5 EXPECTED_STEP_NAMES,
6 FIXTURE_CONFIG_PATH,
7 FIXTURE_DEFAULT_BRANCH,
8 FIXTURE_REPO_URL,
9 GITHUB_WEBHOOK_PROVIDER,
10 type ProjectId,
11 type RunId,
12 type SessionId,
13 assert,
14 createProject,
15 getProjectDetail,
16 getProjectWebhooks,
17 postGitHubPushWebhook,
18 putGitHubWebhook,
19 resolveFixtureHeadCommitSha,
20 triggerRun,
21 waitForAcceptedWebhookDelivery,
22 waitForIndexedRun,
23 waitForProjectSettled,
24 waitForTerminalRun,
25} from "./harness";
26 
27const assertRunPassed = (detail: RunDetail): void => {
28 assert(detail.run.status === "passed", `Expected run ${detail.run.id} to pass, got ${detail.run.status}.`);
29 assert(detail.run.exitCode === 0, `Expected run ${detail.run.id} exitCode=0, got ${detail.run.exitCode}.`);
30 assert(detail.errorMessage === null, `Expected run ${detail.run.id} errorMessage=null, got ${detail.errorMessage}.`);
31 assert(detail.detailAvailable, `Expected run ${detail.run.id} to have detailAvailable=true.`);
32 assert(
33 typeof detail.run.commitSha === "string" && /^[a-f0-9]{40}$/iu.test(detail.run.commitSha),
34 `Expected run ${detail.run.id} to expose a resolved commitSha, got ${detail.run.commitSha}.`,
35 );
36 
37 const stepNames = detail.steps.map((step) => step.name);
38 assert(
39 JSON.stringify(stepNames) === JSON.stringify(EXPECTED_STEP_NAMES),
40 `Expected step names ${EXPECTED_STEP_NAMES.join(", ")}, got ${stepNames.join(", ")}.`,
41 );
42 
43 for (const step of detail.steps) {
44 assert(step.status === "passed", `Expected step ${step.name} to pass, got ${step.status}.`);
45 assert(step.startedAt !== null, `Expected step ${step.name} to have startedAt.`);
46 assert(step.finishedAt !== null, `Expected step ${step.name} to have finishedAt.`);
47 }
48 
49 const logText = detail.recentLogs.map((event) => event.chunk).join("");
50 for (const marker of EXPECTED_LOG_MARKERS) {
51 assert(logText.includes(marker), `Expected run logs to include "${marker}".`);
52 }
53};
54 
55export const scenarioSingleRunPasses = async (baseUrl: string, sessionId: SessionId): Promise<void> => {
56 const project = await createProject(baseUrl, sessionId, "Queue Runner Happy Path", {
57 dispatchMode: "queue",
58 });
59 const projectId = project.id;
60 assert(project.name === "Queue Runner Happy Path", `Expected project name to match, got ${project.name}.`);
61 assert(project.repoUrl === FIXTURE_REPO_URL, `Expected project repoUrl to match, got ${project.repoUrl}.`);
62 assert(
63 project.defaultBranch === FIXTURE_DEFAULT_BRANCH,
64 `Expected project defaultBranch to match, got ${project.defaultBranch}.`,
65 );
66 assert(
67 project.configPath === FIXTURE_CONFIG_PATH,
68 `Expected project configPath to match, got ${project.configPath}.`,
69 );
70 const runId = await triggerRun(baseUrl, sessionId, projectId);
71 const terminalRun = await waitForTerminalRun(baseUrl, sessionId, runId);
72 assertRunPassed(terminalRun);
73 
74 await waitForProjectSettled(baseUrl, sessionId, projectId);
75 const indexedRun = await waitForIndexedRun(baseUrl, sessionId, projectId, runId, "passed");
76 assert(indexedRun.exitCode === 0, `Expected indexed run ${runId} exitCode=0, got ${indexedRun.exitCode}.`);
77 assert(
78 typeof indexedRun.commitSha === "string" && /^[a-f0-9]{40}$/iu.test(indexedRun.commitSha),
79 `Expected indexed run ${runId} to have a resolved commitSha, got ${indexedRun.commitSha}.`,
80 );
81 
82 const projectDetail = await getProjectDetail(baseUrl, sessionId, projectId);
83 assert(projectDetail.project.lastRunStatus === "passed", "Expected project lastRunStatus to be passed.");
84};
85 
86export const scenarioBackToBackRunsAreSerial = async (baseUrl: string, sessionId: SessionId): Promise<void> => {
87 const project = await createProject(baseUrl, sessionId, "Queue Runner Serial Dispatch", {
88 dispatchMode: "queue",
89 });
90 const projectId = project.id;
91 const firstRunId = await triggerRun(baseUrl, sessionId, projectId);
92 const secondRunId = await triggerRun(baseUrl, sessionId, projectId);
93 
94 const firstRun = await waitForTerminalRun(baseUrl, sessionId, firstRunId);
95 const secondRun = await waitForTerminalRun(baseUrl, sessionId, secondRunId);
96 
97 assertRunPassed(firstRun);
98 assertRunPassed(secondRun);
99 const firstFinishedAt = firstRun.run.finishedAt;
100 const secondStartedAt = secondRun.run.startedAt;
101 assert(firstFinishedAt !== null, `Expected run ${firstRunId} to have finishedAt.`);
102 assert(secondStartedAt !== null, `Expected run ${secondRunId} to have startedAt.`);
103 assert(
104 Date.parse(secondStartedAt) >= Date.parse(firstFinishedAt),
105 `Expected run ${secondRunId} to start after ${firstRunId} finished.`,
106 );
107 
108 await waitForProjectSettled(baseUrl, sessionId, projectId);
109 await waitForIndexedRun(baseUrl, sessionId, projectId, firstRunId, "passed");
110 await waitForIndexedRun(baseUrl, sessionId, projectId, secondRunId, "passed");
111};
112 
113export const scenarioWebhookTriggeredRunPasses = async (baseUrl: string, sessionId: SessionId): Promise<void> => {
114 const project = await createProject(baseUrl, sessionId, "Queue Runner Webhook Happy Path", {
115 dispatchMode: "queue",
116 });
117 const projectId = project.id;
118 const webhookSecret = "queue-runner-integration-webhook-secret";
119 const expectedCommitSha = await resolveFixtureHeadCommitSha();
120 const createdWebhook = await putGitHubWebhook(baseUrl, sessionId, projectId, webhookSecret);
121 
122 assert(
123 createdWebhook.provider === GITHUB_WEBHOOK_PROVIDER,
124 `Expected webhook provider github, got ${createdWebhook.provider}.`,
125 );
126 assert(createdWebhook.enabled, "Expected created GitHub webhook to be enabled.");
127 
128 const deliveryId = `github-int-${Date.now().toString(36)}`;
129 const webhookResponse = await postGitHubPushWebhook(baseUrl, project, webhookSecret, deliveryId, expectedCommitSha);
130 assert(
131 webhookResponse.status === 202,
132 `Expected webhook delivery ${deliveryId} to return HTTP 202, got ${webhookResponse.status}: ${webhookResponse.text}`,
133 );
134 
135 const delivery = await waitForAcceptedWebhookDelivery(baseUrl, sessionId, projectId, deliveryId);
136 assert(delivery.runId !== null, `Expected webhook delivery ${deliveryId} to include a runId.`);
137 assert(delivery.provider === GITHUB_WEBHOOK_PROVIDER, `Expected delivery provider github, got ${delivery.provider}.`);
138 assert(
139 delivery.commitSha === expectedCommitSha,
140 `Expected delivery commitSha ${expectedCommitSha}, got ${delivery.commitSha}.`,
141 );
142 
143 const runId = delivery.runId;
144 const terminalRun = await waitForTerminalRun(baseUrl, sessionId, runId);
145 assertRunPassed(terminalRun);
146 assert(
147 terminalRun.run.triggerType === "webhook",
148 `Expected run ${runId} triggerType=webhook, got ${terminalRun.run.triggerType}.`,
149 );
150 assert(terminalRun.run.triggeredByUserId === null, `Expected run ${runId} triggeredByUserId=null.`);
151 assert(terminalRun.run.branch === FIXTURE_DEFAULT_BRANCH, `Expected run ${runId} branch=${FIXTURE_DEFAULT_BRANCH}.`);
152 assert(
153 terminalRun.run.commitSha === expectedCommitSha,
154 `Expected run ${runId} commitSha=${expectedCommitSha}, got ${terminalRun.run.commitSha}.`,
155 );
156 
157 await waitForProjectSettled(baseUrl, sessionId, projectId);
158 const indexedRun = await waitForIndexedRun(baseUrl, sessionId, projectId, runId, "passed");
159 assert(indexedRun.exitCode === 0, `Expected indexed run ${runId} exitCode=0, got ${indexedRun.exitCode}.`);
160 assert(
161 indexedRun.triggerType === "webhook",
162 `Expected indexed run ${runId} triggerType=webhook, got ${indexedRun.triggerType}.`,
163 );
164 assert(indexedRun.triggeredByUserId === null, `Expected indexed run ${runId} triggeredByUserId=null.`);
165 assert(
166 indexedRun.commitSha === expectedCommitSha,
167 `Expected indexed run ${runId} commitSha=${expectedCommitSha}, got ${indexedRun.commitSha}.`,
168 );
169 
170 const webhookListing = await getProjectWebhooks(baseUrl, sessionId, projectId);
171 const webhook = webhookListing.webhooks.find((candidate) => candidate.provider === GITHUB_WEBHOOK_PROVIDER);
172 assert(webhook !== undefined, "Expected GitHub webhook to be listed.");
173 const recordedDelivery = webhook.recentDeliveries.find((candidate) => candidate.deliveryId === deliveryId);
174 assert(recordedDelivery !== undefined, `Expected webhook recent deliveries to include ${deliveryId}.`);
175 assert(recordedDelivery.outcome === "accepted", `Expected delivery ${deliveryId} outcome=accepted.`);
176 assert(
177 recordedDelivery.runId === runId,
178 `Expected delivery ${deliveryId} runId=${runId}, got ${recordedDelivery.runId}.`,
179 );
180 assert(
181 recordedDelivery.commitSha === expectedCommitSha,
182 `Expected delivery ${deliveryId} commitSha=${expectedCommitSha}, got ${recordedDelivery.commitSha}.`,
183 );
184 
185 const projectDetail = await getProjectDetail(baseUrl, sessionId, projectId);
186 assert(projectDetail.project.lastRunStatus === "passed", "Expected project lastRunStatus to be passed.");
187};
188 
189export const scenarioWorkflowRunPasses = async (baseUrl: string, sessionId: SessionId): Promise<void> => {
190 const project = await createProject(baseUrl, sessionId, "Workflows Runner Happy Path", {
191 dispatchMode: "workflows",
192 });
193 const projectId = project.id;
194 
195 assert(project.name === "Workflows Runner Happy Path", `Expected project name to match, got ${project.name}.`);
196 assert(project.dispatchMode === "workflows", `Expected project dispatchMode=workflows, got ${project.dispatchMode}.`);
197 assert(project.repoUrl === FIXTURE_REPO_URL, `Expected project repoUrl to match, got ${project.repoUrl}.`);
198 assert(
199 project.defaultBranch === FIXTURE_DEFAULT_BRANCH,
200 `Expected project defaultBranch to match, got ${project.defaultBranch}.`,
201 );
202 assert(
203 project.configPath === FIXTURE_CONFIG_PATH,
204 `Expected project configPath to match, got ${project.configPath}.`,
205 );
206 
207 const runId = await triggerRun(baseUrl, sessionId, projectId);
208 const terminalRun = await waitForTerminalRun(baseUrl, sessionId, runId);
209 assertRunPassed(terminalRun);
210 
211 await waitForProjectSettled(baseUrl, sessionId, projectId);
212 const indexedRun = await waitForIndexedRun(baseUrl, sessionId, projectId, runId, "passed");
213 assert(indexedRun.exitCode === 0, `Expected indexed run ${runId} exitCode=0, got ${indexedRun.exitCode}.`);
214 assert(
215 typeof indexedRun.commitSha === "string" && /^[a-f0-9]{40}$/iu.test(indexedRun.commitSha),
216 `Expected indexed run ${runId} to have a resolved commitSha, got ${indexedRun.commitSha}.`,
217 );
218 
219 const projectDetail = await getProjectDetail(baseUrl, sessionId, projectId);
220 assert(projectDetail.project.dispatchMode === "workflows", "Expected project dispatchMode to be workflows.");
221 assert(projectDetail.project.lastRunStatus === "passed", "Expected project lastRunStatus to be passed.");
222};