Files
openclaw/extensions/copilot/src/usage-bridge.test.ts
Ramrajprabu f3cfd752d3 feat(copilot): add GitHub Copilot agent runtime
Adds the opt-in bundled GitHub Copilot agent runtime, pinned SDK install path, docs/inventory, SDK/tool/sandbox/auth wiring, and replay/tool-safety fixes.

Verification:
- Local: git diff --check; fnm exec --using 24.15.0 pnpm tsgo:extensions; fnm exec --using 24.15.0 pnpm check:test-types; fnm exec --using 24.15.0 pnpm build.
- Autoreview local: clean for the replay-safety fix; branch autoreview engine returned empty output twice, so local autoreview plus local/Crabbox/CI proof was used.
- Crabbox focused Copilot: run_2c0db9f48a4a, 19 files / 485 tests passed.
- Crabbox additional boundary shard: run_26a246a1aa24, prompt snapshots and plugin SDK boundary/export checks passed.
- Crabbox live Copilot: run_d128e4048b4e, real gpt-4.1 turn with live_echo phase-1-green and clean session-file check.
- GitHub checks: green on head 7cc8657e0d, including Dependency Guard after exact-head approval.

Co-authored-by: Ramraj Balasubramanian <ramrajba@microsoft.com>
2026-05-29 05:15:22 +01:00

271 lines
7.2 KiB
TypeScript

import type { NormalizedUsage } from "openclaw/plugin-sdk/agent-harness-runtime";
import { describe, expect, it } from "vitest";
import {
buildCopilotAssistantUsage,
deriveCopilotUsageTotal,
normalizeCopilotUsage,
} from "./usage-bridge.js";
const ZERO_SNAPSHOT: NormalizedUsage = {
cacheRead: undefined,
cacheWrite: undefined,
input: undefined,
output: undefined,
total: 0,
};
describe("usage-bridge", () => {
describe("normalizeCopilotUsage", () => {
it("normalizes SDK inputTokens and outputTokens into NormalizedUsage", () => {
expect(normalizeCopilotUsage({ inputTokens: 10, outputTokens: 5 })).toEqual({
cacheRead: undefined,
cacheWrite: undefined,
input: 10,
output: 5,
total: 15,
});
});
it("normalizes SDK cacheReadTokens and cacheWriteTokens when present", () => {
expect(normalizeCopilotUsage({ cacheReadTokens: 3, cacheWriteTokens: 4 })).toEqual({
cacheRead: 3,
cacheWrite: 4,
input: undefined,
output: undefined,
total: 7,
});
});
it("leaves missing cache token fields undefined rather than zero", () => {
const usage = normalizeCopilotUsage({ inputTokens: 2 });
expect(usage).toEqual({
cacheRead: undefined,
cacheWrite: undefined,
input: 2,
output: undefined,
total: 2,
});
expect(usage?.cacheRead).toBeUndefined();
expect(usage?.cacheWrite).toBeUndefined();
});
it("returns a defined zero-snapshot when SDK event is an object with no valid fields", () => {
expect(normalizeCopilotUsage({})).toEqual(ZERO_SNAPSHOT);
expect(normalizeCopilotUsage({ inputTokens: undefined })).toEqual(ZERO_SNAPSHOT);
});
it("returns undefined for null / non-object input", () => {
expect(normalizeCopilotUsage(null)).toBeUndefined();
expect(normalizeCopilotUsage(undefined)).toBeUndefined();
expect(normalizeCopilotUsage("usage")).toBeUndefined();
});
it("ignores string-typed token counts", () => {
expect(normalizeCopilotUsage({ inputTokens: "5" })).toEqual(ZERO_SNAPSHOT);
});
it("ignores NaN and Infinity token counts", () => {
expect(normalizeCopilotUsage({ inputTokens: Number.NaN })).toEqual(ZERO_SNAPSHOT);
expect(normalizeCopilotUsage({ outputTokens: Number.POSITIVE_INFINITY })).toEqual(
ZERO_SNAPSHOT,
);
expect(normalizeCopilotUsage({ cacheReadTokens: Number.NEGATIVE_INFINITY })).toEqual(
ZERO_SNAPSHOT,
);
expect(normalizeCopilotUsage({ inputTokens: 2, outputTokens: Number.NaN })).toEqual({
cacheRead: undefined,
cacheWrite: undefined,
input: 2,
output: undefined,
total: 2,
});
});
it("clamps negative token counts to zero", () => {
expect(normalizeCopilotUsage({ inputTokens: -3 })).toEqual({
cacheRead: undefined,
cacheWrite: undefined,
input: 0,
output: undefined,
total: 0,
});
});
it("truncates fractional token counts", () => {
expect(normalizeCopilotUsage({ inputTokens: 3.7 })).toEqual({
cacheRead: undefined,
cacheWrite: undefined,
input: 3,
output: undefined,
total: 3,
});
});
it("derives total from normalized SDK component counts for compatibility", () => {
expect(
normalizeCopilotUsage({
cacheReadTokens: 3,
cacheWriteTokens: 4,
inputTokens: 1,
outputTokens: 2,
}),
).toEqual({
cacheRead: 3,
cacheWrite: 4,
input: 1,
output: 2,
total: 10,
});
});
it("does not mutate the caller-provided SDK event data", () => {
const data = Object.freeze({ inputTokens: 4, outputTokens: 6 });
expect(normalizeCopilotUsage(data)).toEqual({
cacheRead: undefined,
cacheWrite: undefined,
input: 4,
output: 6,
total: 10,
});
expect(data).toEqual({ inputTokens: 4, outputTokens: 6 });
});
it("only whitelists known SDK fields and ignores unrelated input keys", () => {
expect(
normalizeCopilotUsage({
inputTokens: 5,
malicious_field: 999,
outputTokens: "bad",
prompt_tokens: 100,
}),
).toEqual({
cacheRead: undefined,
cacheWrite: undefined,
input: 5,
output: undefined,
total: 5,
});
});
});
describe("buildCopilotAssistantUsage", () => {
it("builds rich AssistantMessage usage with zero cost fields", () => {
expect(
buildCopilotAssistantUsage({
usage: { cacheRead: 3, cacheWrite: 4, input: 1, output: 2, total: 10 },
}),
).toEqual({
cacheRead: 3,
cacheWrite: 4,
cost: {
cacheRead: 0,
cacheWrite: 0,
input: 0,
output: 0,
total: 0,
},
input: 1,
output: 2,
totalTokens: 10,
});
});
it("defaults missing usage fields to zero in the rich block only", () => {
expect(
buildCopilotAssistantUsage({
usage: { input: 4 },
}),
).toEqual({
cacheRead: 0,
cacheWrite: 0,
cost: {
cacheRead: 0,
cacheWrite: 0,
input: 0,
output: 0,
total: 0,
},
input: 4,
output: 0,
totalTokens: 0,
});
});
it("uses fallback outputTokens when no usage event was captured", () => {
expect(buildCopilotAssistantUsage({ fallbackOutputTokens: 7 })).toEqual({
cacheRead: 0,
cacheWrite: 0,
cost: {
cacheRead: 0,
cacheWrite: 0,
input: 0,
output: 0,
total: 0,
},
input: 0,
output: 7,
totalTokens: 7,
});
});
it("does not use fallback outputTokens when normalized usage is already present", () => {
expect(
buildCopilotAssistantUsage({
fallbackOutputTokens: 9,
usage: { input: 4, total: 4 },
}),
).toEqual({
cacheRead: 0,
cacheWrite: 0,
cost: {
cacheRead: 0,
cacheWrite: 0,
input: 0,
output: 0,
total: 0,
},
input: 4,
output: 0,
totalTokens: 4,
});
});
it("returns an all-zero block when both usage and fallback are missing", () => {
expect(buildCopilotAssistantUsage({})).toEqual({
cacheRead: 0,
cacheWrite: 0,
cost: {
cacheRead: 0,
cacheWrite: 0,
input: 0,
output: 0,
total: 0,
},
input: 0,
output: 0,
totalTokens: 0,
});
});
});
describe("deriveCopilotUsageTotal", () => {
it("returns undefined when usage is undefined", () => {
expect(deriveCopilotUsageTotal(undefined)).toBeUndefined();
});
it("sums input/output/cacheRead/cacheWrite for total", () => {
const usage: NormalizedUsage = {
cacheRead: 3,
cacheWrite: 4,
input: 1,
output: 2,
total: 999,
};
expect(deriveCopilotUsageTotal(usage)).toBe(10);
});
});
});