612 lines
30 KiB
TypeScript
612 lines
30 KiB
TypeScript
import { test, expect } from "vitest";
|
|
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { WorkspaceRuntimeConfigLeaseFactory } from "../src/workspaces/runtime-config-lease.js";
|
|
import { parse } from "yaml";
|
|
import { parseWorkspaceYaml, serializeWorkspaceYaml } from "../src/workspaces/schema.js";
|
|
|
|
const workspace = "abc";
|
|
const descriptor = `workspace:
|
|
schema_version: 3
|
|
id: ${workspace}
|
|
name: Lease
|
|
language: en
|
|
dwh:
|
|
engine: postgres
|
|
database: analytics
|
|
schema: mart
|
|
supported_transports: [postgres_direct]
|
|
semantic_index:
|
|
vector_store:
|
|
engine: qdrant
|
|
collection: abc
|
|
dimensions: 1024
|
|
distance: cosine
|
|
embedding:
|
|
provider: ollama_internal
|
|
model: qwen3-embedding:0.6b
|
|
dimensions: 1024
|
|
llm_policy:
|
|
allowed: [zai/glm-5.2]
|
|
`;
|
|
|
|
function fixture(extraEnv: Record<string, string> = {}) {
|
|
const root = mkdtempSync(join(tmpdir(), "runtime-config-lease-"));
|
|
const canonicalDescriptor = serializeWorkspaceYaml(parseWorkspaceYaml(descriptor));
|
|
const snapshots = join(root, "snapshots");
|
|
const repo = join(root, "repo");
|
|
mkdirSync(join(repo, "workspaces"), { recursive: true });
|
|
execFileSync("git", ["init", "--initial-branch=main"], { cwd: repo });
|
|
execFileSync("git", ["config", "user.name", "Fixture"], { cwd: repo });
|
|
execFileSync("git", ["config", "user.email", "fixture@example.invalid"], { cwd: repo });
|
|
writeFileSync(join(repo, "workspaces", `${workspace}.yaml`), canonicalDescriptor);
|
|
execFileSync("git", ["add", "."], { cwd: repo });
|
|
execFileSync("git", ["commit", "-m", "fixture"], { cwd: repo });
|
|
const actualCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf8" }).trim();
|
|
const blob = execFileSync("git", ["rev-parse", `HEAD:workspaces/${workspace}.yaml`], { cwd: repo, encoding: "utf8" }).trim();
|
|
const snapshotsDir = join(snapshots, actualCommit);
|
|
const snapshotPath = join(snapshotsDir, `${workspace}.yaml`);
|
|
const dataRoot = join(root, "data");
|
|
const harness = join(root, "harness");
|
|
mkdirSync(snapshotsDir, { recursive: true, mode: 0o700 });
|
|
chmodSync(snapshots, 0o700);
|
|
const publicFiles = {
|
|
[`${workspace}.yaml`]: canonicalDescriptor,
|
|
[`${workspace}.env.example`]: "# fixture\n",
|
|
[`${workspace}.md`]: "# Lease\n",
|
|
};
|
|
for (const [name, contents] of Object.entries(publicFiles)) writeFileSync(join(snapshotsDir, name), contents, { mode: 0o400 });
|
|
writeFileSync(join(snapshotsDir, "snapshot.json"), JSON.stringify({
|
|
head: actualCommit,
|
|
revisions: [{ id: workspace, commit: actualCommit, blob, snapshotPath }],
|
|
files: Object.fromEntries(Object.entries(publicFiles).map(([name, contents]) => [name, createHash("sha256").update(contents).digest("hex")])),
|
|
}), { mode: 0o400 });
|
|
mkdirSync(harness);
|
|
const secret = join(root, "password");
|
|
writeFileSync(secret, "secret", { mode: 0o600 });
|
|
const configPath = join(harness, "config.yaml");
|
|
writeFileSync(configPath, "profile: workstation\n");
|
|
const factoryInput = {
|
|
dataRoot, runtimeSnapshotRoot: snapshots, harnessDir: harness, configPath,
|
|
env: {
|
|
THT_WS_ABC_DWH_TRANSPORT: "postgres_direct", THT_WS_ABC_DWH_HOST: "dwh",
|
|
THT_WS_ABC_DWH_PORT: "5432", THT_WS_ABC_DWH_USER: "reader",
|
|
THT_WS_ABC_DWH_PASSWORD_FILE: secret, ...extraEnv,
|
|
}, secretRoots: [root], semanticRuntime: {
|
|
internalQdrantUrl: "http://qdrant:6333", internalEmbeddingUrl: "http://embedding:11434",
|
|
internalEmbeddingModel: "qwen3-embedding:0.6b", internalEmbeddingDimensions: 1024,
|
|
},
|
|
};
|
|
const factory = new WorkspaceRuntimeConfigLeaseFactory(factoryInput);
|
|
return { root, repo, snapshotPath, factory, factoryInput, canonicalDescriptor, snapshotManifest: join(snapshotsDir, "snapshot.json") };
|
|
}
|
|
|
|
test("session and maintenance share deterministic bytes and path", () => {
|
|
const f = fixture();
|
|
try {
|
|
const session = f.factory.acquireSession(f.snapshotPath);
|
|
const maintenance = 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);
|
|
expect(existsSync(maintenance.manifestPath)).toBe(true);
|
|
const manifest = JSON.parse(readFileSync(maintenance.manifestPath, "utf8"));
|
|
expect(manifest).toMatchObject({ version: 1, descriptor_dev: expect.any(String), descriptor_ino: expect.any(String) });
|
|
expect(existsSync(join(dirname(dirname(maintenance.path)), "runtime-config.lock"))).toBe(false);
|
|
session.release(); maintenance.release();
|
|
expect(existsSync(session.path)).toBe(true);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("same revision changed bytes are refused", () => {
|
|
const f = fixture();
|
|
try {
|
|
const first = 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);
|
|
first.release();
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("snapshot descriptor must equal the Git canonical descriptor", () => {
|
|
const f = fixture();
|
|
try {
|
|
const mutated = f.canonicalDescriptor.replace("database: analytics", "database: evil").replace("name: Lease", "name: Lease analytics");
|
|
chmodSync(f.snapshotPath, 0o600);
|
|
writeFileSync(f.snapshotPath, mutated, { mode: 0o600 });
|
|
chmodSync(f.snapshotPath, 0o400);
|
|
const manifestPath = join(dirname(f.snapshotPath), "snapshot.json");
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
manifest.files[`${workspace}.yaml`] = createHash("sha256").update(mutated).digest("hex");
|
|
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);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("same-byte replacement of the registry descriptor is refused", () => {
|
|
const f = fixture();
|
|
try {
|
|
const first = 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);
|
|
first.release();
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
test("manifest binds the complete canonical destination directory chain", () => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = 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(
|
|
`${process.platform === "darwin" ? "/private" : ""}${join(f.root, "data", "sessions", workspace, "preprocessing")}`,
|
|
);
|
|
expect(manifest.directory_identities.every((entry: Record<string, string>) =>
|
|
["path", "dev", "ino", "mode", "uid"].every((key) => typeof entry[key] === "string"),
|
|
)).toBe(true);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("raw Git identity ignores replacement refs", () => {
|
|
const f = fixture();
|
|
try {
|
|
const evil = f.canonicalDescriptor.replace("database: analytics", "database: evil");
|
|
chmodSync(join(f.repo, "workspaces", `${workspace}.yaml`), 0o600);
|
|
writeFileSync(join(f.repo, "workspaces", `${workspace}.yaml`), evil);
|
|
execFileSync("git", ["add", "."], { cwd: f.repo });
|
|
execFileSync("git", ["commit", "-m", "evil"], { cwd: f.repo });
|
|
const evilCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: f.repo, encoding: "utf8" }).trim();
|
|
const oldCommit = JSON.parse(readFileSync(f.snapshotManifest, "utf8")).head;
|
|
execFileSync("git", ["replace", oldCommit, evilCommit], { cwd: f.repo });
|
|
chmodSync(f.snapshotPath, 0o600);
|
|
writeFileSync(f.snapshotPath, evil);
|
|
chmodSync(f.snapshotPath, 0o400);
|
|
const snapshot = JSON.parse(readFileSync(f.snapshotManifest, "utf8"));
|
|
snapshot.files[`${workspace}.yaml`] = createHash("sha256").update(evil).digest("hex");
|
|
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);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("replacement of canonical destination directories is refused", () => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = f.factory.acquireSession(f.snapshotPath);
|
|
const original = join(f.root, "data", "sessions", workspace);
|
|
const moved = `${original}.moved`;
|
|
renameSync(original, moved);
|
|
mkdirSync(join(original, "preprocessing", "runtime-config"), { recursive: true, mode: 0o700 });
|
|
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);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("rename faults fail closed and remove staging files", () => {
|
|
const f = fixture({ THT_RUNTIME_CONFIG_RENAME_FAIL: "1" });
|
|
try {
|
|
expect(() => f.factory.acquireSession(f.snapshotPath)).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);
|
|
}
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
test("session and operator outputs retain normalized private-host policy and binding", () => {
|
|
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 output = readFileSync(session.path, "utf8");
|
|
expect(output).toContain("http_private_host_allowlist");
|
|
expect(output).toContain("- internal.example");
|
|
expect(output).toContain("- warehouse.example");
|
|
expect(output).toBe(readFileSync(maintenance.path, "utf8"));
|
|
const manifest = JSON.parse(readFileSync(session.manifestPath, "utf8"));
|
|
expect(manifest.config_dwh_binding).toEqual({
|
|
workspace_id: expect.any(String), config_fingerprint: expect.any(String), input_fingerprint: expect.any(String),
|
|
});
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
test("unexpected manifest fields are refused before handoff", () => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = 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);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("runtime config symlink replacement is refused", () => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = 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);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
function realHarnessBinding(config: string): Record<string, string> {
|
|
const helper = join(process.cwd(), "..", "harness", "tht", "runtime_config_lease_io.py");
|
|
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") }),
|
|
}));
|
|
}
|
|
|
|
test("explicit installation overlay is canonical and has one real harness binding", () => {
|
|
const f = fixture();
|
|
const overlay = {
|
|
profile: "workstation",
|
|
session_storage: { type: "postgres_direct", connection: {
|
|
host: "session-db", port: 5432, database: "sessions", schema: "public",
|
|
user: "runtime", password: "secret", sslmode: "verify-full",
|
|
} },
|
|
egress: { http_private_host_allowlist: ["zeta.example", "alpha.example", "warehouse.example"] },
|
|
};
|
|
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 sessionYaml = readFileSync(session.path, "utf8");
|
|
const maintenanceYaml = readFileSync(maintenance.path, "utf8");
|
|
expect(session.path).toBe(maintenance.path);
|
|
expect(sessionYaml).toBe(maintenanceYaml);
|
|
expect(parse(sessionYaml)).toMatchObject({
|
|
profile: overlay.profile,
|
|
session_storage: overlay.session_storage,
|
|
egress: { http_private_host_allowlist: ["alpha.example", "warehouse.example", "zeta.example"] },
|
|
});
|
|
const firstBinding = realHarnessBinding(sessionYaml);
|
|
const secondBinding = realHarnessBinding(maintenanceYaml);
|
|
expect(firstBinding).toEqual(secondBinding);
|
|
expect(JSON.parse(readFileSync(session.manifestPath, "utf8")).config_dwh_binding).toEqual(firstBinding);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
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) => {
|
|
const f = fixture({ THT_RUNTIME_CONFIG_FSYNC_FAIL: stage });
|
|
try {
|
|
expect(() => f.factory.acquireSession(f.snapshotPath)).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))) {
|
|
expect(readdirSync(join(runtime, dir)).filter((name) => name.includes("staging")).length).toBe(0);
|
|
}
|
|
}
|
|
const recovered = new WorkspaceRuntimeConfigLeaseFactory({ ...f.factoryInput, env: {
|
|
...f.factoryInput.env, THT_RUNTIME_CONFIG_FSYNC_FAIL: undefined,
|
|
} });
|
|
const lease = 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)
|
|
.toBe(createHash("sha256").update(readFileSync(lease.path)).digest("hex"));
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
for (const [label, mutate] of [
|
|
["wrong commit", (f: ReturnType<typeof fixture>) => f.snapshotPath.replace(/\/[0-9a-f]{40}\//, "/" + "0".repeat(40) + "/")],
|
|
["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`, () => {
|
|
const f = fixture();
|
|
try {
|
|
expect(() => f.factory.acquireSession(mutate(f))).toThrow(/trusted|snapshot|identity|path|Git|unavailable|ENOENT|No such/i);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
}
|
|
|
|
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}`, () => {
|
|
const f = fixture();
|
|
try {
|
|
replace(f.snapshotPath, f.root);
|
|
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/trusted|integrity|identity|link/i);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
}
|
|
|
|
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`, () => {
|
|
const f = fixture();
|
|
try {
|
|
const first = 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);
|
|
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);
|
|
expect(recovered.path).toBe(first.path);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
}
|
|
|
|
for (const [label, target, mode] of [
|
|
["config", "config", 0o600], ["manifest", "manifest", 0o400],
|
|
] as const) {
|
|
test(`refuses wrong ${label} mode then recovers after restoring mode`, () => {
|
|
const f = fixture();
|
|
try {
|
|
const first = 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);
|
|
chmodSync(path, target === "config" ? 0o400 : 0o600);
|
|
expect(() => f.factory.acquireSession(f.snapshotPath)).not.toThrow();
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
}
|
|
|
|
test("release retains durable state while changed binding is refused by a new factory", () => {
|
|
const f = fixture();
|
|
try {
|
|
const first = 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);
|
|
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", () => {
|
|
const f = fixture();
|
|
try {
|
|
const first = 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);
|
|
writeFileSync(first.manifestPath, original, { mode: 0o600 });
|
|
expect(() => f.factory.acquireSession(f.snapshotPath)).not.toThrow();
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
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) => {
|
|
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) => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = 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);
|
|
rmSync(parent);
|
|
renameSync(moved, parent);
|
|
expect(() => f.factory.acquireSession(f.snapshotPath)).not.toThrow();
|
|
lease.release();
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("release is idempotent and does not remove either durable publication", () => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = 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", () => {
|
|
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')`;
|
|
const output = execFileSync(python, ["-c", code], { encoding: "utf8" });
|
|
expect(output).toBe("partial-write-ok\n");
|
|
});
|
|
|
|
test("a clean existing equal publication is reconciled by a new factory", () => {
|
|
const f = fixture();
|
|
try {
|
|
const first = f.factory.acquireSession(f.snapshotPath);
|
|
const second = 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) => {
|
|
const f = fixture({ THT_RUNTIME_CONFIG_RENAME_FAIL: kind });
|
|
try {
|
|
expect(() => f.factory.acquireSession(f.snapshotPath)).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);
|
|
}
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("snapshot manifest rejects an undeclared extra immutable file", () => {
|
|
const f = fixture();
|
|
try {
|
|
const snapshot = JSON.parse(readFileSync(f.snapshotManifest, "utf8"));
|
|
writeFileSync(join(dirname(f.snapshotPath), "smuggled.txt"), "smuggled", { mode: 0o400 });
|
|
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);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
test("independent OS publishers converge when equal and elect one winner when unequal", () => {
|
|
const f = fixture();
|
|
try {
|
|
const seed = 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"]
|
|
.map((key) => [key, full[key]]));
|
|
const python = join(process.cwd(), "..", "harness", ".venv", "bin", "python");
|
|
const helper = join(process.cwd(), "..", "harness", "tht", "runtime_config_lease_io.py");
|
|
const configBytes = readFileSync(seed.path);
|
|
// 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,
|
|
workspace_id: workspace, workspace_revision: full.workspace_revision,
|
|
config_hex: Buffer.from(configBytes).toString("hex"), manifest_base: base });
|
|
const code = `import json,multiprocessing,sys
|
|
multiprocessing.set_start_method("fork")
|
|
sys.path.insert(0, ${JSON.stringify(dirname(helper))})
|
|
import runtime_config_lease_io as m
|
|
import contextlib,os
|
|
def run(x):
|
|
try:
|
|
with open(os.devnull,"w") as error, contextlib.redirect_stderr(error): m.publish(x)
|
|
except Exception: raise SystemExit(1)
|
|
x=json.loads(sys.argv[1]); y=json.loads(sys.argv[1])
|
|
if len(sys.argv)>2: y["config_hex"]="646966666572656e742d72756e74696d652d636f6e666967"
|
|
p=[multiprocessing.Process(target=run,args=(x,)),multiprocessing.Process(target=run,args=(y,))]
|
|
[q.start() for q in p]; [q.join() for q in p]
|
|
print(json.dumps([q.exitcode for q in p]))`
|
|
const equal = JSON.parse(execFileSync(python, ["-c", code, payload], { encoding: "utf8" }).trim());
|
|
expect(equal.sort()).toEqual([0, 0]);
|
|
rmSync(join(f.factoryInput.dataRoot, "sessions", workspace, "preprocessing", "runtime-config", `${full.workspace_revision}.yaml`));
|
|
rmSync(join(f.factoryInput.dataRoot, "sessions", workspace, "preprocessing", "runtime-config-manifests", `${full.workspace_revision}.json`));
|
|
const unequalPayload = JSON.stringify({ ...JSON.parse(payload), config_hex: Buffer.from("different-runtime-config").toString("hex") });
|
|
const unequal = JSON.parse(execFileSync(python, ["-c", code, payload, "unequal"], { encoding: "utf8" }).trim());
|
|
expect(unequal.filter((exit: number) => exit === 0)).toHaveLength(1);
|
|
expect(unequal.filter((exit: number) => exit !== 0)).toHaveLength(1);
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
test.each(["leaf", "ancestor"] as const)("actual harness rejects canonical %s swap", (kind) => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = 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 };
|
|
if (kind === "leaf") {
|
|
const moved = `${lease.path}.moved`;
|
|
renameSync(lease.path, moved);
|
|
execFileSync("ln", ["-s", moved, lease.path]);
|
|
} else {
|
|
const parent = dirname(lease.path);
|
|
const moved = `${parent}.moved`;
|
|
renameSync(parent, moved);
|
|
execFileSync("ln", ["-s", moved, parent]);
|
|
}
|
|
expect(() => execFileSync(harnessBin, ["config", "check", "-c", lease.path], {
|
|
cwd: harnessCwd, env, stdio: "pipe",
|
|
})).toThrow();
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
|
|
test("actual harness rejects a workspace chain swap between config and manifest traversal", () => {
|
|
const f = fixture();
|
|
try {
|
|
const lease = 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);
|
|
const code = `import os,sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, sys.argv[3])
|
|
from tht.config import ConfigError, _read_runtime_config_source
|
|
config_path = Path(sys.argv[1])
|
|
workspace = Path(sys.argv[2])
|
|
revision = config_path.stem
|
|
|
|
def swap_workspace_chain():
|
|
moved = Path(str(workspace) + ".moved")
|
|
os.rename(workspace, moved)
|
|
(workspace / "preprocessing").mkdir(parents=True, mode=0o700)
|
|
for name, leaf in (("runtime-config", revision + ".yaml"),
|
|
("runtime-config-manifests", revision + ".json")):
|
|
source = moved / "preprocessing" / name
|
|
destination = workspace / "preprocessing" / name
|
|
original_inode = os.stat(source).st_ino
|
|
original_leaf_inode = os.stat(source / leaf).st_ino
|
|
os.rename(source, destination)
|
|
assert os.stat(destination).st_ino == original_inode
|
|
assert os.stat(destination / leaf).st_ino == original_leaf_inode
|
|
|
|
try:
|
|
_read_runtime_config_source(
|
|
config_path,
|
|
between_config_and_manifest_traversal=swap_workspace_chain,
|
|
)
|
|
except ConfigError:
|
|
print("rejected")
|
|
else:
|
|
raise SystemExit("secure reader accepted a replaced workspace chain")
|
|
`;
|
|
const output = execFileSync(python, ["-c", code, lease.path, workspaceRoot, helper], {
|
|
cwd: join(process.cwd(), "..", "harness"),
|
|
env: { ...process.env, THT_RUNTIME_CONFIG_MANIFEST_SHA256: lease.manifestSha256 },
|
|
encoding: "utf8",
|
|
});
|
|
expect(output.trim()).toBe("rejected");
|
|
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
|
});
|