Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 7 additions & 4 deletions src/lib/agents/adapters/core-json.test.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
18 changes: 15 additions & 3 deletions src/lib/sync/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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({
Expand Down
Loading