
























1+/**
2+ * Tests for ingress model.usage diagnostic emission in agentCommandFromIngress.
3+ *
4+ * Covers:
5+ * - ingressDiagnosticChannel channel label resolution
6+ * - emitIngressModelUsageDiagnostic with diagnostics enabled + valid usage
7+ * - emitIngressModelUsageDiagnostic with diagnostics disabled
8+ * - emitIngressModelUsageDiagnostic with null/missing usage
9+ */
10+11+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
12+13+const mocks = vi.hoisted(() => ({
14+emitTrustedDiagnosticEvent: vi.fn(),
15+isDiagnosticsEnabled: vi.fn(),
16+getRuntimeConfig: vi.fn(),
17+hasNonzeroUsage: vi.fn(),
18+resolveModelCostConfig: vi.fn(),
19+estimateUsageCost: vi.fn(),
20+}));
21+22+vi.mock("../infra/diagnostic-events.js", async () => {
23+const actual = await vi.importActual<typeof import("../infra/diagnostic-events.js")>(
24+"../infra/diagnostic-events.js",
25+);
26+return {
27+ ...actual,
28+emitTrustedDiagnosticEvent: mocks.emitTrustedDiagnosticEvent,
29+isDiagnosticsEnabled: mocks.isDiagnosticsEnabled,
30+};
31+});
32+33+vi.mock("../utils/usage-format.js", () => ({
34+resolveModelCostConfig: (...args: Array<unknown>) => mocks.resolveModelCostConfig(...args),
35+estimateUsageCost: (...args: Array<unknown>) => mocks.estimateUsageCost(...args),
36+}));
37+38+vi.mock("./usage.js", () => ({
39+hasNonzeroUsage: (usage: unknown) => mocks.hasNonzeroUsage(usage),
40+}));
41+42+vi.mock("../config/io.js", () => ({
43+getRuntimeConfig: () => mocks.getRuntimeConfig(),
44+}));
45+46+let testing: typeof import("./agent-command.js").testing;
47+48+beforeAll(async () => {
49+const mod = await import("./agent-command.js");
50+testing = mod.testing;
51+});
52+53+beforeEach(() => {
54+vi.clearAllMocks();
55+mocks.isDiagnosticsEnabled.mockReturnValue(true);
56+mocks.hasNonzeroUsage.mockReturnValue(true);
57+mocks.getRuntimeConfig.mockReturnValue({});
58+mocks.resolveModelCostConfig.mockReturnValue({});
59+mocks.estimateUsageCost.mockReturnValue(0.001);
60+});
61+62+afterEach(() => {
63+vi.clearAllMocks();
64+});
65+66+function makeResult(overrides?: Record<string, unknown>) {
67+return {
68+payloads: [{ text: "hello", mediaUrl: "" }],
69+meta: {
70+durationMs: 1234,
71+aborted: false,
72+stopReason: "end_turn",
73+agentMeta: {
74+provider: "openai",
75+model: "gpt-5.5",
76+sessionId: "sess-abc",
77+usage: {
78+input: 500,
79+output: 200,
80+cacheRead: 50,
81+cacheWrite: 25,
82+total: 775,
83+},
84+contextTokens: 128000,
85+promptTokens: 1200,
86+lastCallUsage: { input: 500, output: 200 },
87+ ...(overrides?.agentMeta as Record<string, unknown> | undefined),
88+},
89+ ...(overrides?.meta as Record<string, unknown> | undefined),
90+},
91+ ...overrides,
92+};
93+}
94+95+function makeOpts(overrides?: Record<string, unknown>) {
96+return {
97+message: "hello",
98+sessionKey: "agent:main:main",
99+agentId: "main",
100+allowModelOverride: false,
101+messageChannel: "api",
102+ ...overrides,
103+};
104+}
105+106+// ---------------------------------------------------------------------------
107+// ingressDiagnosticChannel
108+// ---------------------------------------------------------------------------
109+describe("ingressDiagnosticChannel", () => {
110+it("returns runContext.messageChannel when set", () => {
111+const channel = testing.ingressDiagnosticChannel({
112+message: "hi",
113+allowModelOverride: false,
114+runContext: { messageChannel: "discord" },
115+messageChannel: "api",
116+channel: "http",
117+});
118+expect(channel).toBe("discord");
119+});
120+121+it("falls back to opts.messageChannel", () => {
122+const channel = testing.ingressDiagnosticChannel({
123+message: "hi",
124+allowModelOverride: false,
125+messageChannel: "api",
126+channel: "http",
127+});
128+expect(channel).toBe("api");
129+});
130+131+it("falls back to opts.channel", () => {
132+const channel = testing.ingressDiagnosticChannel({
133+message: "hi",
134+allowModelOverride: false,
135+channel: "webchat",
136+});
137+expect(channel).toBe("webchat");
138+});
139+140+it('defaults to "http" when no channel info is present', () => {
141+const channel = testing.ingressDiagnosticChannel({
142+message: "hi",
143+allowModelOverride: false,
144+});
145+expect(channel).toBe("http");
146+});
147+});
148+149+// ---------------------------------------------------------------------------
150+// emitIngressModelUsageDiagnostic
151+// ---------------------------------------------------------------------------
152+describe("emitIngressModelUsageDiagnostic", () => {
153+it("emits model.usage when diagnostics are enabled and result has usage", () => {
154+const result = makeResult();
155+const opts = makeOpts();
156+157+testing.emitIngressModelUsageDiagnostic(result, opts);
158+159+expect(mocks.emitTrustedDiagnosticEvent).toHaveBeenCalledTimes(1);
160+const event = mocks.emitTrustedDiagnosticEvent.mock.calls[0]?.[0];
161+expect(event).toMatchObject({
162+type: "model.usage",
163+sessionKey: "agent:main:main",
164+sessionId: "sess-abc",
165+channel: "api",
166+agentId: "main",
167+provider: "openai",
168+model: "gpt-5.5",
169+usage: {
170+input: 500,
171+output: 200,
172+cacheRead: 50,
173+cacheWrite: 25,
174+promptTokens: 575,
175+total: 775,
176+},
177+durationMs: 1234,
178+});
179+});
180+181+it("does not emit when diagnostics are disabled", () => {
182+mocks.isDiagnosticsEnabled.mockReturnValue(false);
183+const result = makeResult();
184+const opts = makeOpts();
185+186+testing.emitIngressModelUsageDiagnostic(result, opts);
187+188+expect(mocks.emitTrustedDiagnosticEvent).not.toHaveBeenCalled();
189+});
190+191+it("does not emit when agentMeta is missing", () => {
192+const result = makeResult({
193+meta: { durationMs: 100, aborted: false, stopReason: "end_turn" },
194+});
195+// result.meta.agentMeta is undefined
196+(result as Record<string, unknown>).meta = { durationMs: 100 };
197+198+const opts = makeOpts();
199+200+testing.emitIngressModelUsageDiagnostic(result, opts);
201+202+expect(mocks.emitTrustedDiagnosticEvent).not.toHaveBeenCalled();
203+});
204+205+it("does not emit when usage is zero", () => {
206+mocks.hasNonzeroUsage.mockReturnValue(false);
207+const result = makeResult();
208+const opts = makeOpts();
209+210+testing.emitIngressModelUsageDiagnostic(result, opts);
211+212+expect(mocks.emitTrustedDiagnosticEvent).not.toHaveBeenCalled();
213+});
214+215+it("resolves channel from runContext when available", () => {
216+const result = makeResult();
217+const opts = makeOpts({
218+runContext: { messageChannel: "discord" },
219+messageChannel: "api",
220+});
221+222+testing.emitIngressModelUsageDiagnostic(result, opts);
223+224+expect(mocks.emitTrustedDiagnosticEvent).toHaveBeenCalledTimes(1);
225+const event = mocks.emitTrustedDiagnosticEvent.mock.calls[0]?.[0];
226+expect(event.channel).toBe("discord");
227+});
228+229+it('defaults channel to "http" when no channel info is present', () => {
230+const result = makeResult();
231+const opts = { message: "hi", allowModelOverride: false };
232+233+testing.emitIngressModelUsageDiagnostic(result, opts);
234+235+expect(mocks.emitTrustedDiagnosticEvent).toHaveBeenCalledTimes(1);
236+const event = mocks.emitTrustedDiagnosticEvent.mock.calls[0]?.[0];
237+expect(event.channel).toBe("http");
238+});
239+240+it("computes cost when billable usage buckets are present", () => {
241+const result = makeResult();
242+const opts = makeOpts();
243+244+testing.emitIngressModelUsageDiagnostic(result, opts);
245+246+expect(mocks.resolveModelCostConfig).toHaveBeenCalledWith({
247+provider: "openai",
248+model: "gpt-5.5",
249+config: expect.any(Object) as unknown,
250+});
251+expect(mocks.estimateUsageCost).toHaveBeenCalled();
252+expect(mocks.emitTrustedDiagnosticEvent).toHaveBeenCalledTimes(1);
253+const event = mocks.emitTrustedDiagnosticEvent.mock.calls[0]?.[0];
254+expect(event.costUsd).toBe(0.001);
255+});
256+257+it("handles missing optional usage fields gracefully", () => {
258+const result = makeResult({
259+agentMeta: {
260+provider: "openai",
261+model: "gpt-5.5",
262+sessionId: "sess-min",
263+usage: { input: 100, output: 50 },
264+},
265+});
266+const opts = makeOpts();
267+268+testing.emitIngressModelUsageDiagnostic(result, opts);
269+270+expect(mocks.emitTrustedDiagnosticEvent).toHaveBeenCalledTimes(1);
271+const event = mocks.emitTrustedDiagnosticEvent.mock.calls[0]?.[0];
272+expect(event.usage).toMatchObject({
273+input: 100,
274+output: 50,
275+cacheRead: 0,
276+cacheWrite: 0,
277+promptTokens: 100,
278+total: 150,
279+});
280+});
281+282+it("omits context.used when promptTokens is undefined", () => {
283+const result = makeResult({
284+agentMeta: {
285+promptTokens: undefined,
286+provider: "openai",
287+model: "gpt-5.5",
288+sessionId: "sess-no-prompt",
289+usage: { input: 10, output: 5 },
290+contextTokens: 128000,
291+},
292+});
293+const opts = makeOpts();
294+295+testing.emitIngressModelUsageDiagnostic(result, opts);
296+297+expect(mocks.emitTrustedDiagnosticEvent).toHaveBeenCalledTimes(1);
298+const event = mocks.emitTrustedDiagnosticEvent.mock.calls[0]?.[0];
299+expect(event.context).toEqual({ limit: 128000 });
300+});
301+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。