Merge PR #3735: fix(stats): avoid macOS stats sync worker abort (@roboomp)

# Conflicts:
#	packages/stats/src/parser.ts
#	packages/stats/test/parser-large-session.test.ts
This commit is contained in:
can1357
2026-06-29 16:48:59 +02:00
14 changed files with 367 additions and 295 deletions
+5 -33
View File
@@ -1,43 +1,15 @@
import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { getOverviewStats } from "@oh-my-pi/omp-stats/aggregator";
import { closeDb, getStatsByAgentType, initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import { getStatsByAgentType, initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import { classifyAgentType } from "@oh-my-pi/omp-stats/parser";
import type { AgentType, MessageStats } from "@oh-my-pi/omp-stats/types";
import {
getAgentDir,
getConfigRootDir,
getSessionsDir,
getStatsDbPath,
setAgentDir,
TempDir,
} from "@oh-my-pi/pi-utils";
import { getConfigRootDir, getSessionsDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-agent-type-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
installStatsTestIsolation("@pi-stats-agent-type-");
interface Tokens {
input: number;
+4 -25
View File
@@ -1,34 +1,13 @@
import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { syncAllSessions } from "@oh-my-pi/omp-stats/aggregator";
import { closeDb, getBehaviorOverall, getFileOffset, initDb } from "@oh-my-pi/omp-stats/db";
import { getAgentDir, getStatsDbPath, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { getAgentDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-behavior-backfill-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
installStatsTestIsolation("@pi-stats-behavior-backfill-");
async function writeSessionFile(): Promise<string> {
const sessionDir = path.join(getAgentDir(), "sessions", "--tmp--behavior-backfill");
+4 -26
View File
@@ -1,34 +1,12 @@
import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as os from "node:os";
import * as path from "node:path";
import { describe, expect, it } from "bun:test";
import { closeDb, getRecentRequests, initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import type { MessageStats } from "@oh-my-pi/omp-stats/types";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import { getAgentDir, getStatsDbPath, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { getStatsDbPath } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-db-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
installStatsTestIsolation("@pi-stats-db-");
function createCodexGptStats(entryId: string): MessageStats {
return {
+4 -27
View File
@@ -1,33 +1,10 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as os from "node:os";
import * as path from "node:path";
import { describe, expect, it } from "bun:test";
import { getDashboardStats } from "@oh-my-pi/omp-stats/aggregator";
import { closeDb, initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import { initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import type { MessageStats } from "@oh-my-pi/omp-stats/types";
import { getAgentDir, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-db-range-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
installStatsTestIsolation("@pi-stats-db-range-");
function makeMessage(timestamp: number, entryId: string): MessageStats {
return {
+4 -36
View File
@@ -1,46 +1,14 @@
import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { syncAllSessions } from "@oh-my-pi/omp-stats/aggregator";
import { closeDb, getOverallStats, getRecentRequests, initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import type { MessageStats } from "@oh-my-pi/omp-stats/types";
import { getAgentDir, getSessionsDir, getStatsDbPath, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { getSessionsDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const XDG_KEYS = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"] as const;
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
const originalXdg: Record<string, string | undefined> = {};
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-fork-dedup-");
for (const key of XDG_KEYS) {
originalXdg[key] = process.env[key];
delete process.env[key];
}
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
for (const key of XDG_KEYS) {
const prior = originalXdg[key];
if (prior === undefined) delete process.env[key];
else process.env[key] = prior;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
installStatsTestIsolation("@pi-stats-fork-dedup-");
interface AssistantOptions {
entryId: string;
+5 -26
View File
@@ -1,34 +1,13 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { closeDb, initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import { initDb, insertMessageStats } from "@oh-my-pi/omp-stats/db";
import { dedupeProjects, getGainDashboardStats, normalizeProjectPath } from "@oh-my-pi/omp-stats/gain-aggregator";
import type { MessageStats } from "@oh-my-pi/omp-stats/types";
import { getAgentDir, getStatsDbPath, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { getStatsDbPath } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-gain-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
installStatsTestIsolation("@pi-stats-gain-");
function makeMessage(sessionFile: string, folder: string, entryId: string, timestamp: number): MessageStats {
return {
+70
View File
@@ -0,0 +1,70 @@
/**
* Shared test isolation for stats Bun tests.
*
* The default profile's stats.db is redirected to `$XDG_DATA_HOME/omp/stats.db`
* by {@link DirResolver} whenever `agentDirOverride === defaultAgent`. Tests
* that only set `PI_CONFIG_DIR` + `setAgentDir(<home>/<config>/agent)` resolve
* to that default and silently share `stats.db` across files when an XDG
* variable is set (e.g. CI's `XDG_DATA_HOME`), producing the cross-test row
* pollution that fails `db-range`, `behavior-backfill`, `priority-premium-*`,
* and `agent-type` runs.
*
* `installStatsTestIsolation` snapshots and clears `XDG_*_HOME` plus
* `PI_CONFIG_DIR` for the test, points the agent directory at a fresh
* `TempDir`, closes the stats DB handle, and tears everything back down in the
* matching `afterEach`.
*/
import { afterEach, beforeEach } from "bun:test";
import * as os from "node:os";
import * as path from "node:path";
import { closeDb } from "@oh-my-pi/omp-stats/db";
import { getAgentDir, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
const XDG_KEYS = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"] as const;
export interface StatsTestIsolation {
/** Active per-test `TempDir`. Null between tests. */
current(): TempDir | null;
}
export function installStatsTestIsolation(prefix: string): StatsTestIsolation {
const originalAgentDir = getAgentDir();
let originalConfigDir: string | undefined;
const originalXdg: Record<string, string | undefined> = {};
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync(prefix);
originalConfigDir = process.env.PI_CONFIG_DIR;
for (const key of XDG_KEYS) {
originalXdg[key] = process.env[key];
delete process.env[key];
}
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
for (const key of XDG_KEYS) {
const prior = originalXdg[key];
if (prior === undefined) delete process.env[key];
else process.env[key] = prior;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
return {
current() {
return tempDir;
},
};
}
@@ -1,32 +1,56 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { afterEach, describe, expect, it, vi } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { parseSessionFile } from "@oh-my-pi/omp-stats/parser";
import { getAgentDir, getSessionsDir, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { getSessionsDir } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-large-session-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
installStatsTestIsolation("@pi-stats-large-session-");
afterEach(() => {
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
vi.restoreAllMocks();
});
async function writeLargeSessionFile(): Promise<string> {
const sessionDir = path.join(getSessionsDir(), "--tmp--large-session");
await fs.mkdir(sessionDir, { recursive: true });
const sessionFile = path.join(sessionDir, "session.jsonl");
const timestamp = new Date().toISOString();
const payload = "x".repeat(16 * 1024);
const lines: string[] = [];
for (let i = 0; i < 256; i++) {
lines.push(
JSON.stringify({
type: "message",
id: `assistant-${i}`,
parentId: null,
timestamp,
message: {
role: "assistant",
content: [{ type: "text", text: payload }],
api: "openai-responses",
provider: "openai",
model: "gpt-5.4",
usage: {
input: 1,
output: 2,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 3,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now() + i,
duration: 10,
ttft: 5,
},
}),
);
}
await Bun.write(sessionFile, `${lines.join("\n")}\n`);
return sessionFile;
}
describe("large session parsing", () => {
it("parses a JSONL chunk with more entries than the JavaScript argument limit", async () => {
const dir = path.join(getSessionsDir(), "--tmp--large-session");
@@ -43,4 +67,16 @@ describe("large session parsing", () => {
expect(result.userStats).toEqual([]);
expect(result.userLinks).toEqual([]);
});
it("parses multi-megabyte JSONL without entering Bun.JSONL.parseChunk", async () => {
const sessionFile = await writeLargeSessionFile();
vi.spyOn(Bun.JSONL, "parseChunk").mockImplementation(() => {
throw new Error("native JSONL parser unavailable");
});
const result = await parseSessionFile(sessionFile);
expect(result.stats).toHaveLength(256);
expect(result.newOffset).toBeGreaterThan(4 * 1024 * 1024);
});
});
@@ -1,35 +1,14 @@
import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { syncAllSessions } from "@oh-my-pi/omp-stats/aggregator";
import { closeDb, getOverallStats, getRecentRequests } from "@oh-my-pi/omp-stats/db";
import { parseSessionFile } from "@oh-my-pi/omp-stats/parser";
import { getAgentDir, getSessionsDir, getStatsDbPath, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { getSessionsDir, getStatsDbPath } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-priority-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
installStatsTestIsolation("@pi-stats-priority-");
interface SessionLines {
lines: Array<Record<string, unknown>>;
@@ -0,0 +1,21 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import { smokeTestSyncWorker } from "@oh-my-pi/omp-stats/aggregator";
import { installStatsTestIsolation } from "./helpers/temp-agent";
installStatsTestIsolation("@pi-stats-smoke-darwin-");
afterEach(() => {
vi.restoreAllMocks();
});
describe("smokeTestSyncWorker", () => {
it("skips the worker spawn on darwin so omp --smoke-test stays off the macOS abort surface", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
const workerSpy = vi.spyOn(globalThis, "Worker").mockImplementation(() => {
throw new Error("worker should not be created on darwin");
});
await expect(smokeTestSyncWorker()).resolves.toBeUndefined();
expect(workerSpy).not.toHaveBeenCalled();
});
});
+84
View File
@@ -0,0 +1,84 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { syncAllSessions } from "@oh-my-pi/omp-stats/aggregator";
import { getOverallStats } from "@oh-my-pi/omp-stats/db";
import { getSessionsDir } from "@oh-my-pi/pi-utils";
import { installStatsTestIsolation } from "./helpers/temp-agent";
installStatsTestIsolation("@pi-stats-sync-serial-");
afterEach(() => {
vi.restoreAllMocks();
});
async function writeSessionFile(): Promise<void> {
const sessionDir = path.join(getSessionsDir(), "--tmp--sync-serial");
await fs.mkdir(sessionDir, { recursive: true });
const timestamp = new Date().toISOString();
const sessionFile = path.join(sessionDir, "session.jsonl");
const assistant = {
type: "message",
id: "assistant-1",
parentId: null,
timestamp,
message: {
role: "assistant",
content: [{ type: "text", text: "ok" }],
api: "openai-responses",
provider: "openai",
model: "gpt-5.4",
usage: {
input: 1,
output: 2,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 3,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
duration: 10,
ttft: 5,
},
};
await Bun.write(sessionFile, `${JSON.stringify(assistant)}\n`);
}
describe("stats sync serial mode", () => {
it("honors workers: 1 without spawning a worker", async () => {
await writeSessionFile();
const workerSpy = vi.spyOn(globalThis, "Worker");
const synced = await syncAllSessions({ workers: 1 });
const overall = getOverallStats();
expect(synced.files).toBe(1);
expect(overall.totalRequests).toBe(1);
expect(workerSpy).not.toHaveBeenCalled();
});
it("uses the serial parser by default on macOS", async () => {
await writeSessionFile();
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
const workerSpy = vi.spyOn(globalThis, "Worker");
const synced = await syncAllSessions();
const overall = getOverallStats();
expect(synced.files).toBe(1);
expect(overall.totalRequests).toBe(1);
expect(workerSpy).not.toHaveBeenCalled();
});
it("spawns a worker pool when callers explicitly request workers: 2 with a single file", async () => {
await writeSessionFile();
const workerProbe = new Error("worker probe");
const workerSpy = vi.spyOn(globalThis, "Worker").mockImplementation(() => {
throw workerProbe;
});
await expect(syncAllSessions({ workers: 2 })).rejects.toBe(workerProbe);
expect(workerSpy).toHaveBeenCalled();
});
});