|
1 | | -import { execFileSync } from "node:child_process"; |
2 | | -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; |
| 1 | +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
3 | 2 | import { tmpdir } from "node:os"; |
4 | 3 | import { join } from "node:path"; |
5 | | -import { describe, expect, it } from "vitest"; |
6 | | -import { formatCfTypegenOutput } from "../../scripts/gen-cf-typegen.mjs"; |
| 4 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 5 | +import { formatCfTypegenOutput, genCfTypegen, OUTPUT_PATH } from "../../scripts/gen-cf-typegen.mjs"; |
| 6 | + |
| 7 | +// Only the `wrangler` invocation is ever faked below (via mockImplementation in the retry tests); every other |
| 8 | +// call (the `git merge-file` regression tests further down) passes straight through to the real execFileSync, |
| 9 | +// so this mock changes nothing about those tests. |
| 10 | +vi.mock("node:child_process", async (importOriginal) => { |
| 11 | + const actual = await importOriginal<typeof import("node:child_process")>(); |
| 12 | + return { ...actual, execFileSync: vi.fn(actual.execFileSync) }; |
| 13 | +}); |
| 14 | +import { execFileSync } from "node:child_process"; |
7 | 15 |
|
8 | 16 | // #1667-followup: wrangler's own `wrangler types` output packs every declared env var into ONE long |
9 | 17 | // single-line `Pick<Cloudflare.Env, "A" | "B" | ...>>` union. Two independent PRs that each add a DIFFERENT |
@@ -105,3 +113,79 @@ describe("gen-cf-typegen: formatCfTypegenOutput", () => { |
105 | 113 | } |
106 | 114 | }); |
107 | 115 | }); |
| 116 | + |
| 117 | +// `wrangler types` is observed to occasionally emit a differently-shaped ProcessEnv union on a "cold" |
| 118 | +// invocation vs a "warm" one for the SAME wrangler.jsonc and wrangler version -- external-tool flakiness, not |
| 119 | +// a defect in this repo's own reformatting logic. genCfTypegen's check path retries once before reporting |
| 120 | +// staleness, so a transient one-off mismatch self-heals instead of failing CI. |
| 121 | +describe("gen-cf-typegen: genCfTypegen retry behavior", () => { |
| 122 | + const originalCwd = process.cwd(); |
| 123 | + let dir: string; |
| 124 | + |
| 125 | + beforeEach(() => { |
| 126 | + dir = mkdtempSync(join(tmpdir(), "cf-typegen-retry-")); |
| 127 | + process.chdir(dir); |
| 128 | + }); |
| 129 | + |
| 130 | + afterEach(() => { |
| 131 | + process.chdir(originalCwd); |
| 132 | + rmSync(dir, { recursive: true, force: true }); |
| 133 | + vi.mocked(execFileSync).mockClear(); |
| 134 | + }); |
| 135 | + |
| 136 | + function mockWrangler(outputs: string[]) { |
| 137 | + let call = 0; |
| 138 | + vi.mocked(execFileSync).mockImplementation(((cmd: string, args: string[]) => { |
| 139 | + if (cmd !== "wrangler") throw new Error(`unexpected command: ${cmd}`); |
| 140 | + const target = args[1]!; |
| 141 | + const output = outputs[Math.min(call, outputs.length - 1)]!; |
| 142 | + call += 1; |
| 143 | + writeFileSync(target, output); |
| 144 | + return Buffer.from(""); |
| 145 | + }) as typeof execFileSync); |
| 146 | + } |
| 147 | + |
| 148 | + it("REGRESSION: a mismatch on the first wrangler invocation self-heals when the retry matches (transient tool flakiness)", () => { |
| 149 | + const committed = formatCfTypegenOutput(rawFixture(["ALPHA", "MID"])); |
| 150 | + writeFileSync(OUTPUT_PATH, committed); |
| 151 | + // First call emits an extra var (simulating a cold-vs-warm wrangler discrepancy); the retry matches. |
| 152 | + mockWrangler([rawFixture(["ALPHA", "MID", "STRAY"]), rawFixture(["ALPHA", "MID"])]); |
| 153 | + |
| 154 | + const { changed } = genCfTypegen({ check: true }); |
| 155 | + |
| 156 | + expect(changed).toBe(false); |
| 157 | + expect(execFileSync).toHaveBeenCalledTimes(2); |
| 158 | + }); |
| 159 | + |
| 160 | + it("still reports genuine drift when BOTH the first attempt and the retry mismatch the committed file", () => { |
| 161 | + const committed = formatCfTypegenOutput(rawFixture(["ALPHA", "MID"])); |
| 162 | + writeFileSync(OUTPUT_PATH, committed); |
| 163 | + mockWrangler([rawFixture(["ALPHA", "MID", "REAL_NEW_VAR"])]); |
| 164 | + |
| 165 | + const { changed } = genCfTypegen({ check: true }); |
| 166 | + |
| 167 | + expect(changed).toBe(true); |
| 168 | + expect(execFileSync).toHaveBeenCalledTimes(2); |
| 169 | + }); |
| 170 | + |
| 171 | + it("does not retry (single wrangler invocation) when the committed file is already up to date", () => { |
| 172 | + const committed = formatCfTypegenOutput(rawFixture(["ALPHA", "MID"])); |
| 173 | + writeFileSync(OUTPUT_PATH, committed); |
| 174 | + mockWrangler([rawFixture(["ALPHA", "MID"])]); |
| 175 | + |
| 176 | + const { changed } = genCfTypegen({ check: true }); |
| 177 | + |
| 178 | + expect(changed).toBe(false); |
| 179 | + expect(execFileSync).toHaveBeenCalledTimes(1); |
| 180 | + }); |
| 181 | + |
| 182 | + it("regenerate mode (check: false) writes the formatted output directly to worker-configuration.d.ts", () => { |
| 183 | + mockWrangler([rawFixture(["ZEBRA", "ALPHA"])]); |
| 184 | + |
| 185 | + const { changed } = genCfTypegen({ check: false }); |
| 186 | + |
| 187 | + expect(changed).toBeUndefined(); |
| 188 | + expect(readFileSync(OUTPUT_PATH, "utf8")).toBe(formatCfTypegenOutput(rawFixture(["ZEBRA", "ALPHA"]))); |
| 189 | + expect(execFileSync).toHaveBeenCalledTimes(1); |
| 190 | + }); |
| 191 | +}); |
0 commit comments