import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; import { chmod, copyFile, mkdtemp, mkdir, readFile, readdir, rename, rm, stat, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; import { GENTLE_AI_PENDING_DIGEST, INSTALLER_VERSION, GENTLE_AI_RELEASE_ASSETS, GENTLE_AI_WINDOWS_SOURCE_MODULE, GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH, GENTLE_AI_WINDOWS_SOURCE_TAG, GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, downloadGentleAiAsset, gentleAiAssetForm, installGentleAi, resolveGentleAiInstallerPackageRoot, resolveGentleAiReleaseAsset, trustedSystemExtractor, } from "../scripts/gentle-ai-installer.mjs"; // v4.3.1 archive digests independently match the minisign-signed release // checksums; binary digests were computed from the extracted executables. function releaseAssets(): Record { const assets: Record = {}; for (const [key, value] of Object.entries(GENTLE_AI_RELEASE_ASSETS)) { assert.ok(typeof value === "object" && value !== null); assert.ok("name" in value && typeof value.name === "string"); assert.ok("sha256" in value && typeof value.sha256 === "string"); assert.ok("binarySha256" in value && typeof value.binarySha256 === "string"); assert.ok("url" in value && typeof value.url === "string"); assets[key] = { name: value.name, sha256: value.sha256, binarySha256: value.binarySha256, url: value.url }; } return assets; } const EXPECTED_ASSETS = { "darwin/amd64": { name: "gentle-ai_4.3.1_darwin_amd64.tar.gz", sha256: "6cf57fee0574daef2abed07f54a355fec3b93ac8f01733dc6c4d3674f1746112", binarySha256: "ca44c3fce186b63a7e7ad455c338133ba1982f4a13485301ce4d66edb73d2928" }, "darwin/arm64": { name: "gentle-ai_4.3.1_darwin_arm64.tar.gz", sha256: "13e8828447f9081465ee0c35a61b24fb49296d51a310c6999227e2aed178956b", binarySha256: "749d08062b0dec13681678a1a1c018613d223ddcca47b5f27681b1d8ab5d14c2" }, "linux/amd64": { name: "gentle-ai_4.3.1_linux_amd64.tar.gz", sha256: "04ec685c56b42c29dff6db10fc7dc2c730217dc9c96736e53ad2acf3226dafb7", binarySha256: "86248792bde3fe5e9ca93b128c07e7caa6a5b31fac0f9fc84b5b3011b123cb90" }, "linux/arm64": { name: "gentle-ai_4.3.1_linux_arm64.tar.gz", sha256: "99e7d313a2ff85eaf23a6415e9b3aae67bf74c2fa169b21b93070ada7613f6a0", binarySha256: "df3580506bb5ea63cc4ef5ccebe8b68cfedf66b72f3b51f84ff393183d8cea2b" }, } as const; test("published Gentle AI v4.3.1 is the installer pin", () => { assert.equal(INSTALLER_VERSION, "4.3.1"); // v4.0.0 moved the Go module path to the /v4 major-version suffix. assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE, "github.com/gentleman-programming/gentle-ai/v4"); assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:00p8dnjJrhYFq1Qikk2oY6K/UXIty9A/IBUUOUiX9XA="); }); test("default installer package root is the package containing scripts, not its parent", () => { const installerPath = fileURLToPath(new URL("../scripts/gentle-ai-installer.mjs", import.meta.url)); const expectedPackageRoot = dirname(dirname(installerPath)); assert.equal(resolveGentleAiInstallerPackageRoot(), expectedPackageRoot); assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot)); }); test("release mapping selects only the supported official v4.3.1 assets and pinned digests", () => { assert.deepEqual( Object.fromEntries(Object.entries(releaseAssets()).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])), EXPECTED_ASSETS, ); assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.3.1_linux_amd64.tar.gz"); assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.3.1_darwin_arm64.tar.gz"); for (const asset of Object.values(releaseAssets())) { assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.3\.1\//); } }); test("raw release assets are admitted only under a prerelease pin", () => { assert.equal(gentleAiAssetForm("gentle-ai_2.4.0_linux_amd64.tar.gz", "2.4.0"), "archive"); assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_linux_amd64", "2.5.0-rc.3"), "raw-binary"); assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary"); // A raw binary under a stable pin means the pin itself is wrong: stable // releases publish signed archives only, so this fails closed pre-download. assert.throws(() => gentleAiAssetForm("gentle-ai_4.3.1_linux_amd64", "4.3.1"), /only admitted for a prerelease pin/); assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/); // The current stable pin admits every pinned asset row through the same // gate the installer uses at download time (default installerVersion // argument): signed archives, never raw binaries. for (const asset of Object.values(releaseAssets())) { assert.equal(gentleAiAssetForm(asset.name), "archive"); } }); test("release digests are all-or-none and install fails closed while any digest is pending", async () => { const digests = Object.values(releaseAssets()).flatMap((asset) => [asset.sha256, asset.binarySha256]); const pinned = digests.filter((digest) => /^[0-9a-f]{64}$/.test(digest)); const pending = digests.filter((digest) => digest === GENTLE_AI_PENDING_DIGEST); assert.equal(pinned.length + pending.length, digests.length, "every digest must be pinned hex or the explicit pending sentinel"); assert.equal(pinned.length === digests.length || pending.length === digests.length, true, "digest table must not mix pinned and pending entries"); if (pending.length === digests.length) { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-pending-")); await assert.rejects( () => installGentleAi({ packageRoot, platform: "linux", arch: "x64", download: async (_url, destination) => writeFile(destination, "unverifiable archive"), }), /checksum mismatch/, ); assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai")), false); } }); interface WindowsGoCall { file: string; arguments_: string[]; options: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: boolean; timeout?: number; maxBuffer?: number; }; } interface WindowsGoFixtureOptions { goVersion?: string; goVersionError?: Error; reportedVersion?: string; installError?: Error; goArchitecture?: "amd64" | "arm64"; } function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) { const calls: WindowsGoCall[] = []; let goExecutable = "go"; const metadata = [ "gentle-ai.exe: go1.25.10", "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai", `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.3.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`, "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows", ].join("\n"); const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => { calls.push({ file, arguments_, options }); if (file === goExecutable && arguments_.length === 1 && arguments_[0] === "version") { if (fixtureOptions.goVersionError) throw fixtureOptions.goVersionError; return { stdout: fixtureOptions.goVersion ?? "go version go1.25.10 windows/amd64\n", stderr: "" }; } if (file === goExecutable && arguments_[0] === "install") { if (fixtureOptions.installError) throw fixtureOptions.installError; const gobin = options.env?.GOBIN; assert.ok(gobin, "go install must receive an explicit staging GOBIN"); await mkdir(gobin, { recursive: true }); await writeFile(join(gobin, "gentle-ai.exe"), "trusted Windows source build"); return { stdout: "", stderr: "" }; } if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" }; if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.3.1\n", stderr: "" }; throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`); }; return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } }; } test("win32 x64 and arm64 install the exact Go SumDB source tag without archive lookup", async () => { for (const [arch, goArchitecture] of [["x64", "amd64"], ["arm64", "arm64"]] as const) { const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-${arch}-`)); const fixture = windowsGoFixture({ goArchitecture }); const goPath = join(packageRoot, "go.exe"); await writeFile(goPath, "trusted local Go executable"); fixture.setGoExecutable(goPath); const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath }); assert.equal(result.installed, true); assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.3.1"], ["version", "-m"], ]); } }); test("Windows source install reports missing or too-old Go without publishing a binary", async () => { for (const fixtureOptions of [ { goVersion: "go version go1.25.9 windows/amd64\n", expectedCode: "GENTLE_AI_GO_TOOLCHAIN_TOO_OLD" }, { goVersionError: Object.assign(new Error("go was not found"), { code: "ENOENT" }), expectedCode: "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE" }, ] as const) { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-go-")); const fixture = windowsGoFixture(fixtureOptions); const goPath = join(packageRoot, "go.exe"); await writeFile(goPath, "trusted local Go executable"); fixture.setGoExecutable(goPath); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }), (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode, ); assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai.exe")), false); assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []); } }); test("a failed Windows go install keeps the go command's error, with its stdout and stderr, as the cause", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-cause-")); try { const installError = Object.assign(new Error("Command failed: go.exe install\ngo: verifying module: checksum mismatch"), { code: 1, stdout: "", stderr: "go: verifying module: checksum mismatch\n" }); const fixture = windowsGoFixture({ installError }); const goPath = join(packageRoot, "go.exe"); await writeFile(goPath, "trusted local Go executable"); fixture.setGoExecutable(goPath); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }), (error: unknown) => error instanceof Error && error.cause === installError && (error.cause as { stderr: string }).stderr.includes("checksum mismatch"), ); } finally { await rm(packageRoot, { recursive: true, force: true }); } }); // Run 38063142924: from a 179-character pnpm store package root, Go's asm.exe failed with // "The directory name is invalid." because its working directory, inside GOMODCACHE, exceeded MAX_PATH. test("Windows source install builds in a short private directory under the temp root, never inside the package, and removes it", async () => { for (const installError of [undefined, Object.assign(new Error("go install failed"), { code: 1, stderr: "go: failed\n" })]) { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-short-build-")); const temporaryDirectory = await mkdtemp(join(tmpdir(), "gentle-pi-installer-temp-")); try { const fixture = windowsGoFixture({ installError }); const goPath = join(packageRoot, "go.exe"); await writeFile(goPath, "trusted local Go executable"); fixture.setGoExecutable(goPath); const install = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath, temporaryDirectory }); if (installError) await assert.rejects(install, (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED"); else assert.equal((await install).installed, true); const goInstall = fixture.calls.find((call) => call.arguments_[0] === "install"); assert.ok(goInstall); const buildDirectory = goInstall.options.cwd ?? ""; assert.match(buildDirectory.slice(temporaryDirectory.length), /^[\\/]gai-[^\\/]+$/, "a fresh gai- directory directly under the temp root"); const env = goInstall.options.env ?? {}; for (const key of ["GOBIN", "GOPATH", "GOMODCACHE", "GOCACHE", "TEMP", "TMP"]) { assert.ok(env[key]?.startsWith(buildDirectory), `${key} is under the build directory`); assert.equal(env[key]?.startsWith(packageRoot), false, `${key} is outside the package`); } for (const call of fixture.calls.filter((entry) => entry.file === goPath)) assert.ok(call.options.cwd?.startsWith(buildDirectory), call.arguments_.join(" ")); assert.deepEqual(await readdir(temporaryDirectory), [], "the build directory is removed"); if (!installError) assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai.exe")), true, "the verified binary is published into the package"); } finally { await rm(packageRoot, { recursive: true, force: true }); await rm(temporaryDirectory, { recursive: true, force: true }); } } }); test("Windows source install cleans staging after Go failure or wrong built version", async () => { for (const fixtureOptions of [ { installError: Object.assign(new Error("go install timed out"), { code: "ETIMEDOUT" }), expectedCode: "GENTLE_AI_GO_INSTALL_FAILED" }, { reportedVersion: "gentle-ai 2.2.1\n", expectedCode: "GENTLE_AI_VERSION_MISMATCH" }, ] as const) { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-cleanup-")); const fixture = windowsGoFixture(fixtureOptions); const goPath = join(packageRoot, "go.exe"); await writeFile(goPath, "trusted local Go executable"); fixture.setGoExecutable(goPath); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }), (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode, ); const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.3.1"); assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install")); assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false); assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false); } }); test("Windows source installs reuse only a fully verified package-local binary", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-reuse-")); const goPath = join(packageRoot, "go.exe"); await writeFile(goPath, "trusted local Go executable"); const initial = windowsGoFixture(); initial.setGoExecutable(goPath); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: async () => goPath }); const reuse = windowsGoFixture(); reuse.setGoExecutable(goPath); const reused = await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: async () => goPath }); assert.equal(reused.installed, false); assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH"); await writeFile(join(packageRoot, ".gentle-ai", "v4.3.1", "integrity.json"), "{}\n"); const repaired = windowsGoFixture(); repaired.setGoExecutable(goPath); assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true); assert.ok(repaired.calls.some((call) => call.arguments_[0] === "install")); }); interface HardenedGoCall { file: string; arguments_: string[]; options: WindowsGoCall["options"] & { cwd?: string }; } const GO_ARCHITECTURE = { AMD64: "amd64", ARM64: "arm64", } as const; type GoArchitecture = (typeof GO_ARCHITECTURE)[keyof typeof GO_ARCHITECTURE]; interface HardenedGoFixtureOptions { architecture?: GoArchitecture; blockInstall?: boolean; metadataOverride?: string; } async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: HardenedGoFixtureOptions = {}) { const goDirectory = join(packageRoot, "trusted-go"); const goPath = join(goDirectory, "go.exe"); await mkdir(goDirectory, { recursive: true }); await writeFile(goPath, "trusted local Go executable"); const calls: HardenedGoCall[] = []; let resolveCalls = 0; let signalInstallStarted: (() => void) | undefined; const installStarted = new Promise((resolve) => { signalInstallStarted = resolve; }); let releaseInstall: (() => void) | undefined; const installGate = fixtureOptions.blockInstall ? new Promise((resolve) => { releaseInstall = resolve; }) : undefined; const metadata = fixtureOptions.metadataOverride ?? [ "gentle-ai.exe: go1.25.10", "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai", `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.3.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`, "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.architecture ?? "amd64"}`, "\tbuild\tGOOS=windows", ].join("\n"); const run = async (file: string, arguments_: string[], options: HardenedGoCall["options"]) => { calls.push({ file, arguments_, options }); if (file === goPath && arguments_.length === 1 && arguments_[0] === "version") return { stdout: "go version go1.25.10 windows/amd64\n", stderr: "" }; if (file === goPath && arguments_[0] === "install") { signalInstallStarted?.(); await installGate; const gobin = options.env?.GOBIN; assert.ok(gobin); await mkdir(gobin, { recursive: true }); await writeFile(join(gobin, "gentle-ai.exe"), "trusted Windows source build"); return { stdout: "", stderr: "" }; } if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" }; if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.3.1\n", stderr: "" }; throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`); }; return { calls, goPath, resolveGoExecutable: async () => { resolveCalls += 1; return goPath; }, resolveCalls: () => resolveCalls, waitForInstall: () => installStarted, releaseInstall: () => releaseInstall?.(), run, }; } test("Windows source installation resolves one validated Go executable and seals hostile Go environment", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-hardened-go-")); const fixture = await hardenedWindowsGoFixture(packageRoot); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, env: { GOENV: "C:\\attacker\\goenv", GOFLAGS: "-mod=mod", GOWORK: "C:\\attacker\\go.work", GOPROXY: "http://attacker.invalid", GOBIN: "C:\\attacker\\bin", GOPATH: "C:\\attacker\\path", GOMODCACHE: "C:\\attacker\\mods", GOCACHE: "C:\\attacker\\cache", GOOS: "linux", GOARCH: "386", PATH: "C:\\attacker", UNRELATED_SECRET: "must-not-reach-go", }, }); assert.equal(fixture.resolveCalls(), 1); const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath); assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.3.1"], ["version", "-m"], ]); for (const call of goCalls) { assert.equal(call.options.shell, false); assert.equal(call.options.env?.GOENV, "off"); assert.equal(call.options.env?.GOFLAGS, ""); assert.equal(call.options.env?.GOWORK, "off"); assert.equal(call.options.env?.GOTOOLCHAIN, "local"); assert.equal(call.options.env?.GOSUMDB, "sum.golang.org"); assert.equal(call.options.env?.GONOSUMDB, ""); assert.equal(call.options.env?.GOPRIVATE, ""); assert.equal(call.options.env?.GONOPROXY, ""); assert.equal(call.options.env?.GOINSECURE, ""); assert.equal(call.options.env?.GOPROXY, "https://proxy.golang.org"); assert.equal(call.options.env?.GOOS, "windows"); assert.equal(call.options.env?.GOARCH, "amd64"); assert.equal(call.options.env?.UNRELATED_SECRET, undefined); assert.doesNotMatch(call.options.env?.PATH ?? "", /attacker/i); } }); test("Windows source manifest binds verified Go metadata and architecture", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-")); const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" }); const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }); const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.3.1", "integrity.json"), "utf8")) as Record; assert.equal(result.installed, true); assert.deepEqual(manifest, { version: "4.3.1", method: "go-sumdb-source-build", package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai", module: "github.com/gentleman-programming/gentle-ai/v4", tag: "v4.3.1", architecture: "arm64", binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"), moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, goVersion: "go1.25.10", goos: "windows", goarch: "arm64", buildMode: "exe", compiler: "gc", cgoEnabled: "0", }); }); test("Windows source installation rejects Go metadata for a different architecture", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-architecture-")); const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" }); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }), (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED", ); assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai.exe")), false); }); test("Windows source installation treats a fresh ownerless lock as active", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-")); const lockPath = join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock"); await mkdir(lockPath, { recursive: true }); const fixture = await hardenedWindowsGoFixture(packageRoot); await assertManualLockRecoveryRequired(packageRoot, fixture); }); test("Windows source installation preserves a lock whose owner nonce changed before release", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-")); const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock", "owner.json"); const fixture = await hardenedWindowsGoFixture(packageRoot); let replacedOwner = false; const run = async (...arguments_: Parameters) => { const result = await fixture.run(...arguments_); if (!replacedOwner && arguments_[0] !== fixture.goPath && arguments_[1].length === 1 && arguments_[1][0] === "version") { replacedOwner = true; await writeFile(lockOwnerPath, `${JSON.stringify({ createdAt: Date.now(), nonce: "1".repeat(64) })}\n`); } return result; }; await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: run, resolveGoExecutable: fixture.resolveGoExecutable }); assert.equal(replacedOwner, true); assert.equal((await readFile(lockOwnerPath, "utf8")).includes("1".repeat(64)), true); }); async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited>, options: Record = {}) { const lockPath = join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock"); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }), (error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message), ); assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false); assert.equal(existsSync(lockPath), true); } function tombstonePath(packageRoot: string, nonce: string): string { return join(packageRoot, ".gentle-ai", `.v4.3.1.install.tombstone-${nonce}`); } async function tombstones(packageRoot: string): Promise { const runtimeRoot = join(packageRoot, ".gentle-ai"); return existsSync(runtimeRoot) ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.3.1.install.tombstone-")) : []; } function backupBundlePath(packageRoot: string, nonce: string): string { return join(packageRoot, ".gentle-ai", `.v4.3.1.backup-${nonce}`); } async function copyWindowsBundle(source: string, destination: string): Promise { await mkdir(destination, { recursive: true }); await copyFile(join(source, "gentle-ai.exe"), join(destination, "gentle-ai.exe")); await copyFile(join(source, "integrity.json"), join(destination, "integrity.json")); } test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-")); const lockPath = join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock"); await mkdir(lockPath, { recursive: true }); const fixture = await hardenedWindowsGoFixture(packageRoot); await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 }); }); test("Windows source installation fails closed for a stale owner lock", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-")); const lockPath = join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock"); await mkdir(lockPath, { recursive: true }); await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`); const fixture = await hardenedWindowsGoFixture(packageRoot); await assertManualLockRecoveryRequired(packageRoot, fixture); }); test("Windows source release moves a replacement lock to a preserved tombstone", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-release-tombstone-")); const fixture = await hardenedWindowsGoFixture(packageRoot); const replacementNonce = "2".repeat(64); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, beforeInstallLockReleaseRename: async (lockPath: string) => writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: Date.now(), nonce: replacementNonce })}\n`), }), /package-private installation lock or tombstone exists/, ); const preserved = await tombstones(packageRoot); assert.equal(preserved.length, 1); const preservedPath = join(packageRoot, ".gentle-ai", preserved[0]); assert.equal((await readFile(join(preservedPath, "owner.json"), "utf8")).includes(replacementNonce), true); const retry = await hardenedWindowsGoFixture(packageRoot); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: retry.run, resolveGoExecutable: retry.resolveGoExecutable }), (error: unknown) => error instanceof Error && error.message.includes(preservedPath), ); }); test("Windows source acquisition fails closed when a tombstone appears after its first scan", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-acquire-tombstone-")); const fixture = await hardenedWindowsGoFixture(packageRoot); const foreignTombstone = tombstonePath(packageRoot, "3".repeat(64)); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, afterInstallLockFirstTombstoneScan: async () => { await mkdir(foreignTombstone, { recursive: true }); await writeFile(join(foreignTombstone, "owner.json"), `${JSON.stringify({ createdAt: Date.now(), nonce: "3".repeat(64) })}\n`); }, }), (error: unknown) => error instanceof Error && error.message.includes(foreignTombstone), ); assert.equal(existsSync(foreignTombstone), true); assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock")), false); assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false); }); test("Windows source release deletes its matching tombstone and allows reuse", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-")); const fixture = await hardenedWindowsGoFixture(packageRoot); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }); assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock")), false); assert.deepEqual(await tombstones(packageRoot), []); const reuse = await hardenedWindowsGoFixture(packageRoot); assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false); }); test("Windows source recovers a valid backup after a crash between publication renames before any build", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-")); const initial = await hardenedWindowsGoFixture(packageRoot); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable }); const live = join(packageRoot, ".gentle-ai", "v4.3.1"); const backup = backupBundlePath(packageRoot, "crash"); await rename(live, backup); const recovery = await hardenedWindowsGoFixture(packageRoot); let buildAttempted = false; const run = async (...arguments_: Parameters) => { if (arguments_[1][0] === "install") { buildAttempted = true; throw new Error("recovery must precede a new build"); } return recovery.run(...arguments_); }; assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: run, resolveGoExecutable: recovery.resolveGoExecutable })).installed, false); assert.equal(existsSync(live), true); assert.equal(existsSync(backup), false); assert.equal(buildAttempted, false); }); test("Windows source fails closed for multiple or invalid backup candidates", async () => { for (const setup of ["multiple", "invalid"] as const) { const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-backup-${setup}-`)); const first = backupBundlePath(packageRoot, "first"); await mkdir(first, { recursive: true }); if (setup === "multiple") await mkdir(backupBundlePath(packageRoot, "second"), { recursive: true }); else await writeFile(join(first, "not-a-bundle"), "invalid"); const fixture = await hardenedWindowsGoFixture(packageRoot); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }), /bundle recovery.*manual/i, ); assert.equal(existsSync(first), true); assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false); } }); test("Windows source cleans one validated backup only when the live bundle is valid", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-")); const initial = await hardenedWindowsGoFixture(packageRoot); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable }); const live = join(packageRoot, ".gentle-ai", "v4.3.1"); const backup = backupBundlePath(packageRoot, "valid"); await copyWindowsBundle(live, backup); const reuse = await hardenedWindowsGoFixture(packageRoot); assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false); assert.equal(existsSync(live), true); assert.equal(existsSync(backup), false); }); test("Windows source preserves a valid backup when the live bundle is invalid", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-")); const initial = await hardenedWindowsGoFixture(packageRoot); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable }); const live = join(packageRoot, ".gentle-ai", "v4.3.1"); const backup = backupBundlePath(packageRoot, "valid"); await copyWindowsBundle(live, backup); await writeFile(join(live, "integrity.json"), "{}\n"); const fixture = await hardenedWindowsGoFixture(packageRoot); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }), /bundle recovery.*manual/i, ); assert.equal(existsSync(backup), true); assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false); }); test("Windows concurrent installs fail closed until normal release, then reuse the published bundle", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-concurrent-")); const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true }); const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }); await fixture.waitForInstall(); const lockPath = join(packageRoot, ".gentle-ai", ".v4.3.1.install.lock"); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }), (error: unknown) => error instanceof Error && error.message.includes(lockPath), ); assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1); fixture.releaseInstall(); assert.equal((await first).installed, true); assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable })).installed, false); assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1); }); test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-")); const versionDirectory = join(packageRoot, ".gentle-ai", "v4.3.1"); await mkdir(versionDirectory, { recursive: true }); await writeFile(join(versionDirectory, "old.txt"), "previous bundle"); const fixture = await hardenedWindowsGoFixture(packageRoot); await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (to === versionDirectory && from.includes(".staging-")) throw new Error("simulated final swap failure"); await rename(from, to); }, }), /simulated final swap failure/, ); assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle"); }); function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); } function expectedWindowsSourceManifest(architecture: "x64" | "arm64") { return { version: INSTALLER_VERSION, method: "go-sumdb-source-build", package: GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH, module: GENTLE_AI_WINDOWS_SOURCE_MODULE, tag: GENTLE_AI_WINDOWS_SOURCE_TAG, architecture, binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"), moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, goVersion: "go1.25.10", goos: "windows", goarch: architecture === "x64" ? "amd64" : "arm64", buildMode: "exe", compiler: "gc", cgoEnabled: "0", }; } async function publicationLeftovers(packageRoot: string) { const runtimeRoot = join(packageRoot, ".gentle-ai"); return existsSync(runtimeRoot) ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.staging-`) || entry.startsWith(`.v${INSTALLER_VERSION}.backup-`) || entry.startsWith(`.v${INSTALLER_VERSION}.install.`)) : []; } async function waitForCount(getCount: () => number, expected: number, label: string) { const deadline = Date.now() + 5_000; while (Date.now() < deadline) { if (getCount() >= expected) return; await new Promise((resolve) => setImmediate(resolve)); } throw new Error(`timed out waiting for ${label} (${getCount()} < ${expected})`); } async function settleWithMockedTimers(t: { mock: { timers: { tick(milliseconds: number): void } } }, operation: Promise): Promise { let done = false; let value: T | undefined; let failure: { error: unknown } | undefined; operation.then( (resolved) => { value = resolved; done = true; }, (error) => { failure = { error }; done = true; }, ); const deadline = Date.now() + 5_000; while (!done && Date.now() < deadline) { await new Promise((resolve) => setImmediate(resolve)); if (!done) t.mock.timers.tick(1_600); } if (!done) throw new Error("install did not settle under mocked publication timers"); if (failure) throw failure.error; return value as T; } test("Windows source publication retries a transient staging lock then reuses the published bundle", async () => { for (const [arch, goArchitecture] of [["x64", "amd64"], ["arm64", "arm64"]] as const) { const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-retry-${arch}-`)); const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: goArchitecture }); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); let stagingAttempts = 0; const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from.includes(".staging-") && to === versionDirectory) { stagingAttempts += 1; if (stagingAttempts === 1) throw codedError("EPERM", "simulated staging lock"); } await rename(from, to); }, }); assert.equal(result.installed, true); assert.equal(stagingAttempts, 2); assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1); assert.equal(await readFile(join(versionDirectory, "gentle-ai.exe"), "utf8"), "trusted Windows source build"); assert.equal(await readFile(join(versionDirectory, "integrity.json"), "utf8"), `${JSON.stringify(expectedWindowsSourceManifest(arch))}\n`); assert.deepEqual(await publicationLeftovers(packageRoot), []); const reuse = await hardenedWindowsGoFixture(packageRoot, { architecture: goArchitecture }); assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch, execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false); assert.equal(reuse.calls.some((call) => call.arguments_[0] === "install"), false); } }); test("Windows publication retries EPERM, EBUSY, and EACCES through five attempts with increasing delays", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const delays = [200, 400, 800, 1600]; for (const code of ["EPERM", "EBUSY", "EACCES"] as const) { const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-retry-budget-${code}-`)); const fixture = await hardenedWindowsGoFixture(packageRoot); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); let stagingAttempts = 0; const install = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from.includes(".staging-") && to === versionDirectory) { stagingAttempts += 1; if (stagingAttempts < 5) throw codedError(code, `simulated ${code} lock`); } await rename(from, to); }, }); for (let attempt = 1; attempt <= 4; attempt += 1) { await waitForCount(() => stagingAttempts, attempt, `${code} attempt ${attempt}`); t.mock.timers.tick(delays[attempt - 1] - 1); await new Promise((resolve) => setImmediate(resolve)); assert.equal(stagingAttempts, attempt, `${code} must not retry before ${delays[attempt - 1]}ms`); t.mock.timers.tick(1); } await waitForCount(() => stagingAttempts, 5, `${code} fifth attempt`); assert.equal((await install).installed, true, `${code} must succeed on the fifth attempt without a further delay`); assert.equal(stagingAttempts, 5); assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1); } }); test("Windows source replacement retries transient backup and staging locks then removes the backup", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-replace-retry-")); const initial = await hardenedWindowsGoFixture(packageRoot); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable }); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); await writeFile(join(versionDirectory, "integrity.json"), "{}\n"); const fixture = await hardenedWindowsGoFixture(packageRoot); let backupAttempts = 0; let stagingAttempts = 0; const result = await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from === versionDirectory && to.includes(".backup-")) { backupAttempts += 1; if (backupAttempts === 1) throw codedError("EPERM", "simulated backup lock"); } if (from.includes(".staging-") && to === versionDirectory) { stagingAttempts += 1; if (stagingAttempts === 1) throw codedError("EBUSY", "simulated staging lock"); } await rename(from, to); }, }); assert.equal(result.installed, true); assert.equal(backupAttempts, 2); assert.equal(stagingAttempts, 2); assert.equal(await readFile(join(versionDirectory, "gentle-ai.exe"), "utf8"), "trusted Windows source build"); assert.equal(await readFile(join(versionDirectory, "integrity.json"), "utf8"), `${JSON.stringify(expectedWindowsSourceManifest("x64"))}\n`); assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false); }); test("Windows source publication restores the prior bundle after exhausting a staging lock, retrying a transient rollback", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-permanent-lock-")); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); await mkdir(versionDirectory, { recursive: true }); await writeFile(join(versionDirectory, "old.txt"), "previous bundle"); const fixture = await hardenedWindowsGoFixture(packageRoot); const stagingErrors: Error[] = []; let rollbackAttempts = 0; await assert.rejects( settleWithMockedTimers(t, installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from.includes(".staging-") && to === versionDirectory) { const error = codedError("EPERM", `simulated staging lock ${stagingErrors.length + 1}`); stagingErrors.push(error); throw error; } if (from.includes(".backup-") && to === versionDirectory) { rollbackAttempts += 1; if (rollbackAttempts === 1) throw codedError("EBUSY", "simulated rollback lock"); } await rename(from, to); }, })), (error: unknown) => error === stagingErrors[4], ); assert.equal(stagingErrors.length, 5); assert.equal(rollbackAttempts, 2); assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle"); assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false); assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false); }); test("Windows source publication leaves the live bundle untouched when the backup move is exhausted", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-backup-exhausted-")); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); await mkdir(versionDirectory, { recursive: true }); await writeFile(join(versionDirectory, "old.txt"), "previous bundle"); const fixture = await hardenedWindowsGoFixture(packageRoot); const backupErrors: Error[] = []; await assert.rejects( settleWithMockedTimers(t, installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from === versionDirectory && to.includes(".backup-")) { const error = codedError("EPERM", `simulated backup lock ${backupErrors.length + 1}`); backupErrors.push(error); throw error; } await rename(from, to); }, })), (error: unknown) => error === backupErrors[4], ); assert.equal(backupErrors.length, 5); assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle"); assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false); }); test("Windows source publication retains the backup when rollback retries are exhausted", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-rollback-exhausted-")); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); await mkdir(versionDirectory, { recursive: true }); await writeFile(join(versionDirectory, "old.txt"), "previous bundle"); const fixture = await hardenedWindowsGoFixture(packageRoot); const rollbackErrors: Error[] = []; await assert.rejects( settleWithMockedTimers(t, installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from.includes(".staging-") && to === versionDirectory) throw codedError("EPERM", "simulated staging lock"); if (from.includes(".backup-") && to === versionDirectory) { const error = codedError("EACCES", `simulated rollback lock ${rollbackErrors.length + 1}`); rollbackErrors.push(error); throw error; } await rename(from, to); }, })), (error: unknown) => error instanceof Error && error.message === "Gentle AI bundle publication failed and rollback could not restore the prior bundle" && error.cause === rollbackErrors[4], ); assert.equal(rollbackErrors.length, 5); assert.equal(existsSync(versionDirectory), false); const backups = (await readdir(join(packageRoot, ".gentle-ai"))).filter((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)); assert.equal(backups.length, 1); assert.equal(await readFile(join(packageRoot, ".gentle-ai", backups[0], "old.txt"), "utf8"), "previous bundle"); }); test("Windows source restores a valid backup through a transient lock on the next install", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-restore-retry-")); const initial = await hardenedWindowsGoFixture(packageRoot); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable }); const live = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); const backup = backupBundlePath(packageRoot, "restore"); await rename(live, backup); const recovery = await hardenedWindowsGoFixture(packageRoot); let restoreAttempts = 0; let buildAttempted = false; const run = async (...arguments_: Parameters) => { if (arguments_[1][0] === "install") { buildAttempted = true; throw new Error("recovery must precede a new build"); } return recovery.run(...arguments_); }; const restored = await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: run, resolveGoExecutable: recovery.resolveGoExecutable, rename: async (from: string, to: string) => { if (from.includes(".backup-") && to === live) { restoreAttempts += 1; if (restoreAttempts === 1) throw codedError("EPERM", "simulated restore lock"); } await rename(from, to); }, }); assert.equal(restored.installed, false); assert.equal(restoreAttempts, 2); assert.equal(existsSync(live), true); assert.equal(existsSync(backup), false); assert.equal(buildAttempted, false); assert.equal(await readFile(join(live, "gentle-ai.exe"), "utf8"), "trusted Windows source build"); }); test("Windows publication does not retry ENOSPC or uncoded rename failures", async () => { for (const failure of [codedError("ENOSPC", "simulated disk full"), new Error("simulated uncoded failure")]) { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-nonretryable-")); const fixture = await hardenedWindowsGoFixture(packageRoot); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); let stagingAttempts = 0; await assert.rejects( () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from.includes(".staging-") && to === versionDirectory) { stagingAttempts += 1; throw failure; } await rename(from, to); }, }), (error: unknown) => error === failure, ); assert.equal(stagingAttempts, 1); assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false); } }); test("Darwin/Linux signed publication fails on EPERM after one attempt", async () => { const payload = Buffer.from("signed archive fixture"); const binary = "signed binary"; for (const [platform, key] of [["darwin", "darwin/amd64"], ["linux", "linux/amd64"]] as const) { const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-signed-noretry-${platform}-`)); const asset = { name: `gentle-ai_${INSTALLER_VERSION}_${platform}_amd64.tar.gz`, sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai", }; const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); let stagingAttempts = 0; await assert.rejects( () => installGentleAi({ packageRoot, platform, arch: "x64", releaseAssets: { [key]: asset }, download: async (_url: string, destination: string) => writeFile(destination, payload), extractArchive: async (_archive: string, destination: string) => { await mkdir(destination, { recursive: true }); await writeFile(join(destination, "gentle-ai"), binary); }, rename: async (from: string, to: string) => { if (from.includes(".staging-") && to === versionDirectory) { stagingAttempts += 1; throw codedError("EPERM", "simulated staging lock"); } await rename(from, to); }, }), (error: unknown) => error instanceof Error && (error as NodeJS.ErrnoException).code === "EPERM", ); assert.equal(stagingAttempts, 1, platform); assert.equal(existsSync(join(versionDirectory, "gentle-ai")), false); } }); test("Windows source install with a persistent publication lock cleans staging without pretending a runtime exists", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-persistent-lock-")); const fixture = await hardenedWindowsGoFixture(packageRoot); const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`); const stagingErrors: Error[] = []; await assert.rejects( settleWithMockedTimers(t, installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, rename: async (from: string, to: string) => { if (from.includes(".staging-") && to === versionDirectory) { const error = codedError("EPERM", `simulated staging lock ${stagingErrors.length + 1}`); stagingErrors.push(error); throw error; } await rename(from, to); }, })), (error: unknown) => error === stagingErrors[4], ); assert.equal(stagingErrors.length, 5); assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false); assert.equal(existsSync(versionDirectory), false); assert.deepEqual(await publicationLeftovers(packageRoot), []); }); test("Darwin/Linux signed bundles retain their four-field manifest and reusable compatibility", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-signed-compatibility-")); const payload = Buffer.from("signed archive fixture"); const asset = { name: "gentle-ai_2.4.0_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" }; const options = { packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": asset }, download: async (_url: string, destination: string) => writeFile(destination, payload), extractArchive: async (_archive: string, destination: string) => { await mkdir(destination, { recursive: true }); await writeFile(join(destination, "gentle-ai"), "signed binary"); }, }; await installGentleAi(options); const manifestPath = join(packageRoot, ".gentle-ai", "v4.3.1", "integrity.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record; assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]); assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false); }); test("a pinned asset falls back to its gentle-pi mirror only on download failure, and a stable pin declares no mirror", async () => { // The mirror fallback is a property of the pinned row, not of the asset // form: a prerelease row carries a mirrorUrl, a stable row never does. // Drive the fallback through an archive-form asset so the same test holds // under the current stable pin, whose form gate refuses raw binaries. const payload = Buffer.from("signed archive fixture"); const binary = "mirrored binary"; const baseAsset = { name: "gentle-ai_4.3.1_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" }; const extractArchive = async (_archive: string, destination: string) => { await mkdir(destination, { recursive: true }); await writeFile(join(destination, "gentle-ai"), binary); }; const attempted: string[] = []; const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-mirror-")); const result = await installGentleAi({ packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": baseAsset }, extractArchive, download: async (url: string, destination: string) => { attempted.push(url); if (url === baseAsset.url) throw new Error("upstream asset deleted"); await writeFile(destination, payload); }, }); assert.equal(result.installed, true); assert.deepEqual(attempted, [baseAsset.url, baseAsset.mirrorUrl]); // Integrity beats availability: wrong bytes from the upstream source fail // immediately and the mirror is never consulted. const mismatched: string[] = []; await assert.rejects(installGentleAi({ packageRoot: await mkdtemp(join(tmpdir(), "gentle-pi-installer-mirror-mismatch-")), platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": baseAsset }, extractArchive, download: async (url: string, destination: string) => { mismatched.push(url); await writeFile(destination, "tampered bytes"); }, }), /checksum mismatch/); assert.deepEqual(mismatched, [baseAsset.url]); // Both sources unavailable: the failure names the documented recovery. await assert.rejects(installGentleAi({ packageRoot: await mkdtemp(join(tmpdir(), "gentle-pi-installer-mirror-unavailable-")), platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": baseAsset }, extractArchive, download: async () => { throw new Error("gone"); }, }), (error: unknown) => error instanceof Error && /GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1/.test(error.message) && (error as { code?: string }).code === "GENTLE_AI_ASSET_UNAVAILABLE"); // A stable pin downloads only from the signed gentle-ai release: no pinned // row carries a mirror, so an upstream failure surfaces as the primary error. for (const pinned of Object.values(GENTLE_AI_RELEASE_ASSETS)) { assert.equal((pinned as { mirrorUrl?: string }).mirrorUrl, undefined); } }); test("Windows archive lookup remains unsupported and names Go SumDB source installation", () => { for (const arch of ["x64", "arm64"]) { assert.throws(() => resolveGentleAiReleaseAsset("win32", arch), /Go SumDB source installation/); } }); test("unsupported platform pairs fail clearly before download", () => { for (const [platform, arch] of [["freebsd", "x64"], ["linux", "ia32"], ["darwin", "ppc64"]]) { assert.throws(() => resolveGentleAiReleaseAsset(platform, arch), /unsupported Gentle AI platform\/architecture/); } }); test("extractors use only absolute trusted system paths, never lifecycle PATH or SystemRoot", () => { const extractor = trustedSystemExtractor("archive.tar.gz", "linux", (path) => path === "/usr/bin/tar"); assert.equal(extractor.command, "/usr/bin/tar"); assert.ok(extractor.command.startsWith("/")); assert.throws(() => trustedSystemExtractor("archive.zip", "linux", () => false), /trusted system unzip/); const originalSystemRoot = process.env.SystemRoot; try { for (const hostileSystemRoot of ["relative", "\\\\attacker\\share", "C:\\attacker", ""]) { process.env.SystemRoot = hostileSystemRoot; const windows = trustedSystemExtractor("archive.zip", "win32", (path) => path === "C:\\Windows\\System32\\tar.exe"); assert.equal(windows.command, "C:\\Windows\\System32\\tar.exe"); } } finally { if (originalSystemRoot === undefined) delete process.env.SystemRoot; else process.env.SystemRoot = originalSystemRoot; } }); function pendingRequest() { const pending = new EventEmitter() as EventEmitter & { destroy(error?: Error): void; setTimeout(): void }; pending.destroy = (error) => queueMicrotask(() => pending.emit("error", error)); pending.setTimeout = () => undefined; return pending; } test("download bounds stalled headers and bodies with transient retry exhaustion", async (t) => { for (const [stage, request] of [ ["headers", (_url: URL, _options: unknown, _callback: (response: PassThrough & { statusCode?: number; headers: Record }) => void) => pendingRequest()], ["body", (_url: URL, _options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record }) => void) => { const response = Object.assign(new PassThrough(), { statusCode: 200, headers: {} }); queueMicrotask(() => callback(response)); return pendingRequest(); }], ] as const) { // A private directory per run: the PID-derived path this test used before lived // in the shared tmpdir and collided with leftovers from earlier runs, which is // how an EEXIST from the exclusive open reached the assertion instead of the // timeout under test. const directory = await mkdtemp(join(tmpdir(), "gentle-pi-stalled-")); t.after(() => rm(directory, { recursive: true, force: true })); const destination = join(directory, stage); let attempts = 0; await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", destination, 1024, 0, { request: (url: URL, options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record }) => void) => { attempts += 1; return request(url, options, callback); }, headerTimeoutMs: 1, bodyTimeoutMs: 1, maxAttempts: 2, retryDelayMs: 0 }), new RegExp(`download ${stage} timed out`)); assert.equal(attempts, 2); // A failed attempt settles only once its destination stream closed, so the retry // never races a late recreation of the path, and nothing partial survives. assert.equal(existsSync(destination), false, `a failed ${stage} download leaves no partial file behind`); } }); test("download never removes a destination it did not create", async (t) => { const directory = await mkdtemp(join(tmpdir(), "gentle-pi-existing-")); t.after(() => rm(directory, { recursive: true, force: true })); const destination = join(directory, "archive"); await writeFile(destination, "a file this download never owned"); // A single attempt on purpose: the retry path removes the destination on its own, // so the ownership rule is only observable without a retry. const request = (_url: URL, _options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record }) => void) => { const response = Object.assign(new PassThrough(), { statusCode: 200, headers: {} }); queueMicrotask(() => callback(response)); return pendingRequest(); }; await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", destination, 1024, 0, { request, headerTimeoutMs: 1, bodyTimeoutMs: 1, maxAttempts: 1, retryDelayMs: 0 })); assert.equal(await readFile(destination, "utf8"), "a file this download never owned"); }); test("download retries only transient HTTP statuses and exhausts within the attempt bound", async () => { for (const [status, expectedAttempts] of [[429, 2], [500, 2], [502, 2], [503, 2], [504, 2], [400, 1], [404, 1]] as const) { let attempts = 0; const request = (_url: URL, _options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record }) => void) => { attempts += 1; const response = Object.assign(new PassThrough(), { statusCode: status, headers: {} }); queueMicrotask(() => { callback(response); response.end(); }); return pendingRequest(); }; await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", join(tmpdir(), `gentle-pi-http-${status}-${process.pid}`), 1024, 0, { request, maxAttempts: 2, retryDelayMs: 0 }), new RegExp(`HTTP ${status}`)); assert.equal(attempts, expectedAttempts, `HTTP ${status}`); } }); test("checksum mismatch cleans temporary state without promoting a binary", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-mismatch-")); await assert.rejects( () => installGentleAi({ packageRoot, platform: "linux", arch: "x64", download: async (_url, destination) => writeFile(destination, "corrupt archive"), }), /checksum mismatch/, ); assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai")), false); assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []); }); test("installer promotes only the expected regular executable with executable POSIX mode", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-promote-")); const payload = Buffer.from("trusted archive fixture"); const sha256 = createHash("sha256").update(payload).digest("hex"); const asset = { name: "gentle-ai_2.1.3_linux_amd64.tar.gz", sha256, url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" }; await installGentleAi({ packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": asset }, download: async (_url, destination) => writeFile(destination, payload), extractArchive: async (_archive, destination) => { await mkdir(destination, { recursive: true }); const extracted = join(destination, "gentle-ai"); await writeFile(extracted, "native executable"); await chmod(extracted, 0o700); }, }); const binary = join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai"); assert.equal(existsSync(binary), true); assert.equal(await readFile(binary, "utf8"), "native executable"); assert.ok(((await stat(binary)).mode & 0o111) !== 0); assert.equal((await installGentleAi({ packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": asset } })).installed, false); }); test("installer rejects an extracted binary that differs from its pinned digest", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-binary-mismatch-")); const payload = Buffer.from("trusted archive fixture"); const asset = { name: "gentle-ai_2.1.9_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: "0".repeat(64), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai", }; await assert.rejects( () => installGentleAi({ packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": asset }, download: async (_url, destination) => writeFile(destination, payload), extractArchive: async (_archive, destination) => { await mkdir(destination, { recursive: true }); await writeFile(join(destination, "gentle-ai"), "native executable"); }, }), /binary checksum mismatch/, ); assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai")), false); }); test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => { if (process.platform === "win32") { t.skip("Windows does not use POSIX executable mode bits"); return; } const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-repair-mode-")); const payload = Buffer.from("trusted archive fixture"); const asset = { name: "gentle-ai_2.1.3_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" }; const options = { packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": asset }, download: async (_url: string, destination: string) => writeFile(destination, payload), extractArchive: async (_archive: string, destination: string) => { await mkdir(destination, { recursive: true }); await writeFile(join(destination, "gentle-ai"), "native executable"); }, }; await installGentleAi(options); const binary = join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai"); await chmod(binary, 0o600); const repaired = await installGentleAi(options); assert.equal(repaired.installed, true); assert.notEqual((await stat(binary)).mode & 0o111, 0); }); test("installer rejects a symlinked package-local runtime parent directory", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-symlink-")); const redirected = await mkdtemp(join(tmpdir(), "gentle-pi-installer-redirected-")); await symlink(redirected, join(packageRoot, ".gentle-ai")); await assert.rejects( () => installGentleAi({ packageRoot, platform: "linux", arch: "x64" }), /package-local runtime directory/, ); }); test("installer rejects archives with multiple expected executable entries", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-cardinality-")); const payload = Buffer.from("trusted archive fixture"); const asset = { name: "gentle-ai_2.1.3_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" }; await assert.rejects( () => installGentleAi({ packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": asset }, download: async (_url, destination) => writeFile(destination, payload), extractArchive: async (_archive, destination) => { await mkdir(join(destination, "first"), { recursive: true }); await mkdir(join(destination, "second"), { recursive: true }); await writeFile(join(destination, "first", "gentle-ai"), "one"); await writeFile(join(destination, "second", "gentle-ai"), "two"); }, }), /exactly one regular gentle-ai/, ); assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai")), false); }); test("installer rejects an archive without the expected regular executable", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-nonregular-")); const payload = Buffer.from("trusted archive fixture"); const asset = { name: "gentle-ai_2.1.3_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" }; await assert.rejects( () => installGentleAi({ packageRoot, platform: "linux", arch: "x64", releaseAssets: { "linux/amd64": asset }, download: async (_url, destination) => writeFile(destination, payload), extractArchive: async (_archive, destination) => mkdir(join(destination, "gentle-ai"), { recursive: true }), }), /non-regular gentle-ai/, ); assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.3.1", "gentle-ai")), false); }); test("safeRemoveDirectory passes Windows-safe retry options to recursive rm cleanup", async () => { const installerSource = await readFile(new URL("../scripts/gentle-ai-installer.mjs", import.meta.url), "utf8"); const match = /async function safeRemoveDirectory\(path\) \{(?[\s\S]*?)\n\}/.exec(installerSource); assert.ok(match?.groups?.body, "safeRemoveDirectory must remain available for scoped cleanup verification"); assert.match( match.groups.body, /await rm\(path, \{ recursive: true, force: true, maxRetries: 3, retryDelay: 100 \}\);/, "safeRemoveDirectory cleanup must retry Windows EPERM failures from read-only Go module cache files", ); }); // #400: a clean Windows source build measured 232 s on a cold module cache, // so `go install` cannot share the 120 s bound that fits the `go version` // probes. The probes keep their bound; the build gets its own. test("Windows source build bounds go install separately from the Go toolchain probes", async () => { const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-timeout-")); const fixture = windowsGoFixture(); const goPath = join(packageRoot, "go.exe"); await writeFile(goPath, "trusted local Go executable"); fixture.setGoExecutable(goPath); await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }); const goCalls = fixture.calls.filter((call) => call.file === goPath); const install = goCalls.find((call) => call.arguments_[0] === "install"); assert.ok(install, "go install must run"); assert.ok(typeof install.options.timeout === "number" && install.options.timeout >= 600_000, `go install must get at least a ten-minute bound, received ${install.options.timeout}`); const probes = goCalls.filter((call) => call.arguments_[0] === "version"); assert.ok(probes.length >= 2, "go version probes must run"); for (const probe of probes) assert.equal(probe.options.timeout, 120_000, `go ${probe.arguments_.join(" ")} keeps the probe bound`); });