Spaces:
Running
Running
Download scripts/changelog-guard.test.js from Felladrin/MiniSearch: direct link, hf CLI and curl.
- Browser
- Download file 8.36 kB
-
https://huggingface.co/spaces/Felladrin/MiniSearch/resolve/main/scripts/changelog-guard.test.js
- Command line
-
hf download hf://spaces/Felladrin/MiniSearch/scripts/changelog-guard.test.js
-
curl -L -o changelog-guard.test.js https://huggingface.co/spaces/Felladrin/MiniSearch/resolve/main/scripts/changelog-guard.test.js
8.36 kB
| import { execFileSync } from "node:child_process"; | |
| import { createRequire } from "node:module"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| import { describe, expect, it } from "vitest"; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const SCRIPT_PATH = path.resolve(__dirname, "changelog-guard.cjs"); | |
| const REPO_ROOT = path.resolve(__dirname, ".."); | |
| const { evaluate, MAX_LISTED_FILES } = createRequire(import.meta.url)( | |
| SCRIPT_PATH, | |
| ); | |
| function runGuard(args = [], { stdin, env } = {}) { | |
| try { | |
| const output = execFileSync(process.execPath, [SCRIPT_PATH, ...args], { | |
| cwd: REPO_ROOT, | |
| encoding: "utf8", | |
| stdio: "pipe", | |
| input: stdin ?? "", | |
| env: { ...process.env, ...env }, | |
| }); | |
| return { exitCode: 0, stdout: output, stderr: "" }; | |
| } catch (error) { | |
| if (error.signal) throw new Error(`Killed by ${error.signal}`); | |
| return { | |
| exitCode: error.status ?? 1, | |
| stdout: error.stdout ?? "", | |
| stderr: error.stderr ?? "", | |
| }; | |
| } | |
| } | |
| // A shell pipeline, so the writer owns its end of the pipe. execFileSync's | |
| // `input` hands the script a file descriptor instead, which never reproduces | |
| // a writer that is slow or that marks the pipe non-blocking. | |
| function runPipeline(writerCommand) { | |
| try { | |
| const output = execFileSync( | |
| "bash", | |
| ["-c", `${writerCommand} | "$0" "$1"`, process.execPath, SCRIPT_PATH], | |
| { cwd: REPO_ROOT, encoding: "utf8", stdio: "pipe" }, | |
| ); | |
| return { exitCode: 0, stdout: output }; | |
| } catch (error) { | |
| if (error.signal) throw new Error(`Killed by ${error.signal}`); | |
| return { exitCode: error.status ?? 1, stdout: error.stdout ?? "" }; | |
| } | |
| } | |
| describe("changelog-guard decision", () => { | |
| it("fails a user-facing change that carries no changelog entry", () => { | |
| const { ok, reason } = evaluate({ | |
| changedFiles: ["client/modules/search.ts"], | |
| }); | |
| expect(ok).toBe(false); | |
| expect(reason).toContain("client/modules/search.ts"); | |
| expect(reason).toContain("changelog.md"); | |
| }); | |
| it("passes the same change once changelog.md is part of it", () => { | |
| const { ok } = evaluate({ | |
| changedFiles: ["client/modules/search.ts", "changelog.md"], | |
| }); | |
| expect(ok).toBe(true); | |
| }); | |
| it("passes a docs-only change", () => { | |
| const { ok } = evaluate({ | |
| changedFiles: ["docs/pull-requests.md", "README.md", "agents.md"], | |
| }); | |
| expect(ok).toBe(true); | |
| }); | |
| it("passes a lockfile-only change", () => { | |
| const { ok } = evaluate({ | |
| changedFiles: ["package-lock.json", "package.json"], | |
| }); | |
| expect(ok).toBe(true); | |
| }); | |
| it("passes a user-facing change carrying the skip-changelog label", () => { | |
| const { ok, reason } = evaluate({ | |
| changedFiles: ["server/webSearchService.ts"], | |
| hasSkipLabel: true, | |
| }); | |
| expect(ok).toBe(true); | |
| expect(reason).toContain("skip-changelog"); | |
| }); | |
| // "renovate[bot]" is the REST shape, which the workflow reads. | |
| // "app/renovate" is the GraphQL shape, which `gh pr view --json author` | |
| // returns. Both pass, so changing where the author comes from cannot | |
| // silently break the hatch. | |
| it.each([ | |
| "renovate[bot]", | |
| "dependabot[bot]", | |
| "app/renovate", | |
| "app/dependabot", | |
| ])("passes a %s pull request that only bumps the Dockerfile", (login) => { | |
| const { ok, reason } = evaluate({ | |
| changedFiles: ["Dockerfile"], | |
| authorLogin: login, | |
| }); | |
| expect(ok).toBe(true); | |
| expect(reason).toContain(login); | |
| }); | |
| it.each(["renovate", "dependabot", "renovate-bot", "app/renovate-fork"])( | |
| "does not treat %s as a bot login", | |
| (login) => { | |
| expect( | |
| evaluate({ changedFiles: ["Dockerfile"], authorLogin: login }).ok, | |
| ).toBe(false); | |
| }, | |
| ); | |
| it("fails a human pull request that changes the Dockerfile with no entry", () => { | |
| const { ok, reason } = evaluate({ | |
| changedFiles: ["Dockerfile"], | |
| authorLogin: "felladrin", | |
| }); | |
| expect(ok).toBe(false); | |
| expect(reason).toContain("Dockerfile"); | |
| }); | |
| it.each([ | |
| "Dockerfile", | |
| "docker-compose.yml", | |
| "docker-compose.production.yml", | |
| "searxng-settings.yml", | |
| "client/index.tsx", | |
| "server/searchToken.ts", | |
| "shared/types.ts", | |
| ])("treats %s as user-facing", (filePath) => { | |
| expect(evaluate({ changedFiles: [filePath] }).ok).toBe(false); | |
| }); | |
| it.each([ | |
| "docs/api.md", | |
| "changelog.md", | |
| "e2e/smoke.spec.ts", | |
| "scripts/changelog-guard.cjs", | |
| ".github/workflows/ci.yml", | |
| "clientele/notes.txt", | |
| ])("treats %s as not user-facing", (filePath) => { | |
| expect(evaluate({ changedFiles: [filePath] }).ok).toBe(true); | |
| }); | |
| it("names today's UTC date in the heading it asks for", () => { | |
| const { reason } = evaluate({ | |
| changedFiles: ["client/index.tsx"], | |
| // 00:30 UTC on the 24th; a local-time formatter would print the 23rd. | |
| now: new Date("2026-09-24T00:30:00Z"), | |
| }); | |
| expect(reason).toContain("## 2026-09-24"); | |
| }); | |
| it("caps the listed files and counts the rest", () => { | |
| const changedFiles = Array.from( | |
| { length: MAX_LISTED_FILES + 3 }, | |
| (_, index) => `client/modules/file${index}.ts`, | |
| ); | |
| const { ok, reason } = evaluate({ changedFiles }); | |
| expect(ok).toBe(false); | |
| expect(reason).toContain("and 3 more"); | |
| expect(reason).not.toContain(`file${MAX_LISTED_FILES}.ts,`); | |
| }); | |
| it("ignores blank lines from an empty git diff", () => { | |
| expect(evaluate({ changedFiles: ["", " ", "\n"] }).ok).toBe(true); | |
| }); | |
| it("requires an entry even when a non-trigger file is also changed", () => { | |
| const { ok } = evaluate({ | |
| changedFiles: ["docs/api.md", "server/rerankerService.ts"], | |
| }); | |
| expect(ok).toBe(false); | |
| }); | |
| }); | |
| describe("changelog-guard command line", () => { | |
| it("exits 1 and prints the remedy for a failing change", () => { | |
| const { exitCode, stdout } = runGuard(["client/modules/search.ts"]); | |
| expect(exitCode).toBe(1); | |
| expect(stdout).toContain("client/modules/search.ts"); | |
| expect(stdout).toContain("skip-changelog"); | |
| }); | |
| it("exits 0 for a change that includes the changelog", () => { | |
| const { exitCode } = runGuard(["client/modules/search.ts", "changelog.md"]); | |
| expect(exitCode).toBe(0); | |
| }); | |
| it("reads the changed files from stdin, one per line", () => { | |
| const { exitCode, stdout } = runGuard([], { | |
| stdin: "server/searchToken.ts\ndocs/api.md\n", | |
| }); | |
| expect(exitCode).toBe(1); | |
| expect(stdout).toContain("server/searchToken.ts"); | |
| }); | |
| it("honours --skip-label", () => { | |
| expect(runGuard(["Dockerfile", "--skip-label"]).exitCode).toBe(0); | |
| }); | |
| it("honours --author for a bot", () => { | |
| expect(runGuard(["Dockerfile", "--author", "renovate[bot]"]).exitCode).toBe( | |
| 0, | |
| ); | |
| expect(runGuard(["Dockerfile", "--author", "app/renovate"]).exitCode).toBe( | |
| 0, | |
| ); | |
| }); | |
| it("reads the same facts from the environment", () => { | |
| expect( | |
| runGuard(["Dockerfile"], { | |
| env: { CHANGELOG_GUARD_SKIP_LABEL: "true" }, | |
| }).exitCode, | |
| ).toBe(0); | |
| expect( | |
| runGuard(["Dockerfile"], { | |
| env: { CHANGELOG_GUARD_AUTHOR: "renovate[bot]" }, | |
| }).exitCode, | |
| ).toBe(0); | |
| expect( | |
| runGuard(["Dockerfile"], { | |
| env: { CHANGELOG_GUARD_SKIP_LABEL: "false" }, | |
| }).exitCode, | |
| ).toBe(1); | |
| }); | |
| it("exits 1 on an unknown option", () => { | |
| const { exitCode, stderr } = runGuard(["--nope"]); | |
| expect(exitCode).toBe(1); | |
| expect(stderr).toContain("Unknown option"); | |
| }); | |
| it("exits 0 when nothing changed", () => { | |
| expect(runGuard([], { stdin: "" }).exitCode).toBe(0); | |
| }); | |
| it("waits for a writer that has not produced anything yet", () => { | |
| const { exitCode, stdout } = runPipeline( | |
| `sleep 0.3; printf 'server/searchToken.ts\\n'`, | |
| ); | |
| expect(exitCode).toBe(1); | |
| expect(stdout).toContain("server/searchToken.ts"); | |
| }); | |
| it("reads a list larger than one read buffer", () => { | |
| const paths = Array.from( | |
| { length: 4000 }, | |
| (_, index) => `docs/file${index}.md`, | |
| ); | |
| paths.push("client/index.tsx"); | |
| const input = `${paths.join("\n")}\n`; | |
| expect(input.length).toBeGreaterThan(64 * 1024); | |
| const { exitCode, stdout } = runGuard([], { stdin: input }); | |
| expect(exitCode).toBe(1); | |
| expect(stdout).toContain("client/index.tsx"); | |
| }); | |
| }); | |