229 lines
7.4 KiB
TypeScript
229 lines
7.4 KiB
TypeScript
import test from "node:test";
|
||
import assert from "node:assert/strict";
|
||
import React from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
|
||
const { default: RequestLoggerDetail } =
|
||
await import("../../src/shared/components/RequestLoggerDetail.tsx");
|
||
|
||
function renderDetailWithSourceFormat(sourceFormat: string) {
|
||
return renderToStaticMarkup(
|
||
React.createElement(RequestLoggerDetail, {
|
||
log: {
|
||
status: 200,
|
||
method: "POST",
|
||
path: "/v1/chat/completions",
|
||
timestamp: "2026-04-09T21:27:08.000Z",
|
||
duration: 2500,
|
||
provider: "openrouter",
|
||
sourceFormat,
|
||
model: "deepseek/deepseek-v4-pro",
|
||
requestedModel: "openrouter/deepseek/deepseek-v4-pro",
|
||
cacheSource: "upstream",
|
||
tokens: {
|
||
in: 10,
|
||
out: 2,
|
||
cacheRead: null,
|
||
cacheWrite: null,
|
||
reasoning: null,
|
||
},
|
||
},
|
||
detail: {
|
||
requestedModel: "openrouter/deepseek/deepseek-v4-pro",
|
||
cacheSource: "upstream",
|
||
tokens: {
|
||
in: 10,
|
||
out: 2,
|
||
cacheRead: null,
|
||
cacheWrite: null,
|
||
reasoning: null,
|
||
},
|
||
},
|
||
loading: false,
|
||
onClose: () => {},
|
||
onCopy: async () => true,
|
||
})
|
||
);
|
||
}
|
||
|
||
test("request log detail splits token badges into input and output groups", () => {
|
||
const html = renderToStaticMarkup(
|
||
React.createElement(RequestLoggerDetail, {
|
||
log: {
|
||
status: 200,
|
||
method: "POST",
|
||
path: "/v1/chat/completions",
|
||
timestamp: "2026-04-09T21:27:08.000Z",
|
||
duration: 2500,
|
||
provider: "openai-compatible-sp-openai",
|
||
sourceFormat: "openai-chat",
|
||
model: "gpt-5.4",
|
||
requestedModel: "openai-compatible-sp-openai/gpt-5.4",
|
||
account: "main",
|
||
apiKeyName: "tools",
|
||
apiKeyId: "29d9***7e37",
|
||
comboName: "_Latest-Discounted",
|
||
cacheSource: "semantic",
|
||
tokens: {
|
||
in: 21818,
|
||
out: 42,
|
||
cacheRead: 21632,
|
||
cacheWrite: null,
|
||
reasoning: null,
|
||
},
|
||
},
|
||
detail: {
|
||
account: "main",
|
||
apiKeyName: "tools",
|
||
apiKeyId: "29d9***7e37",
|
||
comboName: "_Latest-Discounted",
|
||
requestedModel: "openai-compatible-sp-openai/gpt-5.4",
|
||
cacheSource: "semantic",
|
||
tokens: {
|
||
in: 21818,
|
||
out: 42,
|
||
cacheRead: 21632,
|
||
cacheWrite: null,
|
||
reasoning: null,
|
||
},
|
||
},
|
||
loading: false,
|
||
onClose: () => {},
|
||
onCopy: async () => true,
|
||
})
|
||
);
|
||
|
||
const inputLabelIndex = html.indexOf(">Input<");
|
||
const outputLabelIndex = html.indexOf(">Output<");
|
||
const modelLabelIndex = html.indexOf(">Model<");
|
||
const requestedModelLabelIndex = html.indexOf(">Requested Model<");
|
||
|
||
// The completed-request detail grid renders "Started At" / "Ended At" columns
|
||
// (renamed from the old single "Completed Time" label in #5834).
|
||
assert.notEqual(html.indexOf(">Ended At<"), -1);
|
||
assert.equal(html.includes(">Time<"), false);
|
||
assert.notEqual(inputLabelIndex, -1);
|
||
assert.notEqual(outputLabelIndex, -1);
|
||
assert.notEqual(modelLabelIndex, -1);
|
||
assert.notEqual(requestedModelLabelIndex, -1);
|
||
assert.equal(html.includes(">Tokens<"), false);
|
||
assert.equal(inputLabelIndex < outputLabelIndex, true);
|
||
assert.equal(outputLabelIndex < modelLabelIndex, true);
|
||
assert.equal(modelLabelIndex < requestedModelLabelIndex, true);
|
||
assert.notEqual(html.indexOf(">Cache Source<"), -1);
|
||
assert.notEqual(html.indexOf(">Semantic (OmniRoute)<"), -1);
|
||
assert.notEqual(html.indexOf(">OpenAI-Chat<"), -1);
|
||
|
||
assert.match(
|
||
html,
|
||
/data-testid="token-group-input"[\s\S]*Total In: 21[\s\S]*818[\s\S]*Cache Read: 21[\s\S]*632[\s\S]*Cache Write: N\/A/
|
||
);
|
||
assert.match(html, /data-testid="token-group-output"[\s\S]*Total Out: 42[\s\S]*Reasoning: N\/A/);
|
||
});
|
||
|
||
test("request log detail labels OpenAI protocol variants explicitly", () => {
|
||
const chatHtml = renderDetailWithSourceFormat("openai");
|
||
const responsesHtml = renderDetailWithSourceFormat("openai-responses");
|
||
|
||
assert.notEqual(chatHtml.indexOf(">OpenAI-Chat<"), -1);
|
||
assert.notEqual(responsesHtml.indexOf(">OpenAI-Responses<"), -1);
|
||
});
|
||
|
||
test("request log detail compression-summary badge shows positive saved%, never negative", () => {
|
||
// Regression: prior code used `(-{pct}%)` which produced literal "-100%" when the entire
|
||
// prompt was compressed (compressed=5286, totalIn=0). The fix clamps pct to [0, 100] and
|
||
// uses "(N% saved)" so the user-facing label is always positive.
|
||
const make = (tokensIn: number, tokensCompressed: number) =>
|
||
renderToStaticMarkup(
|
||
React.createElement(RequestLoggerDetail, {
|
||
log: {
|
||
status: 200,
|
||
method: "POST",
|
||
path: "/v1/chat/completions",
|
||
timestamp: "2026-04-09T21:27:08.000Z",
|
||
duration: 1500,
|
||
provider: "openai-compatible-sp-openai",
|
||
sourceFormat: "openai-chat",
|
||
model: "gpt-5.4",
|
||
requestedModel: "openai-compatible-sp-openai/gpt-5.4",
|
||
cacheSource: "semantic",
|
||
tokens: {
|
||
in: tokensIn,
|
||
out: 42,
|
||
cacheRead: null,
|
||
cacheWrite: null,
|
||
reasoning: null,
|
||
compressed: tokensCompressed,
|
||
},
|
||
},
|
||
detail: {
|
||
tokens: {
|
||
in: tokensIn,
|
||
out: 42,
|
||
cacheRead: null,
|
||
cacheWrite: null,
|
||
reasoning: null,
|
||
compressed: tokensCompressed,
|
||
},
|
||
},
|
||
loading: false,
|
||
onClose: () => {},
|
||
onCopy: async () => true,
|
||
})
|
||
);
|
||
|
||
// Original bug repro: totalIn=0, compressed=5286 → previously rendered "(−100%)".
|
||
const fullyCompressed = make(0, 5286);
|
||
assert.match(fullyCompressed, /Compressed: 5,286 → 0 \(100% saved\)/);
|
||
assert.equal(fullyCompressed.includes("(-100%)"), false, "literal '(-100%)' must never appear");
|
||
assert.equal(
|
||
fullyCompressed.includes("\u2212100%"),
|
||
false,
|
||
"unicode minus + 100% must never appear"
|
||
);
|
||
|
||
// Half-compressed case must clamp cleanly inside the [0, 100] window.
|
||
const halfCompressed = make(2500, 2500);
|
||
assert.match(halfCompressed, /Compressed: 5,000 → 2,500 \(50% saved\)/);
|
||
assert.equal(halfCompressed.includes("-50%"), false);
|
||
|
||
// Sanity: tiny input, tiny savings still rendered as a positive percentage.
|
||
const small = make(1000, 100);
|
||
assert.match(small, /Compressed: 1,100 → 1,000 \(9% saved\)/);
|
||
});
|
||
|
||
test("request log detail follows the email visibility setting for accounts", () => {
|
||
const props = {
|
||
log: {
|
||
status: 200,
|
||
method: "POST",
|
||
path: "/v1/responses",
|
||
timestamp: "2026-04-09T21:27:08.000Z",
|
||
duration: 2500,
|
||
provider: "codex",
|
||
sourceFormat: "openai-responses",
|
||
model: "gpt-5.5",
|
||
account: "logs.user@example.com",
|
||
tokens: { in: 10, out: 2 },
|
||
},
|
||
detail: {
|
||
account: "logs.user@example.com",
|
||
tokens: { in: 10, out: 2 },
|
||
},
|
||
loading: false,
|
||
onClose: () => {},
|
||
onCopy: async () => true,
|
||
};
|
||
|
||
const hiddenHtml = renderToStaticMarkup(
|
||
React.createElement(RequestLoggerDetail, { ...props, emailsVisible: false })
|
||
);
|
||
const visibleHtml = renderToStaticMarkup(
|
||
React.createElement(RequestLoggerDetail, { ...props, emailsVisible: true })
|
||
);
|
||
|
||
assert.match(hiddenHtml, /log\*{6}@\*{8}com/);
|
||
assert.equal(hiddenHtml.includes("logs.user@example.com"), false);
|
||
assert.notEqual(visibleHtml.indexOf("logs.user@example.com"), -1);
|
||
});
|