Public: anyone can read this; the crew can change it.

Flaky: test/security-merge.test.ts #2129

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2104-flaky-test-security-merge-test-ts → main 1 commit fork

  • Merged. scout/2104-flaky-test-security-merge-test-ts was merged into main as 6285053.

Review

Sign in to review this change.
2 files changed +69 −116
scripts/gate.ts −5
@@ -67,11 +67,6 @@ export const EXCLUDED: readonly Exclusion[] = [
6767 { file: "test/runs-lease.test.ts", reason: "not yet admitted", issue: 261 },
6868 { file: "test/scaffold.test.ts", reason: "not yet admitted", issue: 261 },
6969 { file: "test/search.test.ts", reason: "not yet admitted", issue: 261 },
70 {
71 file: "test/security-merge.test.ts",
72 reason: "not yet admitted",
73 issue: 261,
74 },
7570 { file: "test/security-xss.test.ts", reason: "not yet admitted", issue: 261 },
7671 {
7772 file: "test/snapshot-destination.test.ts",
test/security-merge.test.ts +69 −111
@@ -309,85 +309,81 @@ describe("merge correctness (clean results, recorded for regression)", () => {
309309 await rm(work, { recursive: true, force: true });
310310 });
311311
312 test("squash merge produces exactly one parent; regular merge produces exactly two", async () => {
312 // Each test builds its own branches and pull requests from the current
313 // main, and reads the pull number the forge hands back, so no test depends
314 // on what another left behind or on the order they run in.
315 async function branchFromMain(
316 name: string,
317 file: string,
318 content: string,
319 ): Promise<void> {
313320 const dir = join(work, "app");
321 await gitOk(["fetch", "origin"], dir);
322 await gitOk(["checkout", "-B", "main", "origin/main"], dir);
323 await gitOk(["checkout", "-b", name], dir);
324 await Bun.write(join(dir, file), content);
325 await gitOk(["add", "."], dir);
326 await gitOk(["commit", "-m", `change on ${name}`], dir);
327 await gitOk(["push", "origin", name], dir);
328 }
329
330 async function openPull(head: string): Promise<number> {
331 const res = await fetch(`${base}/bob/app/pulls/new`, {
332 method: "POST",
333 headers: { cookie },
334 body: form({ base: "main", head, title: `PR ${head}`, body: "" }),
335 redirect: "manual",
336 });
337 const m = /\/pulls\/(\d+)$/.exec(res.headers.get("location") ?? "");
338 if (!m) throw new Error(`pull for ${head} not opened: ${res.status}`);
339 return Number(m[1]);
340 }
341
342 async function mergePull(n: number, strategy: string): Promise<void> {
343 await fetch(`${base}/bob/app/pulls/${n}/merge`, {
344 method: "POST",
345 headers: { cookie },
346 body: form({ strategy }),
347 });
348 }
349
350 async function parentCount(sha: string, gitDir: string): Promise<number> {
351 return (await gitCapture(["cat-file", "-p", sha], gitDir))
352 .split("\n")
353 .filter((l) => l.startsWith("parent ")).length;
354 }
355
356 test("squash merge produces exactly one parent; regular merge produces exactly two", async () => {
314357 const repoGitDir = join(dataDir, "repos/bob/app.git");
315358
316 await gitOk(["checkout", "-b", "squash-feature"], dir);
317 await Bun.write(join(dir, "code.txt"), "alpha\nBETA\ngamma\n");
318 await gitOk(["commit", "-am", "squash change"], dir);
319 await gitOk(["push", "origin", "squash-feature"], dir);
320 await fetch(`${base}/bob/app/pulls/new`, {
321 method: "POST",
322 headers: { cookie },
323 body: form({
324 base: "main",
325 head: "squash-feature",
326 title: "Squash PR",
327 body: "",
328 }),
329 });
330 await fetch(`${base}/bob/app/pulls/1/merge`, {
331 method: "POST",
332 headers: { cookie },
333 body: form({ strategy: "squash" }),
334 });
359 await branchFromMain("squash-feature", "code.txt", "alpha\nBETA\ngamma\n");
360 const squashPr = await openPull("squash-feature");
361 await mergePull(squashPr, "squash");
335362 const squashSha = await gitCapture(
336 ["rev-parse", "refs/pull/1/merged"],
363 ["rev-parse", `refs/pull/${squashPr}/merged`],
337364 repoGitDir,
338365 );
339 const squashParents = (
340 await gitCapture(["cat-file", "-p", squashSha], repoGitDir)
341 )
342 .split("\n")
343 .filter((l) => l.startsWith("parent ")).length;
344 expect(squashParents).toBe(1);
366 expect(await parentCount(squashSha, repoGitDir)).toBe(1);
345367
346 await gitOk(["checkout", "main"], dir);
347 await gitOk(["pull", "origin", "main"], dir);
348 await gitOk(["checkout", "-b", "merge-feature"], dir);
349 await Bun.write(join(dir, "other.txt"), "new file\n");
350 await gitOk(["add", "."], dir);
351 await gitOk(["commit", "-m", "merge change"], dir);
352 await gitOk(["push", "origin", "merge-feature"], dir);
353 await fetch(`${base}/bob/app/pulls/new`, {
354 method: "POST",
355 headers: { cookie },
356 body: form({
357 base: "main",
358 head: "merge-feature",
359 title: "Merge PR",
360 body: "",
361 }),
362 });
363 await fetch(`${base}/bob/app/pulls/2/merge`, {
364 method: "POST",
365 headers: { cookie },
366 body: form({ strategy: "merge" }),
367 });
368 await branchFromMain("merge-feature", "other.txt", "new file\n");
369 const mergePr = await openPull("merge-feature");
370 await mergePull(mergePr, "merge");
368371 const mergeSha = await gitCapture(
369 ["rev-parse", "refs/pull/2/merged"],
372 ["rev-parse", `refs/pull/${mergePr}/merged`],
370373 repoGitDir,
371374 );
372 const mergeParents = (
373 await gitCapture(["cat-file", "-p", mergeSha], repoGitDir)
374 )
375 .split("\n")
376 .filter((l) => l.startsWith("parent ")).length;
377 expect(mergeParents).toBe(2);
375 expect(await parentCount(mergeSha, repoGitDir)).toBe(2);
378376 });
379377
380378 test("refs/pull/N/merged survives `git gc --prune=now`", async () => {
381379 const repoGitDir = join(dataDir, "repos/bob/app.git");
382 const before = await gitCapture(
383 ["rev-parse", "refs/pull/1/merged"],
384 repoGitDir,
385 );
380 await branchFromMain("gc-feature", "gc.txt", "gc me\n");
381 const pr = await openPull("gc-feature");
382 await mergePull(pr, "squash");
383 const ref = `refs/pull/${pr}/merged`;
384 const before = await gitCapture(["rev-parse", ref], repoGitDir);
386385 await gitOk(["gc", "--prune=now"], repoGitDir);
387 const after = await gitCapture(
388 ["rev-parse", "refs/pull/1/merged"],
389 repoGitDir,
390 );
386 const after = await gitCapture(["rev-parse", ref], repoGitDir);
391387 expect(after).toBe(before);
392388 // The object itself must still resolve, not just the ref.
393389 const catOut = await gitCapture(["cat-file", "-t", after], repoGitDir);
@@ -395,66 +391,28 @@ describe("merge correctness (clean results, recorded for regression)", () => {
395391 });
396392
397393 test("a conflicting merge attempt leaves the base ref completely unchanged", async () => {
398 const dir = join(work, "app");
399394 const repoGitDir = join(dataDir, "repos/bob/app.git");
400395
401 await gitOk(["checkout", "main"], dir);
402 await gitOk(["pull", "origin", "main"], dir);
403 await gitOk(["checkout", "-b", "conflict-a"], dir);
404 await Bun.write(join(dir, "code.txt"), "ALPHA-A\nbeta\ngamma\n");
405 await gitOk(["commit", "-am", "conflict a"], dir);
406 await gitOk(["push", "origin", "conflict-a"], dir);
407
408 await gitOk(["checkout", "main"], dir);
409 await gitOk(["checkout", "-b", "conflict-b"], dir);
410 await Bun.write(join(dir, "code.txt"), "ALPHA-B\nbeta\ngamma\n");
411 await gitOk(["commit", "-am", "conflict b"], dir);
412 await gitOk(["push", "origin", "conflict-b"], dir);
413
414 await fetch(`${base}/bob/app/pulls/new`, {
415 method: "POST",
416 headers: { cookie },
417 body: form({
418 base: "main",
419 head: "conflict-a",
420 title: "Conflict A",
421 body: "",
422 }),
423 });
424 await fetch(`${base}/bob/app/pulls/new`, {
425 method: "POST",
426 headers: { cookie },
427 body: form({
428 base: "main",
429 head: "conflict-b",
430 title: "Conflict B",
431 body: "",
432 }),
433 });
434 // Merge conflict-a first, so conflict-b's merge attempt against the new
435 // main will genuinely conflict.
436 await fetch(`${base}/bob/app/pulls/3/merge`, {
437 method: "POST",
438 headers: { cookie },
439 body: form({ strategy: "merge" }),
440 });
396 // Both branches rewrite the same file from the same base, so whichever
397 // merges second genuinely conflicts.
398 await branchFromMain("conflict-a", "conflict.txt", "A\n");
399 await branchFromMain("conflict-b", "conflict.txt", "B\n");
400 const a = await openPull("conflict-a");
401 const b = await openPull("conflict-b");
402 await mergePull(a, "merge");
441403
442404 const before = await gitCapture(
443405 ["rev-parse", "refs/heads/main"],
444406 repoGitDir,
445407 );
446 await fetch(`${base}/bob/app/pulls/4/merge`, {
447 method: "POST",
448 headers: { cookie },
449 body: form({ strategy: "merge" }),
450 });
408 await mergePull(b, "merge");
451409 const after = await gitCapture(
452410 ["rev-parse", "refs/heads/main"],
453411 repoGitDir,
454412 );
455413 expect(after).toBe(before);
456414
457 const page = await (await fetch(`${base}/bob/app/pulls/4/files`)).text();
415 const page = await (await fetch(`${base}/bob/app/pulls/${b}/files`)).text();
458416 expect(page).toContain("Can't merge.");
459417 });
460418 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs