From a436f06e2ddc394c100c2e84c7c3f52ea280a3e8 Mon Sep 17 00:00:00 2001 From: Iouri Goussev Date: Thu, 26 Mar 2026 18:06:48 -0400 Subject: [PATCH] fix(gsd): wire setLogBasePath into engine init to resurrect audit log (#2745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- src/resources/extensions/gsd/auto-start.ts | 2 + src/resources/extensions/gsd/auto.ts | 2 + .../gsd/tests/workflow-logger.test.ts | 44 ++++++++++++++++++- .../extensions/gsd/workflow-logger.ts | 1 - 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index f8013394a..f0b45a04e 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -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; diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 73ce6fd16..1a9eff6d7 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -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); diff --git a/src/resources/extensions/gsd/tests/workflow-logger.test.ts b/src/resources/extensions/gsd/tests/workflow-logger.test.ts index db7fbb5b8..911c0d770 100644 --- a/src/resources/extensions/gsd/tests/workflow-logger.test.ts +++ b/src/resources/extensions/gsd/tests/workflow-logger.test.ts @@ -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; diff --git a/src/resources/extensions/gsd/workflow-logger.ts b/src/resources/extensions/gsd/workflow-logger.ts index 35e79bde5..0770408d0 100644 --- a/src/resources/extensions/gsd/workflow-logger.ts +++ b/src/resources/extensions/gsd/workflow-logger.ts @@ -199,7 +199,6 @@ export function readAuditLog(basePath?: string): LogEntry[] { */ export function _resetLogs(): void { _buffer = []; - _auditBasePath = null; } // ─── Internal ───────────────────────────────────────────────────────────