Files
mksglu__context-mode/tests/session/parse-git-invocation.test.ts
Mert Koseoglu ee71b5ae56 fix: cost_usd derivation + tilde/equals-form git parsing (Gaps #1, #2)
Closes Gap #1 (P1) and Gap #2 (P2) per 16-oss-verify-gap-prd §2.

GAP #1 — AgentOutput.usage cost_usd derivation
  Adds MODEL_PRICING_USD_PER_MTOK constant (verified against
  platform.claude.com/docs/en/about-claude/pricing and CloudZero 2026
  pricing analysis) and derives cost_usd inside extractAgentUsage.
  Verified rates ($/MTok):
    opus-4-7:    5.00 / 25.00 / 6.25 / 0.50   (input/output/cache_write/cache_read)
    sonnet-4-6:  3.00 / 15.00 / 3.75 / 0.30
    haiku-4-5:   1.00 /  5.00 / 1.25 / 0.10
    default:     sonnet-4-6 rates (safe forward-compat for new model ids)

  The verify-gap PRD originally cited Opus at $15/$75 — that is the
  prior Opus 4 rate; Opus 4.7 ships at $5/$25 (confirmed via WebSearch
  against CloudZero, finout.io, platform.claude.com). The constant
  ships with the corrected values.

  Resolution order: tool_input.model → input.model → response.model →
  "default". Unknown model id falls back to default Sonnet rates so the
  cost column always has SOMETHING — dashboards can flag default-priced
  rows as approximations.

  Zero-token case skips cost_usd so dashboards never show misleading
  "$0.00 for nothing" rows.

  src/session/extract.ts:1259-1313 — MODEL_PRICING constant + helpers
  src/session/extract.ts:1361-1376 — cost_usd append in extractAgentUsage
  tests/session/extract-agent-usage.test.ts +6 tracers (Sonnet 0.0105,
    Opus > Sonnet, Haiku < Sonnet, cache pricing, default fallback,
    zero-token skip)

GAP #2 — git -C tilde expansion + --directory=/path equals-form
  Two surface-level parsing fixes in parseGitInvocation:

  1. `--directory=value` equals-form now parsed. Tokenizer treats it as
     one token; parser slices on the literal "--directory=" prefix.
  2. Leading `~` or `~/path` expanded to $HOME (or USERPROFILE on
     Windows, or HOMEDRIVE+HOMEPATH fallback). `~user/path` (other
     user) is left unchanged since the bridge does not do passwd
     lookups.

  src/session/extract.ts:474-491 — expandHomeTilde + getHomedirSafe
  src/session/extract.ts:541-547 — --directory= equals-form parsing
  src/session/extract.ts:565    — expansion applied at exit
  tests/session/parse-git-invocation.test.ts — 9 tracers including the
    real-world case (`git -C ~/Server/Mert/foo commit -m "x"` from
    a different cwd attributes to ~/Server/Mert/foo)

Test plan:
- npx vitest run tests/session/extract-agent-usage.test.ts -> 17/17 GREEN
- npx vitest run tests/session/parse-git-invocation.test.ts -> 9/9 GREEN
- regression sweep across extract-git-commit + cross-project-attribution -> 42/42 GREEN

Platform-coordination: cost_usd field rides {...ev} envelope per PRD
§5.4 stability ABI — platform auto-migration picks up the typed column
when first observed.
2026-06-02 18:57:12 +03:00

80 lines
2.9 KiB
TypeScript

/**
* Gap #2 (16-oss-verify-gap-prd §2) — git -C tilde expansion +
* --directory=/path equals-form.
*
* Symptom: `git -C ~/repos/myrepo` was passed through verbatim because
* absolutizePath treats `~/path` as relative. Result: attribution
* resolved against an anchor → `<anchor>/~/path` (wrong directory),
* falls back to inputProjectDir, misses the user's intent.
*
* Fix surfaces on the public extractEvents API: when a Bash git command
* carries `-C <dir>` or `--directory <dir>` or `--directory=<dir>`, the
* cwd event emitted by extractGit must carry the expanded scopedDir.
*/
import { describe, test, expect } from "vitest";
import { homedir } from "node:os";
import { extractEvents } from "../../src/session/extract.js";
function cwdEventDataOf(cmd: string): string | undefined {
return extractEvents({
tool_name: "Bash",
tool_input: { command: cmd },
tool_response: "",
}).find((e) => e.type === "cwd")?.data;
}
describe("parseGitInvocation — Gap #2 tilde + --directory=", () => {
test("tracer: `git -C ~/repos/myrepo status` → expanded homedir", () => {
const data = cwdEventDataOf(`git -C ~/repos/myrepo status`);
expect(data).toBe(`${homedir()}/repos/myrepo`);
});
test("`git --directory=/abs/x log` (equals form) → /abs/x", () => {
const data = cwdEventDataOf(`git --directory=/abs/x log`);
expect(data).toBe(`/abs/x`);
});
test("regression: `git --directory /abs/x log` (space form) → /abs/x", () => {
const data = cwdEventDataOf(`git --directory /abs/x log`);
expect(data).toBe(`/abs/x`);
});
test("`git -C ~/repos commit -m \"feat\"` emits BOTH cwd + git_commit", () => {
const events = extractEvents({
tool_name: "Bash",
tool_input: { command: `git -C ~/repos commit -m "feat: x"` },
tool_response: "",
});
const cwd = events.find((e) => e.type === "cwd");
const commit = events.find((e) => e.type === "git_commit");
expect(cwd?.data).toBe(`${homedir()}/repos`);
expect(commit?.data).toBe("feat: x");
});
test("bare `~` (no path tail) expands to homedir", () => {
const data = cwdEventDataOf(`git -C ~ status`);
expect(data).toBe(homedir());
});
test("`git -C ~user/foo` does NOT expand (no current user resolution)", () => {
const data = cwdEventDataOf(`git -C ~someoneelse/foo status`);
expect(data).toBe(`~someoneelse/foo`);
});
test("regression: `git -C /abs/path status` unchanged", () => {
const data = cwdEventDataOf(`git -C /abs/path status`);
expect(data).toBe(`/abs/path`);
});
test("regression: `git -C relative/path status` unchanged", () => {
const data = cwdEventDataOf(`git -C relative/path status`);
expect(data).toBe(`relative/path`);
});
test("`git --directory=~/abs status` expands equals-form tilde", () => {
const data = cwdEventDataOf(`git --directory=~/abs status`);
expect(data).toBe(`${homedir()}/abs`);
});
});