fix: finalize durable Pi lifecycle
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "vitest";
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { MaintenanceBarrier } from "../src/runtime/maintenance-gate.js";
|
||||
@@ -45,3 +45,58 @@ test("durable activation survives recreation and deactivation removes the marker
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("activation reconciles active state when directory fsync fails after marker rename", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-activate-fsync-"));
|
||||
const marker = join(directory, "maintenance.json");
|
||||
try {
|
||||
const gate = new MaintenanceBarrier(marker, {
|
||||
syncDirectory() { throw new Error("injected post-rename fsync failure"); },
|
||||
});
|
||||
await expect(gate.activate()).rejects.toThrow(/post-rename fsync failure/);
|
||||
expect(existsSync(marker)).toBe(true);
|
||||
expect(gate.status()).toEqual({ active: true, admissions: 0 });
|
||||
expect(gate.acquire()).toBeUndefined();
|
||||
|
||||
const recovered = new MaintenanceBarrier(marker);
|
||||
expect(recovered.status()).toEqual({ active: true, admissions: 0 });
|
||||
expect(recovered.acquire()).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("deactivation reconciles inactive state when directory fsync fails after marker removal", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-deactivate-fsync-"));
|
||||
const marker = join(directory, "maintenance.json");
|
||||
let failSync = false;
|
||||
try {
|
||||
const gate = new MaintenanceBarrier(marker, {
|
||||
syncDirectory() {
|
||||
if (failSync) throw new Error("injected post-remove fsync failure");
|
||||
},
|
||||
});
|
||||
await gate.activate();
|
||||
failSync = true;
|
||||
expect(() => gate.deactivate()).toThrow(/post-remove fsync failure/);
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
expect(gate.status()).toEqual({ active: false, admissions: 0 });
|
||||
expect(gate.acquire()).toBeTypeOf("function");
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("status and admission recover from a persistent marker even when memory started inactive", () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "tht-maintenance-reconcile-"));
|
||||
const marker = join(directory, "maintenance.json");
|
||||
try {
|
||||
const gate = new MaintenanceBarrier(marker);
|
||||
expect(gate.status().active).toBe(false);
|
||||
writeFileSync(marker, '{"version":1,"active":true}\n', { mode: 0o600 });
|
||||
expect(gate.status()).toEqual({ active: true, admissions: 0 });
|
||||
expect(gate.acquire()).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,6 +152,38 @@ test.each(["none", "upstream"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
test("maintenance endpoints report marker-derived state after post-rename and post-remove fsync failures", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "tht-maintenance-endpoint-fsync-"));
|
||||
const marker = path.join(dir, "maintenance.json");
|
||||
let failSync = true;
|
||||
try {
|
||||
const maintenanceBarrier = new MaintenanceBarrier(marker, {
|
||||
syncDirectory() {
|
||||
if (failSync) throw new Error("injected maintenance fsync failure");
|
||||
},
|
||||
});
|
||||
const app = buildApp(loadConfig({
|
||||
AUTH_MODE: "none",
|
||||
THT_HARNESS_DIR: "../harness",
|
||||
THT_MAINTENANCE_FILE: marker,
|
||||
}), { thtRunner: {} as any, maintenanceBarrier });
|
||||
|
||||
const activated = await app.inject({ method: "POST", url: "/internal/maintenance/activate" });
|
||||
expect(activated.statusCode).toBe(500);
|
||||
expect(activated.json()).toMatchObject({ active: true, admissions: 0 });
|
||||
|
||||
failSync = false;
|
||||
expect((await app.inject({ method: "GET", url: "/internal/maintenance/status" })).json())
|
||||
.toEqual({ active: true, admissions: 0 });
|
||||
failSync = true;
|
||||
const deactivated = await app.inject({ method: "POST", url: "/internal/maintenance/deactivate" });
|
||||
expect(deactivated.statusCode).toBe(500);
|
||||
expect(deactivated.json()).toMatchObject({ active: false, admissions: 0 });
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("admin all-sessions response matches the authenticated lifecycle wire fixture", async () => {
|
||||
const fixture = JSON.parse(readFileSync(
|
||||
path.join(import.meta.dirname, "fixtures", "sessions-scope-all.json"),
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { test, expect } from "vitest";
|
||||
import { closeSync, fsyncSync, mkdtempSync, openSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { closeSync, existsSync, fsyncSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { loadSettings, saveSettings } from "../src/settings/settings-store.js";
|
||||
import {
|
||||
captureSettingsSnapshot,
|
||||
loadSettings,
|
||||
restoreSettingsSnapshot,
|
||||
saveSettings,
|
||||
} from "../src/settings/settings-store.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
|
||||
function cfgWith(file: string) {
|
||||
@@ -70,3 +75,25 @@ test("saveSettings restores the previous file when post-rename directory durabil
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("settings snapshots restore exact absent and empty-file states", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "tht-settings-snapshot-"));
|
||||
try {
|
||||
const file = join(dir, "settings.json");
|
||||
const cfg = cfgWith(file);
|
||||
const absent = captureSettingsSnapshot(cfg);
|
||||
expect(absent).toEqual({ exists: false, rawBase64: "" });
|
||||
saveSettings(cfg, { provider: "new", model: "model", thinking: "high" });
|
||||
restoreSettingsSnapshot(cfg, absent);
|
||||
expect(existsSync(file)).toBe(false);
|
||||
|
||||
writeFileSync(file, Buffer.alloc(0), { mode: 0o600 });
|
||||
const empty = captureSettingsSnapshot(cfg);
|
||||
expect(empty.exists).toBe(true);
|
||||
saveSettings(cfg, { provider: "new", model: "model", thinking: "high" });
|
||||
restoreSettingsSnapshot(cfg, empty);
|
||||
expect(readFileSync(file)).toEqual(Buffer.alloc(0));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -66,6 +66,8 @@ test("CLI accepts an explicit valid ID when a legacy filename contains dots", as
|
||||
test("declares a durable isolated registry volume and only read-only Git credential mounts", () => {
|
||||
const compose = readFileSync(new URL("../../compose.yaml", import.meta.url), "utf8");
|
||||
const development = readFileSync(new URL("../../docker-compose.dev.yml", import.meta.url), "utf8");
|
||||
const gitHttps = readFileSync(new URL("../../deploy/compose.git-https.yaml", import.meta.url), "utf8");
|
||||
const gitSsh = readFileSync(new URL("../../deploy/compose.git-ssh.yaml", import.meta.url), "utf8");
|
||||
const dockerfile = readFileSync(new URL("../../docker/core.Dockerfile", import.meta.url), "utf8");
|
||||
const smoke = readFileSync(new URL("../../scripts/workspace-registry-smoke.sh", import.meta.url), "utf8");
|
||||
|
||||
@@ -73,12 +75,14 @@ test("declares a durable isolated registry volume and only read-only Git credent
|
||||
expect(source).toContain("THT_WORKSPACE_REGISTRY_ROOT: /data/workspace-registry");
|
||||
expect(source).toContain("THT_WORKSPACE_GIT_REMOTE: ${THT_WORKSPACE_GIT_REMOTE:?set THT_WORKSPACE_GIT_REMOTE}");
|
||||
expect(source).toContain("workspace-registry:/data/workspace-registry");
|
||||
expect(source).toMatch(/workspace-registry-git-credentials:ro/);
|
||||
expect(source).toMatch(/workspace-registry-git-ca:ro/);
|
||||
expect(source).toMatch(/workspace-registry-git-ssh-key:ro/);
|
||||
expect(source).toMatch(/workspace-registry-git-known-hosts:ro/);
|
||||
}
|
||||
expect(dockerfile).toMatch(/mkdir -p \/data\/workspace-registry && chown -R thoth:thoth \/data\/workspace-registry/);
|
||||
expect(compose).not.toMatch(/workspace-registry-git-(?:credentials|ca|ssh-key|known-hosts):ro/);
|
||||
expect(gitHttps).toMatch(/workspace-registry-git-credentials:ro/);
|
||||
expect(gitHttps).toMatch(/workspace-registry-git-ca:ro/);
|
||||
expect(gitSsh).toMatch(/workspace-registry-git-ssh-key:ro/);
|
||||
expect(gitSsh).toMatch(/workspace-registry-git-known-hosts:ro/);
|
||||
expect(dockerfile).toMatch(/mkdir -p[^\n]*\/data\/workspace-registry/);
|
||||
expect(dockerfile).toMatch(/chown -R thoth:thoth \/home\/thoth\/\.pi \/data/);
|
||||
expect(smoke).toContain('core_remote="/fixtures/offline.git"');
|
||||
expect(smoke).toContain('"degraded":true');
|
||||
expect(smoke).toContain('core_remote="/fixtures/remote.git"');
|
||||
|
||||
Reference in New Issue
Block a user