fix: harden runtime config lease boundary
This commit is contained in:
@@ -84,11 +84,11 @@ function fixture(extraEnv: Record<string, string> = {}) {
|
||||
return { root, repo, snapshotPath, factory, factoryInput, canonicalDescriptor, snapshotManifest: join(snapshotsDir, "snapshot.json") };
|
||||
}
|
||||
|
||||
test("session and maintenance share deterministic bytes and path", () => {
|
||||
test("session and maintenance share deterministic bytes and path", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const session = f.factory.acquireSession(f.snapshotPath);
|
||||
const maintenance = f.factory.acquireMaintenance({ snapshotPath: f.snapshotPath });
|
||||
const session = await f.factory.acquireSession(f.snapshotPath);
|
||||
const maintenance = await f.factory.acquireMaintenance({ snapshotPath: f.snapshotPath });
|
||||
expect(session.path).toBe(maintenance.path);
|
||||
expect(readFileSync(session.path, "utf8")).toBe(readFileSync(maintenance.path, "utf8"));
|
||||
expect(lstatSync(session.path).mode & 0o777).toBe(0o400);
|
||||
@@ -101,19 +101,19 @@ test("session and maintenance share deterministic bytes and path", () => {
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("same revision changed bytes are refused", () => {
|
||||
test("same revision changed bytes are refused", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const first = f.factory.acquireSession(f.snapshotPath);
|
||||
const first = await f.factory.acquireSession(f.snapshotPath);
|
||||
chmodSync(first.path, 0o600);
|
||||
writeFileSync(first.path, "changed", { mode: 0o600 });
|
||||
chmodSync(first.path, 0o400);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/changed|mismatch|trusted/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/changed|mismatch|trusted/i);
|
||||
first.release();
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("snapshot descriptor must equal the Git canonical descriptor", () => {
|
||||
test("snapshot descriptor must equal the Git canonical descriptor", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const mutated = f.canonicalDescriptor.replace("database: analytics", "database: evil").replace("name: Lease", "name: Lease analytics");
|
||||
@@ -126,29 +126,29 @@ test("snapshot descriptor must equal the Git canonical descriptor", () => {
|
||||
chmodSync(manifestPath, 0o600);
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest), { mode: 0o600 });
|
||||
chmodSync(manifestPath, 0o400);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/Git descriptor|integrity|identity/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/Git descriptor|integrity|identity/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("same-byte replacement of the registry descriptor is refused", () => {
|
||||
test("same-byte replacement of the registry descriptor is refused", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const first = f.factory.acquireSession(f.snapshotPath);
|
||||
const first = await f.factory.acquireSession(f.snapshotPath);
|
||||
const replacement = `${f.snapshotPath}.replacement`;
|
||||
writeFileSync(replacement, readFileSync(f.snapshotPath), { mode: 0o400 });
|
||||
chmodSync(f.snapshotPath, 0o600);
|
||||
rmSync(f.snapshotPath);
|
||||
writeFileSync(f.snapshotPath, readFileSync(replacement), { mode: 0o400 });
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/identity|changed|mismatch|trusted/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/identity|changed|mismatch|trusted/i);
|
||||
first.release();
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
test("manifest binds the complete canonical destination directory chain", () => {
|
||||
test("manifest binds the complete canonical destination directory chain", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
const manifest = JSON.parse(readFileSync(lease.manifestPath, "utf8"));
|
||||
expect(manifest.directory_identities.length).toBeGreaterThan(5);
|
||||
expect(manifest.directory_identities.map((entry: { path: string }) => entry.path)).toContain(
|
||||
@@ -160,7 +160,7 @@ test("manifest binds the complete canonical destination directory chain", () =>
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("raw Git identity ignores replacement refs", () => {
|
||||
test("raw Git identity ignores replacement refs", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const evil = f.canonicalDescriptor.replace("database: analytics", "database: evil");
|
||||
@@ -179,14 +179,14 @@ test("raw Git identity ignores replacement refs", () => {
|
||||
chmodSync(f.snapshotManifest, 0o600);
|
||||
writeFileSync(f.snapshotManifest, JSON.stringify(snapshot));
|
||||
chmodSync(f.snapshotManifest, 0o400);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/Git descriptor|integrity|identity/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/Git descriptor|integrity|identity/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("replacement of canonical destination directories is refused", () => {
|
||||
test("replacement of canonical destination directories is refused", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
const original = join(f.root, "data", "sessions", workspace);
|
||||
const moved = `${original}.moved`;
|
||||
renameSync(original, moved);
|
||||
@@ -194,14 +194,14 @@ test("replacement of canonical destination directories is refused", () => {
|
||||
mkdirSync(join(original, "preprocessing", "runtime-config-manifests"), { recursive: true, mode: 0o700 });
|
||||
renameSync(join(moved, "preprocessing", "runtime-config", `${lease.workspaceRevision}.yaml`), join(original, "preprocessing", "runtime-config", `${lease.workspaceRevision}.yaml`));
|
||||
renameSync(join(moved, "preprocessing", "runtime-config-manifests", `${lease.workspaceRevision}.json`), join(original, "preprocessing", "runtime-config-manifests", `${lease.workspaceRevision}.json`));
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/changed|mismatch|same-revision|identity|trusted/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/changed|mismatch|same-revision|identity|trusted/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("rename faults fail closed and remove staging files", () => {
|
||||
test("rename faults fail closed and remove staging files", async () => {
|
||||
const f = fixture({ THT_RUNTIME_CONFIG_RENAME_FAIL: "1" });
|
||||
try {
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/rename|failed/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/rename|failed/i);
|
||||
const runtime = join(f.root, "data", "sessions", workspace, "preprocessing");
|
||||
for (const dir of ["runtime-config", "runtime-config-manifests"]) {
|
||||
if (existsSync(join(runtime, dir))) expect(readdirSync(join(runtime, dir)).filter((name) => name.includes("staging")).length).toBe(0);
|
||||
@@ -210,11 +210,11 @@ test("rename faults fail closed and remove staging files", () => {
|
||||
});
|
||||
|
||||
|
||||
test("session and operator outputs retain normalized private-host policy and binding", () => {
|
||||
test("session and operator outputs retain normalized private-host policy and binding", async () => {
|
||||
const f = fixture({ THT_HTTP_PRIVATE_HOST_ALLOWLIST: "internal.example,warehouse.example" });
|
||||
try {
|
||||
const session = f.factory.acquireSession(f.snapshotPath);
|
||||
const maintenance = f.factory.acquireMaintenance({ snapshotPath: f.snapshotPath });
|
||||
const session = await f.factory.acquireSession(f.snapshotPath);
|
||||
const maintenance = await f.factory.acquireMaintenance({ snapshotPath: f.snapshotPath });
|
||||
const output = readFileSync(session.path, "utf8");
|
||||
expect(output).toContain("http_private_host_allowlist");
|
||||
expect(output).toContain("- internal.example");
|
||||
@@ -228,29 +228,29 @@ test("session and operator outputs retain normalized private-host policy and bin
|
||||
});
|
||||
|
||||
|
||||
test("unexpected manifest fields are refused before handoff", () => {
|
||||
test("unexpected manifest fields are refused before handoff", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
const manifest = JSON.parse(readFileSync(lease.manifestPath, "utf8"));
|
||||
manifest.unexpected = true;
|
||||
chmodSync(lease.manifestPath, 0o600);
|
||||
writeFileSync(lease.manifestPath, JSON.stringify(manifest));
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/manifest|invalid|changed/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/manifest|invalid|changed/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("runtime config symlink replacement is refused", () => {
|
||||
test("runtime config symlink replacement is refused", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
const replacement = `${lease.path}.real`;
|
||||
writeFileSync(replacement, readFileSync(lease.path), { mode: 0o400 });
|
||||
chmodSync(lease.path, 0o600);
|
||||
rmSync(lease.path);
|
||||
// A no-follow handoff must never consume this pathname.
|
||||
execFileSync("ln", ["-s", replacement, lease.path]);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/trusted|changed|configuration|symbolic/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/trusted|changed|configuration|symbolic/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
@@ -260,11 +260,11 @@ function realHarnessBinding(config: string): Record<string, string> {
|
||||
const python = join(process.cwd(), "..", "harness", ".venv", "bin", "python");
|
||||
return JSON.parse(execFileSync(python, [helper], {
|
||||
cwd: join(process.cwd(), "..", "harness"), encoding: "utf8",
|
||||
input: JSON.stringify({ action: "binding", config_hex: Buffer.from(config).toString("hex") }),
|
||||
input: JSON.stringify({ protocol_version: 1, action: "binding", config_hex: Buffer.from(config).toString("hex") }),
|
||||
}));
|
||||
}
|
||||
|
||||
test("explicit installation overlay is canonical and has one real harness binding", () => {
|
||||
test("explicit installation overlay is canonical and has one real harness binding", async () => {
|
||||
const f = fixture();
|
||||
const overlay = {
|
||||
profile: "workstation",
|
||||
@@ -277,8 +277,8 @@ test("explicit installation overlay is canonical and has one real harness bindin
|
||||
const sessionFactory = new WorkspaceRuntimeConfigLeaseFactory({ ...f.factoryInput, installationOverlay: overlay });
|
||||
const maintenanceFactory = new WorkspaceRuntimeConfigLeaseFactory({ ...f.factoryInput, installationOverlay: overlay });
|
||||
try {
|
||||
const session = sessionFactory.acquireSession(f.snapshotPath);
|
||||
const maintenance = maintenanceFactory.acquireMaintenance({ workspaceConfigPath: f.snapshotPath });
|
||||
const session = await sessionFactory.acquireSession(f.snapshotPath);
|
||||
const maintenance = await maintenanceFactory.acquireMaintenance({ workspaceConfigPath: f.snapshotPath });
|
||||
const sessionYaml = readFileSync(session.path, "utf8");
|
||||
const maintenanceYaml = readFileSync(maintenance.path, "utf8");
|
||||
expect(session.path).toBe(maintenance.path);
|
||||
@@ -298,10 +298,10 @@ test("explicit installation overlay is canonical and has one real harness bindin
|
||||
test.each([
|
||||
["config-file", "config-file"], ["config-parent", "config-parent"],
|
||||
["manifest-file", "manifest-file"], ["manifest-parent", "manifest-parent"],
|
||||
] as const)("fsync fault at %s fails closed and retries to the same durable pair", (_label, stage) => {
|
||||
] as const)("fsync fault at %s fails closed and retries to the same durable pair", async (_label, stage) => {
|
||||
const f = fixture({ THT_RUNTIME_CONFIG_FSYNC_FAIL: stage });
|
||||
try {
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/fsync|failed/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/fsync|failed/i);
|
||||
const runtime = join(f.root, "data", "sessions", workspace, "preprocessing");
|
||||
for (const dir of ["runtime-config", "runtime-config-manifests"]) {
|
||||
if (existsSync(join(runtime, dir))) {
|
||||
@@ -311,7 +311,7 @@ test.each([
|
||||
const recovered = new WorkspaceRuntimeConfigLeaseFactory({ ...f.factoryInput, env: {
|
||||
...f.factoryInput.env, THT_RUNTIME_CONFIG_FSYNC_FAIL: undefined,
|
||||
} });
|
||||
const lease = recovered.acquireSession(f.snapshotPath);
|
||||
const lease = await recovered.acquireSession(f.snapshotPath);
|
||||
expect(existsSync(lease.path)).toBe(true);
|
||||
expect(existsSync(lease.manifestPath)).toBe(true);
|
||||
expect(JSON.parse(readFileSync(lease.manifestPath, "utf8")).config_sha256)
|
||||
@@ -324,10 +324,10 @@ for (const [label, mutate] of [
|
||||
["outside path", (f: ReturnType<typeof fixture>) => join(f.root, "outside.yaml")],
|
||||
["wrong workspace id", (f: ReturnType<typeof fixture>) => f.snapshotPath.replace("abc.yaml", "abd.yaml")],
|
||||
] as const) {
|
||||
test(`rejects ${label} before publication`, () => {
|
||||
test(`rejects ${label} before publication`, async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
expect(() => f.factory.acquireSession(mutate(f))).toThrow(/trusted|snapshot|identity|path|Git|unavailable|ENOENT|No such/i);
|
||||
await expect(f.factory.acquireSession(mutate(f))).rejects.toThrow(/trusted|snapshot|identity|path|Git|unavailable|ENOENT|No such/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
@@ -336,11 +336,11 @@ for (const [label, replace] of [
|
||||
["descriptor symlink", (path: string, root: string) => { const target = `${path}.target`; writeFileSync(target, readFileSync(path), { mode: 0o400 }); rmSync(path); execFileSync("ln", ["-s", target, path]); }],
|
||||
["descriptor hardlink", (path: string, root: string) => { const target = `${path}.target`; execFileSync("ln", [path, target]); rmSync(path); execFileSync("ln", [target, path]); }],
|
||||
] as const) {
|
||||
test(`rejects ${label}`, () => {
|
||||
test(`rejects ${label}`, async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
replace(f.snapshotPath, f.root);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/trusted|integrity|identity|link/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/trusted|integrity|identity|link/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
@@ -349,23 +349,23 @@ for (const [label, target] of [
|
||||
["config symlink", "config"], ["config hardlink", "config"],
|
||||
["manifest symlink", "manifest"], ["manifest hardlink", "manifest"],
|
||||
] as const) {
|
||||
test(`rejects destination ${label} and recovers safely`, () => {
|
||||
test(`rejects destination ${label} and recovers safely`, async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const first = f.factory.acquireSession(f.snapshotPath);
|
||||
const first = await f.factory.acquireSession(f.snapshotPath);
|
||||
const path = target === "config" ? first.path : first.manifestPath;
|
||||
const backup = `${path}.target`;
|
||||
writeFileSync(backup, readFileSync(path), { mode: target === "config" ? 0o400 : 0o600 });
|
||||
rmSync(path);
|
||||
if (label.includes("symlink")) execFileSync("ln", ["-s", backup, path]);
|
||||
else execFileSync("ln", [backup, path]);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/trusted|configuration|manifest|link|changed/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/trusted|configuration|manifest|link|changed/i);
|
||||
rmSync(path);
|
||||
// A replaced inode can never be trusted again. Remove the paired durable
|
||||
// publication and let a fresh no-replace publication recover the layout.
|
||||
rmSync(first.path, { force: true });
|
||||
rmSync(first.manifestPath, { force: true });
|
||||
const recovered = f.factory.acquireSession(f.snapshotPath);
|
||||
const recovered = await f.factory.acquireSession(f.snapshotPath);
|
||||
expect(recovered.path).toBe(first.path);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -374,44 +374,44 @@ for (const [label, target] of [
|
||||
for (const [label, target, mode] of [
|
||||
["config", "config", 0o600], ["manifest", "manifest", 0o400],
|
||||
] as const) {
|
||||
test(`refuses wrong ${label} mode then recovers after restoring mode`, () => {
|
||||
test(`refuses wrong ${label} mode then recovers after restoring mode`, async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const first = f.factory.acquireSession(f.snapshotPath);
|
||||
const first = await f.factory.acquireSession(f.snapshotPath);
|
||||
const path = target === "config" ? first.path : first.manifestPath;
|
||||
chmodSync(path, mode);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/trusted|mode|configuration|manifest/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/trusted|mode|configuration|manifest/i);
|
||||
chmodSync(path, target === "config" ? 0o400 : 0o600);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).not.toThrow();
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).resolves.toBeDefined();
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
|
||||
test("release retains durable state while changed binding is refused by a new factory", () => {
|
||||
test("release retains durable state while changed binding is refused by a new factory", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const first = f.factory.acquireSession(f.snapshotPath);
|
||||
const first = await f.factory.acquireSession(f.snapshotPath);
|
||||
first.release();
|
||||
const changed = new WorkspaceRuntimeConfigLeaseFactory({ ...f.factoryInput, env: {
|
||||
...f.factoryInput.env, THT_WS_ABC_DWH_HOST: "other-dwh",
|
||||
} });
|
||||
expect(() => changed.acquireSession(f.snapshotPath)).toThrow(/changed|mismatch|configuration/i);
|
||||
await expect(changed.acquireSession(f.snapshotPath)).rejects.toThrow(/changed|mismatch|configuration/i);
|
||||
expect(existsSync(first.path)).toBe(true);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("strict manifest rejects an extra field and recovery preserves exact bytes", () => {
|
||||
test("strict manifest rejects an extra field and recovery preserves exact bytes", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const first = f.factory.acquireSession(f.snapshotPath);
|
||||
const first = await f.factory.acquireSession(f.snapshotPath);
|
||||
const original = readFileSync(first.manifestPath, "utf8");
|
||||
const manifest = JSON.parse(original);
|
||||
manifest.extra = "reject";
|
||||
chmodSync(first.manifestPath, 0o600);
|
||||
writeFileSync(first.manifestPath, JSON.stringify(manifest), { mode: 0o600 });
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/manifest|invalid|changed/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/manifest|invalid|changed/i);
|
||||
writeFileSync(first.manifestPath, original, { mode: 0o600 });
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).not.toThrow();
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).resolves.toBeDefined();
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
@@ -420,40 +420,40 @@ test.each([
|
||||
["duplicate", "alpha.example,alpha.example"],
|
||||
["uppercase", "Alpha.example"],
|
||||
["ip address", "127.0.0.1"],
|
||||
] as const)("rejects %s private-host policy", (_label, allowlist) => {
|
||||
] as const)("rejects %s private-host policy", async (_label, allowlist) => {
|
||||
expect(() => fixture({ THT_HTTP_PRIVATE_HOST_ALLOWLIST: allowlist }))
|
||||
.toThrow(/allowlist|hostname|duplicate|invalid/i);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["runtime-config", "config"], ["runtime-config-manifests", "manifest"],
|
||||
] as const)("rejects a replaced %s destination ancestor", (directory, _kind) => {
|
||||
] as const)("rejects a replaced %s destination ancestor", async (directory, _kind) => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
const parent = join(f.root, "data", "sessions", workspace, "preprocessing", directory);
|
||||
const moved = `${parent}.moved`;
|
||||
renameSync(parent, moved);
|
||||
execFileSync("ln", ["-s", moved, parent]);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/trusted|symbolic|changed|directory/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/trusted|symbolic|changed|directory/i);
|
||||
rmSync(parent);
|
||||
renameSync(moved, parent);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).not.toThrow();
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).resolves.toBeDefined();
|
||||
lease.release();
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("release is idempotent and does not remove either durable publication", () => {
|
||||
test("release is idempotent and does not remove either durable publication", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
lease.release(); lease.release();
|
||||
expect(existsSync(lease.path)).toBe(true);
|
||||
expect(existsSync(lease.manifestPath)).toBe(true);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("partial os.write calls are completed by the real Python publication helper", () => {
|
||||
test("partial os.write calls are completed by the real Python publication helper", async () => {
|
||||
const python = join(process.cwd(), "..", "harness", ".venv", "bin", "python");
|
||||
const helper = join(process.cwd(), "..", "harness", "tht", "runtime_config_lease_io.py");
|
||||
const code = `import os, sys; sys.path.insert(0, ${JSON.stringify(dirname(helper))}); import runtime_config_lease_io as m; real=os.write; os.write=lambda fd,b: real(fd,b[:3]); m.write_all(1, b'partial-write-ok\\n')`;
|
||||
@@ -461,20 +461,20 @@ test("partial os.write calls are completed by the real Python publication helper
|
||||
expect(output).toBe("partial-write-ok\n");
|
||||
});
|
||||
|
||||
test("a clean existing equal publication is reconciled by a new factory", () => {
|
||||
test("a clean existing equal publication is reconciled by a new factory", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const first = f.factory.acquireSession(f.snapshotPath);
|
||||
const second = new WorkspaceRuntimeConfigLeaseFactory(f.factoryInput).acquireMaintenance({ snapshotPath: f.snapshotPath });
|
||||
const first = await f.factory.acquireSession(f.snapshotPath);
|
||||
const second = await new WorkspaceRuntimeConfigLeaseFactory(f.factoryInput).acquireMaintenance({ snapshotPath: f.snapshotPath });
|
||||
expect(readFileSync(second.path)).toEqual(readFileSync(first.path));
|
||||
expect(readFileSync(second.manifestPath)).toEqual(readFileSync(first.manifestPath));
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test.each([["config"], ["manifest"]] as const)("rename failure is scoped to the %s branch and leaves no staging", (kind) => {
|
||||
test.each([["config"], ["manifest"]] as const)("rename failure is scoped to the %s branch and leaves no staging", async (kind) => {
|
||||
const f = fixture({ THT_RUNTIME_CONFIG_RENAME_FAIL: kind });
|
||||
try {
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/rename|failed/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/rename|failed/i);
|
||||
const runtime = join(f.root, "data", "sessions", workspace, "preprocessing");
|
||||
for (const dir of ["runtime-config", "runtime-config-manifests"]) {
|
||||
if (existsSync(join(runtime, dir))) expect(readdirSync(join(runtime, dir)).some((name) => name.includes("staging"))).toBe(false);
|
||||
@@ -482,7 +482,7 @@ test.each([["config"], ["manifest"]] as const)("rename failure is scoped to the
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("snapshot manifest rejects an undeclared extra immutable file", () => {
|
||||
test("snapshot manifest rejects an undeclared extra immutable file", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const snapshot = JSON.parse(readFileSync(f.snapshotManifest, "utf8"));
|
||||
@@ -490,15 +490,15 @@ test("snapshot manifest rejects an undeclared extra immutable file", () => {
|
||||
snapshot.files["smuggled.txt"] = createHash("sha256").update("smuggled").digest("hex");
|
||||
chmodSync(f.snapshotManifest, 0o600);
|
||||
writeFileSync(f.snapshotManifest, JSON.stringify(snapshot), { mode: 0o400 });
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/integrity|snapshot|trusted/i);
|
||||
await expect(f.factory.acquireSession(f.snapshotPath)).rejects.toThrow(/integrity|snapshot|trusted/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
test("independent OS publishers converge when equal and elect one winner when unequal", () => {
|
||||
test("independent OS publishers converge when equal and elect one winner when unequal", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const seed = f.factory.acquireSession(f.snapshotPath);
|
||||
const seed = await f.factory.acquireSession(f.snapshotPath);
|
||||
const full = JSON.parse(readFileSync(seed.manifestPath, "utf8"));
|
||||
const base = Object.fromEntries(["workspace_id", "workspace_revision", "descriptor_git_blob",
|
||||
"descriptor_sha256", "descriptor_dev", "descriptor_ino", "config_dwh_binding"]
|
||||
@@ -509,7 +509,7 @@ test("independent OS publishers converge when equal and elect one winner when un
|
||||
// Leave the workspace-owned destination directories in place, but remove
|
||||
// both durable leaves: the following OS processes race on a clean layout.
|
||||
rmSync(seed.path); rmSync(seed.manifestPath);
|
||||
const payload = JSON.stringify({ action: "publish", data_root: f.factoryInput.dataRoot,
|
||||
const payload = JSON.stringify({ protocol_version: 1, action: "publish", data_root: f.factoryInput.dataRoot,
|
||||
workspace_id: workspace, workspace_revision: full.workspace_revision,
|
||||
config_hex: Buffer.from(configBytes).toString("hex"), manifest_base: base });
|
||||
const code = `import json,multiprocessing,sys
|
||||
@@ -538,10 +538,10 @@ print(json.dumps([q.exitcode for q in p]))`
|
||||
});
|
||||
|
||||
|
||||
test.each(["leaf", "ancestor"] as const)("actual harness rejects canonical %s swap", (kind) => {
|
||||
test.each(["leaf", "ancestor"] as const)("actual harness rejects canonical %s swap", async (kind) => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
const harnessBin = join(process.cwd(), "..", "harness", ".venv", "bin", "tht");
|
||||
const harnessCwd = join(process.cwd(), "..", "harness");
|
||||
const env = { ...process.env, THT_RUNTIME_CONFIG_MANIFEST_SHA256: lease.manifestSha256 };
|
||||
@@ -562,10 +562,10 @@ test.each(["leaf", "ancestor"] as const)("actual harness rejects canonical %s sw
|
||||
});
|
||||
|
||||
|
||||
test("actual harness rejects a workspace chain swap between config and manifest traversal", () => {
|
||||
test("actual harness rejects a workspace chain swap between config and manifest traversal", async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const lease = await f.factory.acquireSession(f.snapshotPath);
|
||||
const python = join(process.cwd(), "..", "harness", ".venv", "bin", "python");
|
||||
const helper = join(process.cwd(), "..", "harness", "tht");
|
||||
const workspaceRoot = join(f.factoryInput.dataRoot, "sessions", workspace);
|
||||
|
||||
@@ -169,8 +169,8 @@ test("real schema-v3 registry revision loads through ThtRunner and the harness c
|
||||
test("separate runtime leases hand off byte-identical revision Evidence configs accepted by tht", async () => {
|
||||
const f = await fixture();
|
||||
const runner = runnerFor(f);
|
||||
const first = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const second = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const first = await runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const second = await runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const expectedRoot = join(
|
||||
f.registryConfig.root,
|
||||
"snapshots",
|
||||
@@ -226,7 +226,7 @@ test("separate runtime leases hand off byte-identical revision Evidence configs
|
||||
test("real Evidence-content-only commit changes runtime identity and root with identical descriptor YAML", async () => {
|
||||
const f = await fixture();
|
||||
const runner = runnerFor(f);
|
||||
const first = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const first = await runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const descriptorBefore = readFileSync(f.revision.snapshotPath, "utf8");
|
||||
writeFileSync(
|
||||
join(f.source, "workspace-content", "psd-clinical", "evidence", "guide.md"),
|
||||
@@ -237,7 +237,7 @@ test("real Evidence-content-only commit changes runtime identity and root with i
|
||||
await git(f.source, ["push", "origin", "main"]);
|
||||
await f.registry.pull();
|
||||
const current = (await f.registry.list())[0];
|
||||
const second = runner.acquireWorkspaceRuntime(current.snapshotPath);
|
||||
const second = await runner.acquireWorkspaceRuntime(current.snapshotPath);
|
||||
|
||||
try {
|
||||
expect(current.commit).not.toBe(f.revision.commit);
|
||||
@@ -281,7 +281,7 @@ test("signed HTTP Evidence resolves its file binding and config check never capt
|
||||
max_cache_bytes: 67890
|
||||
`));
|
||||
const runner = runnerFor(f);
|
||||
const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const lease = await runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
try {
|
||||
const yaml = readFileSync(lease.path, "utf8");
|
||||
expect(parse(yaml).evidence.sources).toEqual([{
|
||||
@@ -325,7 +325,7 @@ test("static S3 Evidence resolves only configured secret-root file paths", async
|
||||
retain_published_generations: 7
|
||||
`));
|
||||
const runner = runnerFor(f);
|
||||
const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const lease = await runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
try {
|
||||
const yaml = readFileSync(lease.path, "utf8");
|
||||
expect(parse(yaml).evidence.sources).toEqual([{
|
||||
|
||||
Reference in New Issue
Block a user