428 lines
16 KiB
TypeScript
428 lines
16 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import {
|
|
chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { afterEach, expect, test, vi } from "vitest";
|
|
import { parse } from "yaml";
|
|
import { buildApp } from "../src/app.js";
|
|
import { loadConfig } from "../src/config.js";
|
|
import { ThtRunner } from "../src/tht/tht-runner.js";
|
|
import { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
|
import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js";
|
|
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
|
|
|
const runFile = promisify(execFile);
|
|
const harnessDir = resolve("../harness");
|
|
const thtBin = join(harnessDir, ".venv", "bin", "tht");
|
|
const roots: string[] = [];
|
|
|
|
const canonicalWorkspace = `workspace:
|
|
schema_version: 4
|
|
id: psd-clinical
|
|
name: Runtime handoff
|
|
language: en
|
|
dwh:
|
|
engine: postgres
|
|
database: analytics
|
|
schema: mart
|
|
supported_transports: [postgres_direct]
|
|
`;
|
|
|
|
const filesystemWorkspace = `${canonicalWorkspace}evidence:
|
|
source:
|
|
type: filesystem
|
|
uri: psd-clinical/evidence
|
|
`;
|
|
|
|
function evidenceWorkspace(source: string, policy = ""): string {
|
|
return `${canonicalWorkspace}evidence:
|
|
source:
|
|
${source}${policy}`;
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
|
|
});
|
|
|
|
async function git(cwd: string, args: string[]): Promise<string> {
|
|
return (await runFile("git", args, { cwd })).stdout.trim();
|
|
}
|
|
|
|
async function fixture(workspaceSource = filesystemWorkspace) {
|
|
const root = mkdtempSync(join(tmpdir(), "tht-runtime-handoff-"));
|
|
roots.push(root);
|
|
const remote = join(root, "remote.git");
|
|
const source = join(root, "source");
|
|
const registryRoot = join(root, "registry");
|
|
const secretRoot = join(root, "secrets");
|
|
const dataRoot = join(root, "data");
|
|
await git(root, ["init", "--bare", "--initial-branch=main", remote]);
|
|
mkdirSync(source);
|
|
await git(source, ["init", "--initial-branch=main"]);
|
|
await git(source, ["config", "user.name", "Runtime Handoff Test"]);
|
|
await git(source, ["config", "user.email", "runtime-handoff@example.invalid"]);
|
|
writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces: [{id: psd-clinical, name: Runtime handoff}]\n");
|
|
mkdirSync(join(source, "psd-clinical"), { recursive: true });
|
|
writeFileSync(join(source, "psd-clinical", "workspace.yaml"), workspaceSource);
|
|
const evidenceRoot = join(source, "psd-clinical", "evidence");
|
|
mkdirSync(evidenceRoot, { recursive: true });
|
|
writeFileSync(join(evidenceRoot, "guide.md"), "# Immutable revision evidence\n");
|
|
await git(source, ["add", "."]);
|
|
await git(source, ["commit", "-m", "Canonical workspace"]);
|
|
await git(source, ["remote", "add", "origin", remote]);
|
|
await git(source, ["push", "origin", "main"]);
|
|
mkdirSync(secretRoot);
|
|
const secretContents: Record<string, string> = {
|
|
"dwh-password": "dwh-password-value",
|
|
"evidence-signed-urls.json": JSON.stringify([
|
|
"https://evidence.example.test/guide.md?token=SIGNED-HANDOFF-CANARY",
|
|
]),
|
|
"evidence-access": "ACCESS-HANDOFF-CANARY",
|
|
"evidence-secret": "SECRET-HANDOFF-CANARY",
|
|
"evidence-token": "TOKEN-HANDOFF-CANARY",
|
|
};
|
|
for (const [name, contents] of Object.entries(secretContents)) {
|
|
const path = join(secretRoot, name);
|
|
writeFileSync(path, contents, { mode: 0o600 });
|
|
chmodSync(path, 0o600);
|
|
}
|
|
mkdirSync(dataRoot);
|
|
const registryConfig: WorkspaceRegistryConfig = {
|
|
root: registryRoot,
|
|
remoteUrl: remote,
|
|
branch: "main",
|
|
gitAuthorName: "Runtime Handoff Test",
|
|
gitAuthorEmail: "runtime-handoff@example.invalid",
|
|
installationId: "test",
|
|
secretRoots: [secretRoot],
|
|
maxImportBytes: 1024 * 1024,
|
|
maxImportEntries: 16,
|
|
};
|
|
const registry = new WorkspaceRegistry(registryConfig);
|
|
await registry.bootstrap();
|
|
const revision = (await registry.list())[0];
|
|
const environment = {
|
|
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct",
|
|
THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.invalid",
|
|
THT_WS_PSD_CLINICAL_DWH_PORT: "5432",
|
|
THT_WS_PSD_CLINICAL_DWH_USER: "reader",
|
|
THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: join(secretRoot, "dwh-password"),
|
|
THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE: join(secretRoot, "evidence-signed-urls.json"),
|
|
THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE: join(secretRoot, "evidence-access"),
|
|
THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE: join(secretRoot, "evidence-secret"),
|
|
THT_WS_PSD_CLINICAL_EVIDENCE_SESSION_TOKEN_FILE: join(secretRoot, "evidence-token"),
|
|
};
|
|
for (const [name, value] of Object.entries(environment)) vi.stubEnv(name, value);
|
|
vi.stubEnv("THT_HOME", join(root, "home"));
|
|
return { root, source, dataRoot, secretRoot, registry, registryConfig, revision };
|
|
}
|
|
|
|
function runnerFor(f: Awaited<ReturnType<typeof fixture>>): ThtRunner {
|
|
return new ThtRunner({
|
|
thtBin,
|
|
harnessDir,
|
|
configPath: "config/tht.yaml",
|
|
dataRoot: f.dataRoot,
|
|
runtimeSnapshotRoot: join(f.registryConfig.root, "snapshots", "runtime"),
|
|
secretRoots: f.registryConfig.secretRoots,
|
|
} as any);
|
|
}
|
|
|
|
test("real schema-v4 registry revision loads through ThtRunner and the harness contract", async () => {
|
|
const f = await fixture();
|
|
const runner = runnerFor(f);
|
|
|
|
expect(await runner.sessionList(f.revision.snapshotPath)).toEqual([]);
|
|
const created = await runner.sessionNew({
|
|
question: "runtime handoff",
|
|
workspaceConfigPath: f.revision.snapshotPath,
|
|
workspaceId: f.revision.id,
|
|
workspaceRevision: f.revision.commit,
|
|
});
|
|
expect(await runner.sessionShow(created.id, f.revision.snapshotPath)).toMatchObject({
|
|
id: created.id,
|
|
workspace_id: "psd-clinical",
|
|
workspace_revision: f.revision.commit,
|
|
});
|
|
expect(existsSync(join(
|
|
f.dataRoot, "sessions", "psd-clinical", "sessions", created.id, "session_manifest.yaml",
|
|
))).toBe(true);
|
|
expect(readdirSync(join(f.registryConfig.root, "snapshots", "runtime"))).toEqual([]);
|
|
});
|
|
|
|
test("ThtRunner uses a vault secret only for the lifetime of its runtime lease", async () => {
|
|
const f = await fixture();
|
|
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE", "");
|
|
const vaultRoot = join(f.root, "workspace-secrets");
|
|
const runtimeRoot = join(f.root, "workspace-secret-runtime");
|
|
const secretStore = new WorkspaceSecretStore({
|
|
root: vaultRoot,
|
|
runtimeRoot,
|
|
installationId: "test",
|
|
});
|
|
secretStore.put("psd-clinical", "dwh.password", "vault-runtime-password");
|
|
const runner = new ThtRunner({
|
|
thtBin,
|
|
harnessDir,
|
|
configPath: "config/tht.yaml",
|
|
dataRoot: f.dataRoot,
|
|
runtimeSnapshotRoot: join(f.registryConfig.root, "snapshots", "runtime"),
|
|
secretRoots: f.registryConfig.secretRoots,
|
|
workspaceSecretStore: secretStore,
|
|
} as any);
|
|
|
|
const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
|
const rendered = parse(readFileSync(lease.path, "utf8")) as {
|
|
database: { password_file: string };
|
|
};
|
|
expect(readFileSync(rendered.database.password_file, "utf8")).toBe("vault-runtime-password");
|
|
lease.release();
|
|
expect(existsSync(rendered.database.password_file)).toBe(false);
|
|
});
|
|
|
|
test("ThtRunner binds and cleans the effective relationship snapshot with its runtime lease", async () => {
|
|
const f = await fixture();
|
|
const runner = runnerFor(f);
|
|
const relationships = JSON.stringify({
|
|
schemaVersion: 1,
|
|
workspaceId: "psd-clinical",
|
|
relationships: [],
|
|
});
|
|
|
|
const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath, relationships);
|
|
const rendered = parse(readFileSync(lease.path, "utf8")) as {
|
|
paths: { effective_relationships: string };
|
|
};
|
|
|
|
expect(readFileSync(rendered.paths.effective_relationships, "utf8")).toBe(relationships);
|
|
lease.release();
|
|
expect(existsSync(rendered.paths.effective_relationships)).toBe(false);
|
|
});
|
|
|
|
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 expectedRoot = join(
|
|
f.registryConfig.root,
|
|
"snapshots",
|
|
f.revision.commit,
|
|
"psd-clinical",
|
|
"evidence",
|
|
);
|
|
|
|
try {
|
|
expect(first.path).not.toBe(second.path);
|
|
const firstYaml = readFileSync(first.path, "utf8");
|
|
const secondYaml = readFileSync(second.path, "utf8");
|
|
expect(secondYaml).toBe(firstYaml);
|
|
expect(parse(firstYaml).runtime_identity).toEqual({
|
|
workspace_id: "psd-clinical",
|
|
workspace_revision: f.revision.commit,
|
|
source_identity: "workspace://psd-clinical",
|
|
});
|
|
expect(parse(firstYaml).evidence).toEqual({
|
|
sources: [{
|
|
type: "filesystem",
|
|
root: expectedRoot,
|
|
patterns: ["**/*.md"],
|
|
max_bytes: 10_485_760,
|
|
}],
|
|
});
|
|
expect(parse(firstYaml).vector).toEqual({
|
|
max_chunk_chars: 4_000,
|
|
retain_published_generations: 3,
|
|
});
|
|
expect(expectedRoot).not.toContain(join(f.registryConfig.root, "repo"));
|
|
|
|
for (const lease of [first, second]) {
|
|
const checked = await runFile(thtBin, ["config", "check", "-c", lease.path], {
|
|
cwd: harnessDir,
|
|
env: { ...process.env, THT_HOME: join(f.root, "home") },
|
|
});
|
|
expect(`${checked.stdout}${checked.stderr}`).not.toContain("HANDOFF-CANARY");
|
|
}
|
|
|
|
first.release();
|
|
expect(existsSync(first.path)).toBe(false);
|
|
expect(existsSync(second.path)).toBe(true);
|
|
second.release();
|
|
expect(existsSync(second.path)).toBe(false);
|
|
} finally {
|
|
first.release();
|
|
second.release();
|
|
}
|
|
});
|
|
|
|
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 descriptorBefore = readFileSync(f.revision.snapshotPath, "utf8");
|
|
writeFileSync(
|
|
join(f.source, "psd-clinical", "evidence", "guide.md"),
|
|
"# Content-only revision two\n",
|
|
);
|
|
await git(f.source, ["add", "psd-clinical/evidence/guide.md"]);
|
|
await git(f.source, ["commit", "-m", "Update Evidence content only"]);
|
|
await git(f.source, ["push", "origin", "main"]);
|
|
await f.registry.pull();
|
|
const current = (await f.registry.list())[0];
|
|
const second = runner.acquireWorkspaceRuntime(current.snapshotPath);
|
|
|
|
try {
|
|
expect(current.commit).not.toBe(f.revision.commit);
|
|
expect(current.blob).toBe(f.revision.blob);
|
|
expect(readFileSync(current.snapshotPath, "utf8")).toBe(descriptorBefore);
|
|
const firstRendered = parse(readFileSync(first.path, "utf8"));
|
|
const secondRendered = parse(readFileSync(second.path, "utf8"));
|
|
expect(firstRendered.runtime_identity.workspace_revision).toBe(f.revision.commit);
|
|
expect(secondRendered.runtime_identity.workspace_revision).toBe(current.commit);
|
|
expect(secondRendered.evidence.sources[0].root).toBe(join(
|
|
f.registryConfig.root,
|
|
"snapshots",
|
|
current.commit,
|
|
"psd-clinical",
|
|
"evidence",
|
|
));
|
|
expect(secondRendered.evidence.sources[0].root).not.toBe(firstRendered.evidence.sources[0].root);
|
|
|
|
for (const lease of [first, second]) {
|
|
await expect(runFile(thtBin, ["config", "check", "-c", lease.path], {
|
|
cwd: harnessDir,
|
|
env: { ...process.env, THT_HOME: join(f.root, "home") },
|
|
})).resolves.toBeDefined();
|
|
}
|
|
} finally {
|
|
first.release();
|
|
second.release();
|
|
}
|
|
});
|
|
|
|
test("signed HTTP Evidence resolves its file binding and config check never captures its contents", async () => {
|
|
const f = await fixture(evidenceWorkspace(` type: http
|
|
uris: [https://evidence.example.test/guide.md]
|
|
authentication: signed_urls_file
|
|
connect_timeout_ms: 1250
|
|
read_timeout_ms: 30001
|
|
max_bytes: 12345
|
|
max_redirects: 2
|
|
allow_private_hosts: false
|
|
max_cache_bytes: 67890
|
|
`));
|
|
const runner = runnerFor(f);
|
|
const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
|
try {
|
|
const yaml = readFileSync(lease.path, "utf8");
|
|
expect(parse(yaml).evidence.sources).toEqual([{
|
|
type: "http",
|
|
provenance_urls: ["https://evidence.example.test/guide.md"],
|
|
signed_urls_file: realpathSync(join(f.secretRoot, "evidence-signed-urls.json")),
|
|
connect_timeout: 1.25,
|
|
read_timeout: 30.001,
|
|
max_bytes: 12_345,
|
|
max_redirects: 2,
|
|
allow_private_hosts: false,
|
|
max_cache_bytes: 67_890,
|
|
}]);
|
|
expect(yaml).not.toContain("SIGNED-HANDOFF-CANARY");
|
|
|
|
const checked = await runFile(thtBin, ["config", "check", "-c", lease.path], {
|
|
cwd: harnessDir,
|
|
env: { ...process.env, THT_HOME: join(f.root, "home") },
|
|
});
|
|
expect(`${checked.stdout}${checked.stderr}`).not.toContain("SIGNED-HANDOFF-CANARY");
|
|
} finally {
|
|
lease.release();
|
|
}
|
|
});
|
|
|
|
test("static S3 Evidence resolves only configured secret-root file paths", async () => {
|
|
const f = await fixture(evidenceWorkspace(` type: s3
|
|
uri: s3://clinical-evidence/published/
|
|
endpoint_url: https://s3.example.test/
|
|
region: eu-west-1
|
|
credentials: static_files
|
|
trusted_endpoint: true
|
|
allow_private_endpoint: true
|
|
allow_insecure_endpoint: false
|
|
max_bytes: 222
|
|
max_objects: 33
|
|
max_pages: 4
|
|
page_size: 5
|
|
`, ` policy:
|
|
max_chunk_chars: 2500
|
|
retain_published_generations: 7
|
|
`));
|
|
const runner = runnerFor(f);
|
|
const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
|
try {
|
|
const yaml = readFileSync(lease.path, "utf8");
|
|
expect(parse(yaml).evidence.sources).toEqual([{
|
|
type: "s3",
|
|
bucket: "clinical-evidence",
|
|
prefix: "published/",
|
|
endpoint_url: "https://s3.example.test/",
|
|
region: "eu-west-1",
|
|
access_key_file: realpathSync(join(f.secretRoot, "evidence-access")),
|
|
secret_key_file: realpathSync(join(f.secretRoot, "evidence-secret")),
|
|
session_token_file: realpathSync(join(f.secretRoot, "evidence-token")),
|
|
trusted_endpoint: true,
|
|
allow_private_endpoint: true,
|
|
allow_insecure_endpoint: false,
|
|
max_bytes: 222,
|
|
max_objects: 33,
|
|
max_pages: 4,
|
|
page_size: 5,
|
|
}]);
|
|
expect(parse(yaml).vector).toEqual({
|
|
max_chunk_chars: 2_500,
|
|
retain_published_generations: 7,
|
|
});
|
|
for (const canary of ["ACCESS-HANDOFF-CANARY", "SECRET-HANDOFF-CANARY", "TOKEN-HANDOFF-CANARY"]) {
|
|
expect(yaml).not.toContain(canary);
|
|
}
|
|
|
|
const checked = await runFile(thtBin, ["config", "check", "-c", lease.path], {
|
|
cwd: harnessDir,
|
|
env: { ...process.env, THT_HOME: join(f.root, "home") },
|
|
});
|
|
const output = `${checked.stdout}${checked.stderr}`;
|
|
for (const canary of ["ACCESS-HANDOFF-CANARY", "SECRET-HANDOFF-CANARY", "TOKEN-HANDOFF-CANARY"]) {
|
|
expect(output).not.toContain(canary);
|
|
}
|
|
} finally {
|
|
lease.release();
|
|
}
|
|
});
|
|
|
|
test("local GET sessions mine uses the real canonical handoff and returns an empty inventory", async () => {
|
|
const f = await fixture();
|
|
const app = buildApp(loadConfig({
|
|
AUTH_MODE: "none",
|
|
THT_HARNESS_DIR: harnessDir,
|
|
THT_BIN: thtBin,
|
|
THT_DATA_ROOT: f.dataRoot,
|
|
THT_WORKSPACE_REGISTRY_ROOT: f.registryConfig.root,
|
|
THT_WORKSPACE_GIT_REMOTE: f.registryConfig.remoteUrl,
|
|
THT_WORKSPACE_SECRET_ROOTS: f.registryConfig.secretRoots.join(","),
|
|
}), {
|
|
thtRunner: runnerFor(f),
|
|
workspaceRegistry: f.registry,
|
|
mgr: { get: () => undefined } as any,
|
|
});
|
|
try {
|
|
const response = await app.inject({ method: "GET", url: "/sessions?scope=mine" });
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json()).toEqual([]);
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|