stabilize runtime state on native Windows

This commit is contained in:
Sinabina
2026-07-17 14:21:10 -07:00
parent 9b3188e74b
commit 9bb382f2ee
8 changed files with 129 additions and 33 deletions
@@ -7,8 +7,10 @@ import {
cleanupRuntime,
ensureManagedHome,
pathExists,
renameWithRetry,
resolveRuntimePaths,
runtimeLifecycleLockPath,
syncDirectory,
} from "../runtime/index.js";
const roots: string[] = [];
@@ -31,6 +33,44 @@ afterEach(async () => {
});
describe("runtime cleanup boundary", () => {
test("retries transient Windows lock creation and rename races", async () => {
const home = await temporaryHome();
const lockPath = path.join(home, "locks", "windows-race.lock");
let mkdirAttempts = 0;
const release = await acquireLock(lockPath, {
platform: "win32",
mkdir: async (target: string, options: Record<string, unknown>) => {
if (target === lockPath && mkdirAttempts++ === 0) {
throw Object.assign(new Error("simulated Windows delete race"), { code: "EPERM" });
}
return fs.mkdir(target, options);
},
});
expect(mkdirAttempts).toBe(2);
await release();
let renameAttempts = 0;
await renameWithRetry("source", "destination", {
platform: "win32",
timeoutMs: 100,
rename: async () => {
if (renameAttempts++ < 2) throw Object.assign(new Error("simulated scanner race"), { code: "EPERM" });
},
});
expect(renameAttempts).toBe(3);
});
test("closes a directory handle when Windows does not support directory fsync", async () => {
let closes = 0;
await expect(syncDirectory("fixture", {
open: async () => ({
sync: async () => { throw Object.assign(new Error("unsupported directory sync"), { code: "EPERM" }); },
close: async () => { closes += 1; },
}),
})).resolves.toBeUndefined();
expect(closes).toBe(1);
});
test("removes only allowlisted stale runtime scratch and dead locks", async () => {
const home = await temporaryHome();
const paths = resolveRuntimePaths({ home });
+6 -3
View File
@@ -31,8 +31,10 @@ async function temporaryRoot(label = "gstack2 runtime ") {
}
afterEach(async () => {
await Promise.all(temporaryRoots.splice(0).map((root) =>
fs.chmod(root, 0o700).catch(() => {}).then(() => fs.rm(root, { recursive: true, force: true }))));
for (const root of temporaryRoots.splice(0)) {
await fs.chmod(root, 0o700).catch(() => {});
await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
describe("gstack 2 host-neutral paths and state", () => {
@@ -92,10 +94,11 @@ describe("gstack 2 host-neutral paths and state", () => {
gitDir: path.join(root, "repo", ".git"),
});
await initializeProject(home, identity);
await Promise.all(Array.from({ length: 60 }, () =>
const updates = await Promise.allSettled(Array.from({ length: 60 }, () =>
updateProjectState(home, identity.projectId, (state) => {
state.concurrentCounter = Number(state.concurrentCounter ?? 0) + 1;
})));
expect(updates.filter((result) => result.status === "rejected")).toEqual([]);
const { state } = await inspectProject(home, identity);
expect(state.concurrentCounter).toBe(60);
expect(state.revision).toBe(60);
+1
View File
@@ -389,6 +389,7 @@ describe("GStack 2 managed runtime installer", () => {
const cli = path.join(result.path, "runtime", "cli.js");
const originalMode = (await fs.stat(cli)).mode & 0o777;
await fs.chmod(cli, originalMode === 0o600 ? 0o644 : 0o600);
await expect(validateRuntimeBundle(result.path, { version: "2.0.0", platform: "win32" })).resolves.toBe(true);
await expect(validateRuntimeBundle(result.path, { version: "2.0.0" })).rejects.toMatchObject({
code: "INSTALL_VALIDATION_FAILED",
});
+4 -4
View File
@@ -164,7 +164,7 @@ describe("managed-home destructive boundary", () => {
describe("one config authority", () => {
test("compatibility helper and runtime config share config.json", async () => {
const home = path.join(await root(), "state");
const run = (args: string[]) => spawnSync(configBin, args, {
const run = (args: string[]) => spawnSync(process.execPath, [configBin, ...args], {
encoding: "utf8",
env: { ...process.env, GSTACK_HOME: home },
});
@@ -201,20 +201,20 @@ describe("one config authority", () => {
const home = path.join(await root(), "legacy");
await fs.mkdir(home);
await fs.writeFile(path.join(home, "config.yaml"), "telemetry: community\n");
const get = spawnSync(configBin, ["get", "telemetry"], {
const get = spawnSync(process.execPath, [configBin, "get", "telemetry"], {
encoding: "utf8",
env: { ...process.env, GSTACK_HOME: home },
});
expect(get.status).toBe(0);
expect(get.stdout).toBe("community");
const set = spawnSync(configBin, ["set", "telemetry", "off"], {
const set = spawnSync(process.execPath, [configBin, "set", "telemetry", "off"], {
encoding: "utf8",
env: { ...process.env, GSTACK_HOME: home },
});
expect(set.status).toBe(0);
expect(await fs.readFile(path.join(home, "config.yaml"), "utf8")).toBe("telemetry: community\n");
expect(JSON.parse(await fs.readFile(path.join(home, "config.json"), "utf8")).telemetry).toBe("off");
const reread = spawnSync(configBin, ["get", "telemetry"], {
const reread = spawnSync(process.execPath, [configBin, "get", "telemetry"], {
encoding: "utf8",
env: { ...process.env, GSTACK_HOME: home },
});
+5 -3
View File
@@ -42,8 +42,9 @@ async function fixture(label = "gstack workflow state ", initialize = true) {
}
afterEach(async () => {
await Promise.all(temporaryRoots.splice(0).map((root) =>
fs.rm(root, { recursive: true, force: true })));
for (const root of temporaryRoots.splice(0)) {
await fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}
});
describe("GStack 2 authoritative workflow state", () => {
@@ -150,7 +151,7 @@ describe("GStack 2 authoritative workflow state", () => {
test("all workflow mutations are locked and concurrent evidence writes are not lost", async () => {
const { home, identity } = await fixture();
await beginRun(home, identity.projectId, "qa", { runId: "run_concurrent" });
await Promise.all(Array.from({ length: 24 }, (_, index) =>
const updates = await Promise.allSettled(Array.from({ length: 24 }, (_, index) =>
updateRunWorkflow(home, identity.projectId, "run_concurrent", {
addEvidenceProvenance: {
source: "local-test",
@@ -158,6 +159,7 @@ describe("GStack 2 authoritative workflow state", () => {
capturedAt: `2026-07-16T10:${String(index).padStart(2, "0")}:00.000Z`,
},
})));
expect(updates.filter((result) => result.status === "rejected")).toEqual([]);
const inspected = await inspectRun(home, identity.projectId, "run_concurrent");
expect(inspected.reconstruction.evidenceProvenance).toHaveLength(24);
expect(new Set(inspected.reconstruction.evidenceProvenance.map((entry: any) => entry.reference)).size).toBe(24);