test(config): cover secret bundle inode races

This commit is contained in:
2026-07-12 11:09:09 +02:00
parent 390cfd24b5
commit 3807c65a41
2 changed files with 48 additions and 10 deletions
+17 -1
View File
@@ -2,7 +2,7 @@ import { afterEach, expect, test } from "vitest";
import { chmodSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { loadSecretBundle, secretValue } from "../src/config/secret-bundle.js";
import { loadSecretBundle, loadSecretBundleWithFs, secretValue } from "../src/config/secret-bundle.js";
const dirs: string[] = [];
afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); });
@@ -48,6 +48,22 @@ test("checks inode identity before parsing", () => {
expect(loadSecretBundle(file).get("THT_MODEL_API_KEY")).toBe("replaced");
});
test("rejects inode replacement between lstat and open without reading", () => {
let reads = 0;
const stat = (ino: number) => ({
dev: 7, ino, uid: process.getuid?.() ?? 0, mode: 0o100600, nlink: 1, size: 24,
isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false,
});
expect(() => loadSecretBundleWithFs("/safe/bundle", {
lstat: () => stat(1) as any,
open: () => 9,
fstat: () => stat(2) as any,
read: () => { reads += 1; return "THT_MODEL_API_KEY=secret\n"; },
close: () => undefined,
})).toThrow("secret bundle is unavailable");
expect(reads).toBe(0);
});
test("secretValue prefers bundle and supports the legacy file fallback", () => {
const file = bundle("THT_MODEL_API_KEY=from-bundle\n");
const legacy = bundle("from-legacy");