fix(gsd): wire setLogBasePath into engine init to resurrect audit log (#2745)

* fix: wire setLogBasePath into engine init to resurrect audit log

_auditBasePath was always null — setLogBasePath() existed but was never
called from any production code path. Every logWarning/logError call hit
the if (_auditBasePath) guard as false, so nothing was ever written to
.gsd/audit-log.jsonl.

Two independent fixes:
1. Remove _auditBasePath = null from _resetLogs() — the base path must
   survive unit resets, it's stable for process lifetime
2. Call setLogBasePath(base) after s.basePath = base in both the fresh-
   start path (bootstrapAutoSession) and the resume path (startAuto)

Adds two tests verifying disk persistence and that _resetLogs doesn't
kill the audit path.

Fixes #2722

* refactor: clean up audit log tests and avoid redundant mkdirSync

- Use makeTempDir/cleanup from test-utils.ts instead of inline mkdtempSync/rmSync
- Add afterEach in audit describe block to reset _auditBasePath via
  setLogBasePath("") — prevents state bleed into subsequent tests since
  _resetLogs() no longer clears it
- Drop four raw imports (mkdtempSync, rmSync, tmpdir — join was already used)
- Guard mkdirSync in _push() with _auditDirEnsured flag — was calling
  mkdirSync on every log entry; now called once per base path

* revert: remove _auditDirEnsured flag

mkdirSync({ recursive: true }) on an existing dir is a cheap stat, not
meaningful overhead on a low-frequency warn/error path. The flag added
mutable state for no real gain.
This commit is contained in:
Iouri Goussev 2026-03-26 18:06:48 -04:00 committed by GitHub
parent 913984c26e
commit a436f06e2d
4 changed files with 47 additions and 2 deletions

View file

@ -67,6 +67,7 @@ import {
getDebugLogPath,
} from "./debug-logger.js";
import { parseUnitId } from "./unit-id.js";
import { setLogBasePath } from "./workflow-logger.js";
import type { AutoSession } from "./auto/session.js";
import {
existsSync,
@ -461,6 +462,7 @@ export async function bootstrapAutoSession(
s.verbose = verboseMode;
s.cmdCtx = ctx;
s.basePath = base;
setLogBasePath(base);
s.unitDispatchCount.clear();
s.unitRecoveryCount.clear();
s.lastBudgetAlertLevel = 0;

View file

@ -114,6 +114,7 @@ import {
formatCost,
formatTokenCount,
} from "./metrics.js";
import { setLogBasePath } from "./workflow-logger.js";
import { join } from "node:path";
import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
import { atomicWriteSync } from "./atomic-write.js";
@ -1102,6 +1103,7 @@ export async function startAuto(
s.stepMode = requestedStepMode;
s.cmdCtx = ctx;
s.basePath = base;
setLogBasePath(base);
s.unitDispatchCount.clear();
s.unitLifetimeDispatches.clear();
if (!getLedger()) initMetrics(base);

View file

@ -1,8 +1,11 @@
// GSD Extension — Workflow Logger Tests
// Tests for the centralized warning/error accumulator.
import { describe, test, beforeEach } from "node:test";
import { describe, test, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { makeTempDir, cleanup } from "./test-utils.ts";
import {
logWarning,
logError,
@ -14,6 +17,7 @@ import {
hasAnyIssues,
summarizeLogs,
formatForNotification,
setLogBasePath,
_resetLogs,
} from "../workflow-logger.ts";
@ -222,6 +226,44 @@ describe("workflow-logger", () => {
});
});
describe("audit log persistence", () => {
let dir: string;
beforeEach(() => {
dir = makeTempDir("wl-audit-");
});
afterEach(() => {
setLogBasePath("");
cleanup(dir);
});
test("writes entry to .gsd/audit-log.jsonl after setLogBasePath", () => {
setLogBasePath(dir);
logWarning("engine", "audit test entry");
const auditPath = join(dir, ".gsd", "audit-log.jsonl");
assert.ok(existsSync(auditPath), "audit-log.jsonl should exist");
const content = readFileSync(auditPath, "utf-8");
const entry = JSON.parse(content.trim());
assert.equal(entry.severity, "warn");
assert.equal(entry.component, "engine");
assert.equal(entry.message, "audit test entry");
});
test("_resetLogs does not clear the audit base path", () => {
setLogBasePath(dir);
_resetLogs();
logWarning("engine", "post-reset entry");
const auditPath = join(dir, ".gsd", "audit-log.jsonl");
assert.ok(existsSync(auditPath), "audit-log.jsonl should exist after _resetLogs");
const content = readFileSync(auditPath, "utf-8");
const entry = JSON.parse(content.trim());
assert.equal(entry.message, "post-reset entry");
});
});
describe("buffer limit", () => {
test("caps at MAX_BUFFER entries, dropping oldest", () => {
const OVER = 110;

View file

@ -199,7 +199,6 @@ export function readAuditLog(basePath?: string): LogEntry[] {
*/
export function _resetLogs(): void {
_buffer = [];
_auditBasePath = null;
}
// ─── Internal ───────────────────────────────────────────────────────────