From 0187eb067a26848cccc524749efd20a0bf9677d1 Mon Sep 17 00:00:00 2001 From: Jeffery Lofoneh Asamani Date: Thu, 1 Oct 2026 15:04:20 +0000 Subject: [PATCH] test: make the suite pass on Windows and run it there in CI Sync tests isolated the agent log locations through HOME only, but os.homedir() reads USERPROFILE on Windows, so syncs scanned the real user's logs. Stub HOME, USERPROFILE, APPDATA, LOCALAPPDATA and the XDG base directories to the temp home, and restore them after each test. The unknown-model test removed the pricing cache, so its sync fetched models.dev over the network and could time out on a slow connection, leaving that sync running into the next test. Keep it offline. The Amp fixtures used POSIX paths and drive-less file URLs, which Windows resolves differently. Build them from native paths. Run the whole suite in the Windows CI job instead of only the project path tests. --- .github/workflows/ci.yml | 4 ++-- src/lib/agents/adapters/core-json.test.ts | 11 +++++++---- src/lib/sync/sync.test.ts | 18 +++++++++++++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80266ca..a0fa47f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,5 +39,5 @@ jobs: - name: Install dependencies run: npm ci - - name: Test native and mocked Windows project paths - run: npx vitest run src/lib/usage + - name: Test on Windows + run: npm run test diff --git a/src/lib/agents/adapters/core-json.test.ts b/src/lib/agents/adapters/core-json.test.ts index c1aa611..cd430fc 100644 --- a/src/lib/agents/adapters/core-json.test.ts +++ b/src/lib/agents/adapters/core-json.test.ts @@ -1,6 +1,7 @@ import { mkdtemp, mkdir, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" -import { dirname, join } from "node:path" +import { dirname, join, resolve, sep } from "node:path" +import { pathToFileURL } from "node:url" import { describe, expect, test, vi } from "vitest" import type { AgentAdapter, DiscoveryContext, UsageSource } from "../types" import { ampAdapter } from "./amp" @@ -37,9 +38,11 @@ describe("whole-file JSON adapters", () => { const home = await mkdtemp(join(tmpdir(), "stats-amp-")) const data = join(home, "data") const path = join(data, "amp", "threads", "T-one.json") - await json(path, { id: "thread", created: 1767312000000, env: { initial: { workspaceRoot: "/tmp/worktree/", trees: [{ uri: "file:///other", repository: { url: "/work/other" } }, { uri: "file:///tmp/worktree", repository: { url: "/work/app/." } }] } }, messages: [{ role: "assistant", usage: { model: "wrong", inputTokens: 99 } }], usageLedger: { events: [{ timestamp: "2026-01-02T00:00:00Z", model: "claude-sonnet", credits: 0.25, tokens: { input: 11, output: 5, cacheReadInputTokens: 3, cacheCreationInputTokens: 2 } }] } }) + // Native paths and file URLs: Windows rejects a file URL without a drive. + const worktree = join(home, "worktree") + await json(path, { id: "thread", created: 1767312000000, env: { initial: { workspaceRoot: `${worktree}${sep}`, trees: [{ uri: pathToFileURL(join(home, "other")).href, repository: { url: "/work/other" } }, { uri: pathToFileURL(worktree).href, repository: { url: "/work/app/." } }] } }, messages: [{ role: "assistant", usage: { model: "wrong", inputTokens: 99 } }], usageLedger: { events: [{ timestamp: "2026-01-02T00:00:00Z", model: "claude-sonnet", credits: 0.25, tokens: { input: 11, output: 5, cacheReadInputTokens: 3, cacheCreationInputTokens: 2 } }] } }) const source = await first(ampAdapter, home, { XDG_DATA_HOME: data }) - expect((await ampAdapter.parse(source, parseContext())).events[0]).toMatchObject({ project: "/work/app", tokens: { input: 11, output: 5, cacheRead: 3, cacheWrite: 2, reasoning: 0 }, costUsd: 0.25, costSource: "reported" }) + expect((await ampAdapter.parse(source, parseContext())).events[0]).toMatchObject({ project: resolve("/work/app"), tokens: { input: 11, output: 5, cacheRead: 3, cacheWrite: 2, reasoning: 0 }, costUsd: 0.25, costSource: "reported" }) }) test("Amp uses timestamped usage from actor snapshots", async () => { @@ -48,7 +51,7 @@ describe("whole-file JSON adapters", () => { const path = join(data, "amp", "threads", "T-actor.json") await json(path, { id: "T-actor", created: 1767225600000, env: { initial: { workspaceRoot: "/work/app/" } }, messages: [{ messageId: "M-one", role: "assistant", createdAt: "2026-01-02T00:00:02Z", usage: { timestamp: "2026-01-02T00:00:01Z", model: "gpt-5", inputTokens: 7, outputTokens: 3 } }] }) const source = await first(ampAdapter, home, { XDG_DATA_HOME: data }) - expect((await ampAdapter.parse(source, parseContext())).events[0]).toMatchObject({ sessionId: "T-actor", project: "/work/app", timestamp: 1767312001000, model: "gpt-5", tokens: { input: 7, output: 3 } }) + expect((await ampAdapter.parse(source, parseContext())).events[0]).toMatchObject({ sessionId: "T-actor", project: resolve("/work/app"), timestamp: 1767312001000, model: "gpt-5", tokens: { input: 7, output: 3 } }) }) test("Droid maps settings totals and project sidecar directory", async () => { diff --git a/src/lib/sync/sync.test.ts b/src/lib/sync/sync.test.ts index d4993fa..226dcc9 100644 --- a/src/lib/sync/sync.test.ts +++ b/src/lib/sync/sync.test.ts @@ -8,7 +8,7 @@ import { import { tmpdir } from "node:os" import { join } from "node:path" import Database from "better-sqlite3" -import { afterEach, beforeEach, describe, expect, it } from "vitest" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { codexAdapter } from "../agents/adapters/codex" import { closeDb, getDb } from "../db/client.server" import { syncUsage } from "./sync.server" @@ -72,12 +72,21 @@ describe("syncUsage", () => { beforeEach(() => { home = mkdtempSync(join(tmpdir(), "ts-home-")) dataDir = mkdtempSync(join(tmpdir(), "ts-data-")) - process.env.TELEMETRY_STATS_DATA_DIR = dataDir - process.env.HOME = home + vi.stubEnv("TELEMETRY_STATS_DATA_DIR", dataDir) + // Point every base directory adapters search at the temp home, so a sync never + // reads the real agent logs. os.homedir() reads USERPROFILE on Windows, not HOME. + vi.stubEnv("HOME", home) + vi.stubEnv("USERPROFILE", home) + vi.stubEnv("APPDATA", join(home, "AppData", "Roaming")) + vi.stubEnv("LOCALAPPDATA", join(home, "AppData", "Local")) + vi.stubEnv("XDG_DATA_HOME", join(home, ".local", "share")) + vi.stubEnv("XDG_CONFIG_HOME", join(home, ".config")) writePricingCache() }) afterEach(() => { + vi.unstubAllEnvs() + vi.unstubAllGlobals() closeDb() rmSync(home, { recursive: true, force: true }) rmSync(dataDir, { recursive: true, force: true }) @@ -653,6 +662,9 @@ describe("syncUsage", () => { it("leaves costs null and unpriced for unknown models without a catalog", async () => { rmSync(join(dataDir, "models-dev.json")) + // Without a cached catalog the loader fetches models.dev; keep the test offline so + // a slow network can't time it out and leave its sync running into the next test. + vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("offline"))) writeOmpSession([ HEADER, JSON.stringify({