mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
stabilize runtime state on native Windows
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user