From 8d80cb120969ee9d24734d58ae276eb48522c0db Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 7 Apr 2026 12:47:30 -0500 Subject: [PATCH] test(gsd): add regression tests for wave 4 write safety Tests saveJsonFile atomic write correctness, no residual .tmp files, concurrent write safety, and round-trip through loadJsonFile. --- .../wave4-write-safety-regressions.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/wave4-write-safety-regressions.test.ts diff --git a/src/resources/extensions/gsd/tests/wave4-write-safety-regressions.test.ts b/src/resources/extensions/gsd/tests/wave4-write-safety-regressions.test.ts new file mode 100644 index 000000000..c5d12a51c --- /dev/null +++ b/src/resources/extensions/gsd/tests/wave4-write-safety-regressions.test.ts @@ -0,0 +1,70 @@ +// GSD State Machine — Wave 4 Write Safety Regression Tests +// Validates randomized tmp suffix in json-persistence and atomic writes. + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { saveJsonFile, loadJsonFile } from "../json-persistence.js"; + +// ── Fix 15: json-persistence uses randomized tmp suffix ── + +describe("saveJsonFile atomic write", () => { + test("writes JSON file correctly", () => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-json-test-")); + try { + const file = join(tmp, "test.json"); + saveJsonFile(file, { key: "value" }); + const content = JSON.parse(readFileSync(file, "utf-8")); + assert.deepStrictEqual(content, { key: "value" }); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); + + test("no .tmp file left after successful write", () => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-json-test-")); + try { + const file = join(tmp, "test.json"); + saveJsonFile(file, { data: 123 }); + const files = readdirSync(tmp); + const tmpFiles = files.filter((f: string) => f.includes(".tmp")); + assert.strictEqual(tmpFiles.length, 0, "No .tmp files should remain after write"); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); + + test("concurrent writes don't corrupt data", () => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-json-test-")); + try { + const file = join(tmp, "shared.json"); + // Write two different values rapidly — both should succeed without corruption + saveJsonFile(file, { writer: "first" }); + saveJsonFile(file, { writer: "second" }); + const content = JSON.parse(readFileSync(file, "utf-8")); + assert.strictEqual(content.writer, "second"); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); + + test("round-trip through loadJsonFile", () => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-json-test-")); + try { + const file = join(tmp, "roundtrip.json"); + const data = { items: [1, 2, 3], name: "test" }; + saveJsonFile(file, data); + const loaded = loadJsonFile( + file, + (d): d is typeof data => typeof d === "object" && d !== null && "items" in d, + () => ({ items: [], name: "" }), + ); + assert.deepStrictEqual(loaded.items, [1, 2, 3]); + assert.strictEqual(loaded.name, "test"); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); +});