import { describe, expect, it } from "vitest"; import { GITHUB_ERROR_BODY_MAX_BYTES, canAutoscrubPullRequest, createAutoscrubCommit, dependencyGuardCommentAuthors, dependencyGuardCommentHeadSha, dependencyFieldChanges, dependencyOverrideExpectedSha, findDependencyOverrideCommand, findDependencyOverrideCommandAsync, githubApi, isAutoscrubbedDependencyComment, isDependencyGuardAuthorizedForHead, isDependencyFile, isDependencyGuardMarkerComment, isDependencyManifest, isPackageLockfile, readBoundedGitHubErrorText, renderAuthorizedDependencyComment, renderAutoscrubbedDependencyComment, renderBlockedDependencyComment, renderClearedDependencyGuardComment, sanitizeDisplayValue, securityApproverSet, shouldAutoscrubDependencyLockfiles, } from "../../scripts/github/dependency-guard.mjs"; const headSha = "a".repeat(40); const staleSha = "b".repeat(40); describe("dependency guard script", () => { it("detects dependency guard file surfaces", () => { expect(isDependencyFile("pnpm-lock.yaml")).toBe(true); expect(isDependencyFile("package.json")).toBe(false); expect(isDependencyFile("ui/package.json")).toBe(false); expect(isDependencyFile("packages/core/package.json")).toBe(false); expect(isDependencyFile("qa/convex-credential-broker/package.json")).toBe(false); expect(isDependencyFile("extensions/slack/npm-shrinkwrap.json")).toBe(true); expect(isDependencyFile("tools/nested/pnpm-lock.yaml")).toBe(true); expect(isDependencyFile("src/index.ts")).toBe(false); expect(isPackageLockfile("pnpm-lock.yaml")).toBe(true); expect(isPackageLockfile("extensions/slack/npm-shrinkwrap.json")).toBe(true); expect(isPackageLockfile("package.json")).toBe(false); }); it("compares package manifest fields that can affect dependency resolution", () => { expect(isDependencyManifest("package.json")).toBe(true); expect(isDependencyManifest("extensions/slack/package.json")).toBe(true); expect(isDependencyManifest("qa/convex-credential-broker/package.json")).toBe(true); expect(isDependencyManifest("src/index.ts")).toBe(false); expect( dependencyFieldChanges( { scripts: { test: "old" }, dependencies: { a: "1" } }, { scripts: { test: "new" }, dependencies: { a: "1" } }, ), ).toEqual([]); expect( dependencyFieldChanges( { dependencies: { a: "1" }, devDependencies: { b: "1" } }, { dependencies: { a: "2" }, devDependencies: { b: "1", c: "1" } }, ), ).toEqual(["dependencies", "devDependencies"]); expect( dependencyFieldChanges( { optionalDependencies: { a: "1" }, peerDependencies: { b: "1" }, overrides: { c: "1" }, packageManager: "pnpm@10.0.0", pnpm: { patchedDependencies: { d: "patches/d.patch" } }, scripts: { test: "old" }, }, { optionalDependencies: { a: "2" }, peerDependencies: { b: "2" }, overrides: { c: "2" }, packageManager: "pnpm@10.1.0", pnpm: { patchedDependencies: { d: "patches/d2.patch" } }, scripts: { test: "new" }, }, ), ).toEqual(["optionalDependencies", "peerDependencies", "overrides", "packageManager", "pnpm"]); }); it("accepts only security-member override commands for the current head sha", () => { const comments = [ { body: "/allow-dependencies-change not enough", created_at: "2026-05-28T20:00:00Z", user: { login: "not-security" }, }, { body: "/allow-dependencies-change stale approval", created_at: "2026-05-28T20:01:00Z", user: { login: "security-user" }, }, { body: "/allow-dependencies-change reviewed dependency graph", created_at: "2026-05-28T20:03:00Z", html_url: "https://example.test/comment", user: { login: "security-user" }, }, ]; const override = findDependencyOverrideCommand({ comments, expectedSha: headSha, isSecurityMember: (login) => login === "security-user", newerThan: "2026-05-28T20:02:00Z", }); expect(override).toEqual({ login: "security-user", reason: "reviewed dependency graph", sha: headSha, url: "https://example.test/comment", }); }); it("rejects stale or non-security override commands", async () => { const comments = [ { body: "/allow-dependencies-change stale approval", created_at: "2026-05-28T20:00:00Z", user: { login: "security-user" }, }, { body: "/allow-dependencies-change not enough", created_at: "2026-05-28T20:02:00Z", user: { login: "not-security" }, }, ]; await expect( findDependencyOverrideCommandAsync({ comments, expectedSha: headSha, isSecurityMember: async (login) => login === "security-user", newerThan: "2026-05-28T20:01:00Z", }), ).resolves.toBeNull(); }); it("rejects override commands without a freshness barrier", () => { const override = findDependencyOverrideCommand({ comments: [ { body: "/allow-dependencies-change", created_at: "2026-05-28T20:03:00Z", user: { login: "security-user" }, }, ], expectedSha: headSha, isSecurityMember: (login) => login === "security-user", }); expect(override).toBeNull(); }); it("accepts override commands without a reason", () => { const override = findDependencyOverrideCommand({ comments: [ { body: "/allow-dependencies-change", created_at: "2026-05-28T20:03:00Z", user: { login: "security-user" }, }, ], expectedSha: headSha, isSecurityMember: (login) => login === "security-user", newerThan: "2026-05-28T20:02:00Z", }); expect(override).toEqual({ login: "security-user", reason: null, sha: headSha, url: undefined, }); }); it("binds override commands to the head sha in the blocked guard comment", () => { const blockedComment = { body: renderBlockedDependencyComment({ baseBranch: "main", headSha, lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], }), }; const staleBlockedComment = { body: renderBlockedDependencyComment({ baseBranch: "main", headSha: staleSha, lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], }), }; expect(dependencyGuardCommentHeadSha(blockedComment)).toBe(headSha); expect(dependencyOverrideExpectedSha(blockedComment, headSha)).toBe(headSha); expect(dependencyOverrideExpectedSha(staleBlockedComment, headSha)).toBeNull(); }); it("preserves same-head authorization across reruns", () => { const authorizedComment = { body: renderAuthorizedDependencyComment({ login: "security-user", reason: null, sha: headSha, }), }; expect(dependencyGuardCommentHeadSha(authorizedComment)).toBe(headSha); expect(isDependencyGuardAuthorizedForHead(authorizedComment, headSha)).toBe(true); expect(isDependencyGuardAuthorizedForHead(authorizedComment, staleSha)).toBe(false); expect(dependencyOverrideExpectedSha(authorizedComment, headSha)).toBeNull(); }); it("trusts only configured dependency guard marker comment authors", () => { const trustedAuthors = dependencyGuardCommentAuthors( "github-actions[bot], openclaw-autoscrub[bot]", ); expect( isDependencyGuardMarkerComment( { body: "", user: { login: "openclaw-autoscrub[bot]" }, }, "", trustedAuthors, ), ).toBe(true); expect( isDependencyGuardMarkerComment( { body: "", user: { login: "contributor" }, }, "", trustedAuthors, ), ).toBe(false); expect( isDependencyGuardMarkerComment( { body: "no marker", user: { login: "github-actions[bot]" }, }, "", trustedAuthors, ), ).toBe(false); }); it("renders deterministic removal guidance for blocked lockfile changes", () => { const body = renderBlockedDependencyComment({ baseBranch: "main", headSha, lockfileChanges: ["pnpm-lock.yaml", "extensions/slack/npm-shrinkwrap.json"], dependencyManifestChanges: [ { path: "package.json", fields: ["dependencies"], }, ], }); expect(body).toContain(""); expect(body).toContain("Dependency graph changes are blocked"); expect(body).toContain("`pnpm-lock.yaml` changed."); expect(body).toContain("`extensions/slack/npm-shrinkwrap.json` changed."); expect(body).toContain("`package.json` changed `dependencies`."); expect(body).toContain( "git checkout 'origin/main' -- 'pnpm-lock.yaml' 'extensions/slack/npm-shrinkwrap.json'", ); expect(body).toContain("/allow-dependencies-change"); expect(body).toContain(`current head SHA (\`${headSha}\`)`); expect(body).toContain("A later push requires a fresh approval."); }); it("shell-quotes PR-controlled paths in removal guidance", () => { const body = renderBlockedDependencyComment({ baseBranch: "release/canary branch", headSha, lockfileChanges: [ "dir with spaces/pnpm-lock.yaml", "safe/quote'$(touch bad);/package-lock.json", ], dependencyManifestChanges: [], }); expect(body).toContain( "git checkout 'origin/release/canary branch' -- 'dir with spaces/pnpm-lock.yaml' 'safe/quote'\\''$(touch bad);/package-lock.json'", ); }); it("autoscrubs only lockfile changes with no dependency manifest changes", () => { expect( shouldAutoscrubDependencyLockfiles({ dependencyFiles: ["pnpm-lock.yaml"], lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], }), ).toBe(true); expect( shouldAutoscrubDependencyLockfiles({ dependencyFiles: ["pnpm-lock.yaml"], lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [{ path: "package.json", fields: ["dependencies"] }], }), ).toBe(false); expect( shouldAutoscrubDependencyLockfiles({ dependencyFiles: [], lockfileChanges: [], dependencyManifestChanges: [], }), ).toBe(false); expect( shouldAutoscrubDependencyLockfiles({ dependencyFiles: ["pnpm-lock.yaml", "patches/example.patch"], lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], }), ).toBe(false); expect( shouldAutoscrubDependencyLockfiles({ dependencyFiles: ["pnpm-lock.yaml", "pnpm-workspace.yaml"], lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], }), ).toBe(false); }); it("attempts autoscrub on PR branches maintainers can modify", () => { const sameRepoPullRequest = { head: { ref: "contributor/change", repo: { full_name: "openclaw/openclaw" }, sha: headSha, }, }; const forkPullRequest = { head: { ref: "contributor/change", repo: { full_name: "external/openclaw" }, sha: headSha, }, }; const editableForkPullRequest = { maintainer_can_modify: true, head: { ref: "contributor/change", repo: { full_name: "external/openclaw" }, sha: headSha, }, }; expect( canAutoscrubPullRequest({ owner: "openclaw", repo: "openclaw", pullRequest: sameRepoPullRequest, }), ).toBe(true); expect( canAutoscrubPullRequest({ owner: "openclaw", repo: "openclaw", pullRequest: forkPullRequest, }), ).toBe(false); expect( canAutoscrubPullRequest({ owner: "openclaw", repo: "openclaw", pullRequest: editableForkPullRequest, }), ).toBe(true); }); it("renders deterministic autoscrub success comments", () => { const body = renderAutoscrubbedDependencyComment({ baseBranch: "main", commitSha: staleSha, lockfileChanges: ["pnpm-lock.yaml", "extensions/slack/npm-shrinkwrap.json"], }); expect(body).toContain(""); expect(body).toContain("Dependency lockfile changes were removed"); expect(body).toContain("did not change dependency graph fields in package manifests"); expect(body).toContain("`pnpm-lock.yaml`"); expect(body).toContain("`extensions/slack/npm-shrinkwrap.json`"); expect(body).toContain(`Cleanup commit: \`${staleSha}\``); expect(body).toContain( "restored each listed lockfile from the target branch and pushed the cleanup commit to this PR head", ); expect(body).toContain( "this PR no longer carries those package lockfile diffs after the cleanup commit", ); expect(isAutoscrubbedDependencyComment({ body })).toBe(true); }); it("renders fork and dependency-manifest autoscrub guidance", () => { const forkBody = renderBlockedDependencyComment({ baseBranch: "main", headSha, lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], autoscrubStatus: { kind: "not-attempted" }, }); const unsafeBody = renderBlockedDependencyComment({ baseBranch: "main", headSha, lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], autoscrubStatus: { kind: "blocked-by-dependency-manifest-fields", changes: [{ path: "package.json", fields: ["dependencies"] }], }, }); const mixedBody = renderBlockedDependencyComment({ baseBranch: "main", headSha, lockfileChanges: ["pnpm-lock.yaml"], dependencyManifestChanges: [], autoscrubStatus: { kind: "blocked-by-other-dependency-files", files: ["patches/example.patch", "pnpm-workspace.yaml"], }, }); expect(forkBody).toContain("Auto-scrub was not attempted"); expect(forkBody).toContain( "only push deterministic cleanup commits to PR branches that maintainers can modify", ); expect(unsafeBody).toContain("changes package manifest dependency graph fields"); expect(unsafeBody).toContain("`package.json` changed `dependencies`"); expect(unsafeBody).toContain("Dependency graph changes must be reviewed by security"); expect(mixedBody).toContain("also changes dependency-related files"); expect(mixedBody).toContain("`patches/example.patch`"); expect(mixedBody).toContain("`pnpm-workspace.yaml`"); }); it("reads base lockfiles with the base API before writing autoscrub commits", async () => { const calls: Array<{ api: string; path: string; variables?: unknown }> = []; const baseApi = { request: async (path: string) => { calls.push({ api: "base", path }); if (path.includes("/contents/pnpm-lock.yaml?")) { return { content: Buffer.from("base lockfile").toString("base64"), encoding: "base64", sha: "base-file", type: "file", }; } throw new Error(`unexpected base request: ${path}`); }, }; const writeApi = { graphql: async (_query: string, variables: unknown) => { calls.push({ api: "write", path: "graphql", variables }); return { createCommitOnBranch: { commit: { oid: staleSha } } }; }, }; const commit = await createAutoscrubCommit( { baseApi, writeApi }, { owner: "openclaw", repo: "openclaw", pullRequest: { base: { sha: "base-sha" }, head: { ref: "contributor/change", sha: headSha }, }, lockfileChanges: ["pnpm-lock.yaml"], targetRepository: { owner: "contributor", repo: "openclaw" }, }, ); expect(commit).toEqual({ sha: staleSha }); expect(calls.map((call) => `${call.api}:${call.path}`)).toEqual([ "base:/repos/openclaw/openclaw/contents/pnpm-lock.yaml?ref=base-sha", "write:graphql", ]); expect(calls[1].variables).toMatchObject({ input: { branch: { repositoryNameWithOwner: "contributor/openclaw", branchName: "contributor/change", }, expectedHeadOid: headSha, fileChanges: { additions: [ { contents: Buffer.from("base lockfile").toString("base64"), path: "pnpm-lock.yaml", }, ], deletions: [], }, }, }); }); it("renders a cleared guard comment that preserves approval freshness", () => { const body = renderClearedDependencyGuardComment({ headSha }); expect(body).toContain(""); expect(body).toContain("Dependency graph guard cleared"); expect(body).toContain(headSha); expect(body).toContain("requires a fresh `/allow-dependencies-change` comment"); }); it("parses explicit security approver allowlists", () => { expect(securityApproverSet("vincentkoc, steipete\njoshavant")).toEqual( new Set(["vincentkoc", "steipete", "joshavant"]), ); }); it("sanitizes display values", () => { expect(sanitizeDisplayValue("abc\u0000def")).toBe("abc?def"); expect(sanitizeDisplayValue("x".repeat(300))).toHaveLength(240); }); it("bounds GitHub error bodies by content-length", async () => { const response = new Response("ignored", { headers: { "content-length": String(GITHUB_ERROR_BODY_MAX_BYTES + 1) }, }); await expect(readBoundedGitHubErrorText(response)).rejects.toThrow( `GitHub error response body exceeded ${GITHUB_ERROR_BODY_MAX_BYTES} bytes`, ); }); it("bounds GitHub error bodies by streamed bytes", async () => { const response = new Response( new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(GITHUB_ERROR_BODY_MAX_BYTES + 1)); controller.close(); }, }), ); await expect(readBoundedGitHubErrorText(response)).rejects.toThrow( `GitHub error response body exceeded ${GITHUB_ERROR_BODY_MAX_BYTES} bytes`, ); }); it("preserves GitHub status when an error body exceeds the cap", async () => { const originalFetch = globalThis.fetch; globalThis.fetch = (() => Promise.resolve( new Response( new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(GITHUB_ERROR_BODY_MAX_BYTES + 1)); controller.close(); }, }), { status: 403, statusText: "Forbidden" }, ), )) as typeof fetch; try { await expect(githubApi("token").request("/repos/openclaw/openclaw")).rejects.toMatchObject({ message: `403 Forbidden: GitHub error response body exceeded ${GITHUB_ERROR_BODY_MAX_BYTES} bytes`, status: 403, }); } finally { globalThis.fetch = originalFetch; } }); });