|
9 | 9 | main, |
10 | 10 | getReadableTokenUsagePaths, |
11 | 11 | extractRequestId, |
| 12 | + extractTokenUsageDedupeKey, |
12 | 13 | readDedupedTokenUsage, |
13 | 14 | getSummaryTitle, |
14 | 15 | buildStepSummarySection, |
@@ -356,6 +357,48 @@ describe("parse_token_usage", () => { |
356 | 357 | expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("1.03602")); |
357 | 358 | }); |
358 | 359 |
|
| 360 | + test("exports AWF-reported zero AIC instead of treating it as missing", async () => { |
| 361 | + const agentUsageFile = path.join(tmpDir, "agent_usage.json"); |
| 362 | + const zeroEntry = JSON.stringify({ |
| 363 | + model: "gpt-4o-mini-2024-07-18", |
| 364 | + provider: "copilot", |
| 365 | + input_tokens: 1000, |
| 366 | + output_tokens: 100, |
| 367 | + ai_credits_this_response: 0, |
| 368 | + ai_credits_total: 0, |
| 369 | + }); |
| 370 | + |
| 371 | + fs.existsSync = vi.fn(p => { |
| 372 | + if (p === TOKEN_USAGE_PATH) return true; |
| 373 | + if (p === TOKEN_USAGE_AUDIT_PATH || p === TOKEN_USAGE_AWF_AUDIT_PATH) return false; |
| 374 | + return originalExistsSync(p); |
| 375 | + }); |
| 376 | + fs.statSync = vi.fn(p => { |
| 377 | + if (p === TOKEN_USAGE_PATH) return { size: zeroEntry.length }; |
| 378 | + if (p === TOKEN_USAGE_AUDIT_PATH || p === TOKEN_USAGE_AWF_AUDIT_PATH) return { size: 0 }; |
| 379 | + return originalStatSync(p); |
| 380 | + }); |
| 381 | + fs.readFileSync = vi.fn((p, enc) => { |
| 382 | + if (p === TOKEN_USAGE_PATH) return zeroEntry; |
| 383 | + if (p === TOKEN_USAGE_AUDIT_PATH || p === TOKEN_USAGE_AWF_AUDIT_PATH) return ""; |
| 384 | + return originalReadFileSync(p, enc); |
| 385 | + }); |
| 386 | + fs.writeFileSync = vi.fn((p, data) => { |
| 387 | + if (p === AGENT_USAGE_PATH) { |
| 388 | + originalWriteFileSync(agentUsageFile, data); |
| 389 | + } else { |
| 390 | + originalWriteFileSync(p, data); |
| 391 | + } |
| 392 | + }); |
| 393 | + |
| 394 | + await main(); |
| 395 | + |
| 396 | + const agentUsage = JSON.parse(originalReadFileSync(agentUsageFile, "utf8")); |
| 397 | + expect(agentUsage.ai_credits).toBe(0); |
| 398 | + expect(mockCore.exportVariable).toHaveBeenCalledWith("GH_AW_AIC", "0"); |
| 399 | + expect(mockCore.setOutput).toHaveBeenCalledWith("aic", "0"); |
| 400 | + }); |
| 401 | + |
359 | 402 | test("surfaces fallback accounting warnings", async () => { |
360 | 403 | const malformedEntry = JSON.stringify({ |
361 | 404 | model: "gpt-4o-mini-2024-07-18", |
@@ -589,6 +632,13 @@ describe("parse_token_usage", () => { |
589 | 632 | expect(extractRequestId('{"model":"m"}')).toBe(""); |
590 | 633 | }); |
591 | 634 |
|
| 635 | + test("extractTokenUsageDedupeKey includes event and request_id", () => { |
| 636 | + expect(extractTokenUsageDedupeKey('{"event":"token_usage","request_id":"req-123","model":"m"}')).toBe("token_usage:req-123"); |
| 637 | + expect(extractTokenUsageDedupeKey('{"event":"other","request_id":"req-123","model":"m"}')).toBe("other:req-123"); |
| 638 | + expect(extractTokenUsageDedupeKey('{"request_id":"req-123","model":"m"}')).toBe("token_usage:req-123"); |
| 639 | + expect(extractTokenUsageDedupeKey('{"model":"m"}')).toBe(""); |
| 640 | + }); |
| 641 | + |
592 | 642 | test("getReadableTokenUsagePaths skips failing stat path and keeps valid path", () => { |
593 | 643 | fs.existsSync = vi.fn(p => p === TOKEN_USAGE_AUDIT_PATH || p === TOKEN_USAGE_PATH); |
594 | 644 | fs.statSync = vi.fn(p => { |
@@ -618,6 +668,22 @@ describe("parse_token_usage", () => { |
618 | 668 | expect(deduped.match(/"request_id":"req-1"/g)).toHaveLength(1); |
619 | 669 | }); |
620 | 670 |
|
| 671 | + test("readDedupedTokenUsage keeps different events with the same request_id", () => { |
| 672 | + const fileA = '{"event":"token_usage","request_id":"req-1","model":"m1","input_tokens":1}'; |
| 673 | + const fileB = '{"event":"token_steering","request_id":"req-1","model":"m1","input_tokens":2}'; |
| 674 | + |
| 675 | + fs.readFileSync = vi.fn(p => { |
| 676 | + if (p === TOKEN_USAGE_AUDIT_PATH) return fileA; |
| 677 | + if (p === TOKEN_USAGE_PATH) return fileB; |
| 678 | + return originalReadFileSync(p, "utf8"); |
| 679 | + }); |
| 680 | + |
| 681 | + const deduped = readDedupedTokenUsage([TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH]); |
| 682 | + expect(deduped).toContain('"event":"token_usage"'); |
| 683 | + expect(deduped).toContain('"event":"token_steering"'); |
| 684 | + expect(deduped.match(/"request_id":"req-1"/g)).toHaveLength(2); |
| 685 | + }); |
| 686 | + |
621 | 687 | test("deduplicates mirrored AWF records before aggregating reported credits", () => { |
622 | 688 | const fixture = originalReadFileSync(path.join(__dirname, "fixtures", "awf-v0.28.7-aic-token-usage.jsonl"), "utf8"); |
623 | 689 | fs.readFileSync = vi.fn(p => { |
|
0 commit comments