import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { applyPatchSet, detectPackageManager, shouldApplyPnpmPatchedDependenciesFallback, } from "../scripts/postinstall.js"; function makeTempDir() { return fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-patch-")); } describe("postinstall patcher", () => { it("detects package manager from user agent", () => { expect(detectPackageManager("pnpm/10.0.0 npm/? node/v22.0.0")).toBe("pnpm"); expect(detectPackageManager("npm/10.9.0 node/v22.0.0")).toBe("npm"); expect(detectPackageManager("bun/1.2.2")).toBe("bun"); expect(detectPackageManager("yarn/4.0.0 npm/? node/v22.0.0")).toBe("yarn"); expect(detectPackageManager("")).toBe("unknown"); }); it("skips pnpm.patchedDependencies fallback for pnpm", () => { expect(shouldApplyPnpmPatchedDependenciesFallback("pnpm")).toBe(false); expect(shouldApplyPnpmPatchedDependenciesFallback("npm")).toBe(true); expect(shouldApplyPnpmPatchedDependenciesFallback("bun")).toBe(true); expect(shouldApplyPnpmPatchedDependenciesFallback("unknown")).toBe(true); }); it("applies a simple patch", () => { const dir = makeTempDir(); const target = path.join(dir, "lib"); fs.mkdirSync(target); const filePath = path.join(target, "main.js"); const original = `${[ "var QRCode = require('./../vendor/QRCode'),", " QRErrorCorrectLevel = require('./../vendor/QRCode/QRErrorCorrectLevel'),", ' black = "\\033[40m \\033[0m",', ' white = "\\033[47m \\033[0m",', " toCell = function (isBlack) {", ].join("\n")}\n`; fs.writeFileSync(filePath, original, "utf-8"); const patchText = `diff --git a/lib/main.js b/lib/main.js index 0000000..1111111 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,5 +1,5 @@ -var QRCode = require('./../vendor/QRCode'), - QRErrorCorrectLevel = require('./../vendor/QRCode/QRErrorCorrectLevel'), +var QRCode = require('./../vendor/QRCode/index.js'), + QRErrorCorrectLevel = require('./../vendor/QRCode/QRErrorCorrectLevel.js'), black = "\\033[40m \\033[0m", white = "\\033[47m \\033[0m", toCell = function (isBlack) { `; applyPatchSet({ patchText, targetDir: dir }); const updated = fs.readFileSync(filePath, "utf-8"); expect(updated).toBe( `${[ "var QRCode = require('./../vendor/QRCode/index.js'),", " QRErrorCorrectLevel = require('./../vendor/QRCode/QRErrorCorrectLevel.js'),", ' black = "\\033[40m \\033[0m",', ' white = "\\033[47m \\033[0m",', " toCell = function (isBlack) {", ].join("\n")}\n`, ); fs.rmSync(dir, { recursive: true, force: true }); }); it("handles multiple hunks with offsets", () => { const dir = makeTempDir(); const filePath = path.join(dir, "file.txt"); fs.writeFileSync( filePath, `${["alpha", "beta", "gamma", "delta", "epsilon"].join("\n")}\n`, "utf-8", ); const patchText = `diff --git a/file.txt b/file.txt --- a/file.txt +++ b/file.txt @@ -1,3 +1,4 @@ alpha beta +beta2 gamma @@ -3,3 +4,4 @@ gamma -delta +DELTA epsilon +zeta `; applyPatchSet({ patchText, targetDir: dir }); const updated = fs.readFileSync(filePath, "utf-8").trim().split("\n"); expect(updated).toEqual([ "alpha", "beta", "beta2", "gamma", "DELTA", "epsilon", "zeta", ]); fs.rmSync(dir, { recursive: true, force: true }); }); it("throws on context mismatch", () => { const dir = makeTempDir(); const filePath = path.join(dir, "file.txt"); fs.writeFileSync(filePath, "hello\nworld\n", "utf-8"); const patchText = `diff --git a/file.txt b/file.txt --- a/file.txt +++ b/file.txt @@ -1,2 +1,2 @@ -hola +hi world `; expect(() => applyPatchSet({ patchText, targetDir: dir })).toThrow(); fs.rmSync(dir, { recursive: true, force: true }); }); });