Skip to content

Commit 64677b0

Browse files
committed
fix(os-summary): cache Darwin sw_vers product version across prompt callers (#95145)
Addresses the reviewer P1 from clawsweeper on PR #95189: resolveRuntimePromptOs() is invoked from embedded attempt, compaction, and CLI runtime-prompt callers on every prompt build, so the Darwin branch was synchronously spawning sw_vers once per prompt without reuse. resolveOsSummary() already caches its full summary by platform/release/arch tuple, but the underlying macosProductVersion() helper spawned sw_vers unconditionally on each call — so resolveRuntimePromptOs's direct call to macosProductVersion() bypassed the existing cache entirely. Add a module-level cachedMacosProductVersionByRelease map keyed by os.release(). The macOS marketing product version is stable for the life of a process for a given kernel release; once resolved, both resolveOsSummary() and resolveRuntimePromptOs() reuse the cached string and no longer re-spawn sw_vers. Non-Darwin output is unchanged. Also export a test-only __resetOsSummaryCachesForTests() helper that clears both module-level caches, and wire beforeEach() hooks into the existing test file. Without the reset, the new cache exposes a pre-existing test-isolation gap — the 'falls back to os.release on darwin when sw_vers returns blank' case reuses kernel release 25.5.0 from an earlier case that already cached '26.5.1', so the cache would return the earlier mock and the fallback case would never exercise its fallback. The reset hook makes each test case authoritative for its own mock. Two new cache regression tests: - reuses cached macOS product version across repeated calls (asserts spawnSync is called exactly once across 3 invocations) - runtime-prompt and os-summary share the cached Darwin product version (asserts resolveRuntimePromptOs called after resolveOsSummary for the same kernel release does not re-spawn sw_vers — the two helpers share the module-level cache) Verification: - node scripts/run-vitest.mjs src/infra/os-summary.test.ts src/agents/system-prompt.test.ts src/agents/embedded-agent-runner/run/attempt-system-prompt.test.ts → all 3 shards pass (11 + 1 + 4 tests) - pnpm tsgo:core → clean - pnpm tsgo:core:test → clean
1 parent 8b3cb34 commit 64677b0

2 files changed

Lines changed: 111 additions & 3 deletions

File tree

src/infra/os-summary.test.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Tests operating system summary collection and normalization.
22
import os from "node:os";
3-
import { afterEach, describe, expect, it, vi } from "vitest";
3+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44

55
const spawnSyncMock = vi.hoisted(() => vi.fn());
66

@@ -11,7 +11,11 @@ vi.mock("node:child_process", async () => {
1111
);
1212
});
1313

14-
import { resolveOsSummary, resolveRuntimePromptOs } from "./os-summary.js";
14+
import {
15+
__resetOsSummaryCachesForTests,
16+
resolveOsSummary,
17+
resolveRuntimePromptOs,
18+
} from "./os-summary.js";
1519

1620
type OsSummaryCase = {
1721
name: string;
@@ -23,6 +27,9 @@ type OsSummaryCase = {
2327
};
2428

2529
describe("resolveOsSummary", () => {
30+
beforeEach(() => {
31+
__resetOsSummaryCachesForTests();
32+
});
2633
afterEach(() => {
2734
vi.restoreAllMocks();
2835
});
@@ -106,6 +113,9 @@ type RuntimePromptOsCase = {
106113
};
107114

108115
describe("resolveRuntimePromptOs", () => {
116+
beforeEach(() => {
117+
__resetOsSummaryCachesForTests();
118+
});
109119
afterEach(() => {
110120
vi.restoreAllMocks();
111121
});
@@ -166,3 +176,72 @@ describe("resolveRuntimePromptOs", () => {
166176
expect(resolveRuntimePromptOs()).toBe(expected);
167177
});
168178
});
179+
180+
describe("resolveRuntimePromptOs caching", () => {
181+
beforeEach(() => {
182+
__resetOsSummaryCachesForTests();
183+
});
184+
afterEach(() => {
185+
vi.restoreAllMocks();
186+
});
187+
188+
it("reuses cached macOS product version across repeated calls (PR #95189 reviewer P1)", () => {
189+
// Use a kernel release no other test case in this file uses, so the
190+
// module-level cache miss is exclusively this test's responsibility.
191+
vi.spyOn(os, "platform").mockReturnValue("darwin");
192+
vi.spyOn(os, "release").mockReturnValue("25.7.0");
193+
spawnSyncMock.mockReturnValue({
194+
stdout: "26.7.0\n",
195+
stderr: "",
196+
pid: 1,
197+
output: [],
198+
status: 0,
199+
signal: null,
200+
});
201+
202+
const callsBefore = spawnSyncMock.mock.calls.length;
203+
const first = resolveRuntimePromptOs();
204+
const callsAfterFirst = spawnSyncMock.mock.calls.length;
205+
const second = resolveRuntimePromptOs();
206+
const third = resolveRuntimePromptOs();
207+
const callsAfterThird = spawnSyncMock.mock.calls.length;
208+
209+
expect(first).toBe("macOS 26.7.0");
210+
expect(second).toBe("macOS 26.7.0");
211+
expect(third).toBe("macOS 26.7.0");
212+
// First call must spawn sw_vers exactly once; subsequent calls must reuse
213+
// the cached product version and not re-spawn the subprocess.
214+
expect(callsAfterFirst - callsBefore).toBe(1);
215+
expect(callsAfterThird - callsAfterFirst).toBe(0);
216+
});
217+
218+
it("runtime-prompt and os-summary share the cached Darwin product version", () => {
219+
// Same kernel release across both call paths: the second path must not
220+
// spawn sw_vers again, because the cache is keyed by os.release().
221+
vi.spyOn(os, "platform").mockReturnValue("darwin");
222+
vi.spyOn(os, "release").mockReturnValue("25.8.0");
223+
vi.spyOn(os, "arch").mockReturnValue("arm64");
224+
spawnSyncMock.mockReturnValue({
225+
stdout: "26.8.0\n",
226+
stderr: "",
227+
pid: 1,
228+
output: [],
229+
status: 0,
230+
signal: null,
231+
});
232+
233+
const callsBefore = spawnSyncMock.mock.calls.length;
234+
const summaryLabel = resolveOsSummary().label;
235+
const callsAfterSummary = spawnSyncMock.mock.calls.length;
236+
const runtimeLabel = resolveRuntimePromptOs();
237+
const callsAfterRuntime = spawnSyncMock.mock.calls.length;
238+
239+
expect(summaryLabel).toBe("macos 26.8.0 (arm64)");
240+
expect(runtimeLabel).toBe("macOS 26.8.0");
241+
expect(callsAfterSummary - callsBefore).toBe(1);
242+
// resolveRuntimePromptOs called after resolveOsSummary for the same
243+
// kernel release must not re-spawn sw_vers — the two helpers share the
244+
// module-level cachedMacosProductVersionByRelease map.
245+
expect(callsAfterRuntime - callsAfterSummary).toBe(0);
246+
});
247+
});

src/infra/os-summary.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,39 @@ type OsSummary = {
1212

1313
const cachedOsSummaryByKey = new Map<string, OsSummary>();
1414

15+
/**
16+
* Cache for the slow Darwin `sw_vers -productVersion` lookup, keyed by
17+
* `os.release()` (the kernel release a given binary is observing). Once
18+
* resolved for a given kernel release, the macOS marketing product version
19+
* is stable for the lifetime of the process — there is no scenario where
20+
* macOS changes its product version without the kernel release changing
21+
* too — so re-spawning `sw_vers` per call only burns latency on every
22+
* runtime prompt build (#95145 review feedback on PR #95189).
23+
*/
24+
const cachedMacosProductVersionByRelease = new Map<string, string>();
25+
1526
function macosProductVersion(): string {
27+
const release = os.release();
28+
const cached = cachedMacosProductVersionByRelease.get(release);
29+
if (cached !== undefined) {
30+
return cached;
31+
}
1632
const res = spawnSync("sw_vers", ["-productVersion"], { encoding: "utf-8" });
1733
const out = normalizeOptionalString(res.stdout) ?? "";
18-
return out || os.release();
34+
const resolved = out || release;
35+
cachedMacosProductVersionByRelease.set(release, resolved);
36+
return resolved;
37+
}
38+
39+
/**
40+
* Test-only: clear both module-level caches so each test case can mock a
41+
* different `os.release()` without leaking the previous case's resolved
42+
* Darwin marketing version. Not part of the public API — prefer keying tests
43+
* by unique kernel releases when possible.
44+
*/
45+
export function __resetOsSummaryCachesForTests(): void {
46+
cachedOsSummaryByKey.clear();
47+
cachedMacosProductVersionByRelease.clear();
1948
}
2049

2150
/** Resolves a compact OS label for diagnostics, logs, and environment summaries. */

0 commit comments

Comments
 (0)