Release grok-build-hud v0.3.9: multi-terminal HUD, i18n settings, theme follow
Standalone Grok Build status strip with parallel sessions, width-adaptive layout, Chinese/English settings UI, and token breakdown. Product copy no longer references third-party tools.
This commit is contained in:
@@ -62,6 +62,81 @@ describe("dashboard", () => {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("after /new: prefers newest active session, not first-listed old 78%", async () => {
|
||||
// Repro: active_sessions keeps old tab first; new tab second with low/no signals.
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-new-"));
|
||||
const cwd = "/Users/dex/AI FILM SPACE/0717";
|
||||
const oldId = "old-session-78pct";
|
||||
const newId = "new-session-after-new";
|
||||
const enc = encodeURIComponent(cwd);
|
||||
const oldDir = path.join(tmp, "sessions", enc, oldId);
|
||||
const newDir = path.join(tmp, "sessions", enc, newId);
|
||||
fs.mkdirSync(oldDir, { recursive: true });
|
||||
fs.mkdirSync(newDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(oldDir, "signals.json"),
|
||||
JSON.stringify({
|
||||
contextWindowUsage: 78,
|
||||
contextTokensUsed: 392180,
|
||||
contextWindowTokens: 500000,
|
||||
turnCount: 18,
|
||||
toolCallCount: 344,
|
||||
primaryModelId: "grok-4.5",
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(oldDir, "summary.json"),
|
||||
JSON.stringify({
|
||||
info: { id: oldId, cwd },
|
||||
last_active_at: "2026-07-17T06:00:00.000Z",
|
||||
updated_at: "2026-07-17T06:00:00.000Z",
|
||||
current_model_id: "grok-4.5",
|
||||
}),
|
||||
);
|
||||
// New session often has summary+updates before signals.json exists
|
||||
fs.writeFileSync(
|
||||
path.join(newDir, "summary.json"),
|
||||
JSON.stringify({
|
||||
info: { id: newId, cwd },
|
||||
last_active_at: "2026-07-17T07:43:00.000Z",
|
||||
updated_at: "2026-07-17T07:43:00.000Z",
|
||||
created_at: "2026-07-17T07:43:00.000Z",
|
||||
current_model_id: "grok-4.5",
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(path.join(newDir, "updates.jsonl"), "");
|
||||
// active list: OLD first (insertion order) — bug used to pick this always
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, "active_sessions.json"),
|
||||
JSON.stringify([
|
||||
{
|
||||
session_id: oldId,
|
||||
pid: process.pid,
|
||||
cwd,
|
||||
opened_at: "2026-07-17T04:00:00.000Z",
|
||||
},
|
||||
{
|
||||
session_id: newId,
|
||||
pid: process.pid,
|
||||
cwd,
|
||||
opened_at: "2026-07-17T07:43:00.000Z",
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const r = await refreshDashboard({ grokHome: tmp, noUsage: true });
|
||||
assert.ok(r.session);
|
||||
assert.equal(r.session!.sessionId, newId);
|
||||
assert.ok(
|
||||
Math.round(r.session!.contextPercent) < 10,
|
||||
`expected low ctx after /new, got ${r.session!.contextPercent}`,
|
||||
);
|
||||
assert.doesNotMatch(r.title, /78%/);
|
||||
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("runDashboardLoop respects maxIterations", async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-loop-"));
|
||||
const sid = "fixture-session-001";
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
shouldUseHudForGrokInvoke,
|
||||
installGrokCommandWrap,
|
||||
uninstallGrokCommandWrap,
|
||||
isOurWrapper,
|
||||
} from "../src/grok-wrap.js";
|
||||
|
||||
describe("grok wrap", () => {
|
||||
it("uses HUD for bare interactive grok", () => {
|
||||
assert.equal(
|
||||
shouldUseHudForGrokInvoke({
|
||||
args: [],
|
||||
env: {},
|
||||
isTtyIn: true,
|
||||
isTtyOut: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("skips HUD for -p / login / pipes / escape env", () => {
|
||||
assert.equal(
|
||||
shouldUseHudForGrokInvoke({
|
||||
args: ["-p", "hi"],
|
||||
env: {},
|
||||
isTtyIn: true,
|
||||
isTtyOut: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseHudForGrokInvoke({
|
||||
args: ["login"],
|
||||
env: {},
|
||||
isTtyIn: true,
|
||||
isTtyOut: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseHudForGrokInvoke({
|
||||
args: [],
|
||||
env: {},
|
||||
isTtyIn: false,
|
||||
isTtyOut: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseHudForGrokInvoke({
|
||||
args: [],
|
||||
env: { GROK_NO_HUD: "1" },
|
||||
isTtyIn: true,
|
||||
isTtyOut: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldUseHudForGrokInvoke({
|
||||
args: [],
|
||||
env: { GROK_HUD_ACTIVE: "1" },
|
||||
isTtyIn: true,
|
||||
isTtyOut: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("installs wrapper + grok-real and uninstall restores symlink", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "grok-wrap-"));
|
||||
const downloads = path.join(tmp, "downloads");
|
||||
const bin = path.join(tmp, "bin");
|
||||
fs.mkdirSync(downloads, { recursive: true });
|
||||
fs.mkdirSync(bin, { recursive: true });
|
||||
const fakeReal = path.join(downloads, "grok-fake-bin");
|
||||
// small executable script pretending to be grok
|
||||
fs.writeFileSync(fakeReal, "#!/bin/sh\necho real\n", { mode: 0o755 });
|
||||
fs.symlinkSync(fakeReal, path.join(bin, "grok"));
|
||||
|
||||
const r = installGrokCommandWrap({ grokHome: tmp, nodeBin: process.execPath });
|
||||
assert.ok(fs.existsSync(r.realLink));
|
||||
assert.ok(isOurWrapper(r.wrapperPath));
|
||||
assert.equal(fs.realpathSync(r.realLink), fs.realpathSync(fakeReal));
|
||||
|
||||
const u = uninstallGrokCommandWrap({ grokHome: tmp });
|
||||
assert.equal(u.restored, true);
|
||||
assert.ok(!isOurWrapper(path.join(bin, "grok")));
|
||||
assert.equal(fs.realpathSync(path.join(bin, "grok")), fs.realpathSync(fakeReal));
|
||||
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeLang, t, langLabel } from "../src/i18n.js";
|
||||
import { setLanguage } from "../src/settings-ui.js";
|
||||
import { loadHudConfig, PRESET_FULL } from "../src/hud-config.js";
|
||||
import { formatTmuxStatusLines } from "../src/status.js";
|
||||
import { THEME_TOKYONIGHT } from "../src/theme.js";
|
||||
import { stripTmuxStyles } from "../src/layout.js";
|
||||
import type { SessionSnapshot } from "../src/types.js";
|
||||
|
||||
describe("i18n + settings language", () => {
|
||||
it("defaults and normalizes languages", () => {
|
||||
assert.equal(normalizeLang(undefined), "zh-Hans");
|
||||
assert.equal(normalizeLang("zh"), "zh-Hans");
|
||||
assert.equal(normalizeLang("en"), "en");
|
||||
assert.equal(normalizeLang("tw"), "zh-Hant");
|
||||
assert.equal(t("zh-Hans").ctx, "窗");
|
||||
assert.equal(t("en").ctx, "ctx");
|
||||
assert.match(langLabel("zh-Hans"), /中文/);
|
||||
});
|
||||
|
||||
it("setLanguage writes config", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-lang-"));
|
||||
fs.mkdirSync(path.join(tmp, "hud"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, "hud", "config.json"),
|
||||
JSON.stringify({ ...PRESET_FULL, language: "en" }, null, 2),
|
||||
);
|
||||
setLanguage("zh", tmp);
|
||||
const cfg = loadHudConfig(tmp);
|
||||
assert.equal(cfg.language, "zh-Hans");
|
||||
setLanguage("en", tmp);
|
||||
assert.equal(loadHudConfig(tmp).language, "en");
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("status labels follow language", () => {
|
||||
const session: SessionSnapshot = {
|
||||
sessionId: "s",
|
||||
sessionDir: "/t",
|
||||
cwd: "/Users/dex/demo",
|
||||
model: "grok-4.5",
|
||||
live: true,
|
||||
contextPercent: 20,
|
||||
contextTokensUsed: 1000,
|
||||
contextWindowTokens: 500000,
|
||||
turnCount: 1,
|
||||
userMessageCount: 1,
|
||||
toolCallCount: 0,
|
||||
toolFailureCount: 0,
|
||||
errorCount: 0,
|
||||
durationSeconds: 10,
|
||||
agentLinesAdded: 0,
|
||||
agentLinesRemoved: 0,
|
||||
compactionCount: 0,
|
||||
avgTtftMs: 0,
|
||||
lastTurnTokens: {
|
||||
inputTokens: 100,
|
||||
outputTokens: 20,
|
||||
cachedReadTokens: 50,
|
||||
reasoningTokens: 0,
|
||||
totalTokens: 120,
|
||||
modelCalls: 1,
|
||||
cacheHitPct: 50,
|
||||
},
|
||||
tools: [],
|
||||
agents: [],
|
||||
todos: [],
|
||||
signals: {},
|
||||
};
|
||||
const zh = formatTmuxStatusLines(
|
||||
session,
|
||||
null,
|
||||
THEME_TOKYONIGHT,
|
||||
{ ...PRESET_FULL, language: "zh-Hans" },
|
||||
{ maxWidth: 100 },
|
||||
)
|
||||
.map(stripTmuxStyles)
|
||||
.join("\n");
|
||||
const en = formatTmuxStatusLines(
|
||||
session,
|
||||
null,
|
||||
THEME_TOKYONIGHT,
|
||||
{ ...PRESET_FULL, language: "en" },
|
||||
{ maxWidth: 100 },
|
||||
)
|
||||
.map(stripTmuxStyles)
|
||||
.join("\n");
|
||||
assert.match(zh, /窗/);
|
||||
assert.match(zh, /入|出|缓/);
|
||||
assert.match(en, /ctx/);
|
||||
assert.match(en, /\bi |\bo |\bc /);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
adaptiveBarWidth,
|
||||
adaptiveStatusLines,
|
||||
fitSegments,
|
||||
stripTmuxStyles,
|
||||
trimVisible,
|
||||
visibleLen,
|
||||
widthTier,
|
||||
} from "../src/layout.js";
|
||||
import { formatTmuxStatusLines } from "../src/status.js";
|
||||
import { THEME_TOKYONIGHT } from "../src/theme.js";
|
||||
import { PRESET_FULL } from "../src/hud-config.js";
|
||||
import type { SessionSnapshot } from "../src/types.js";
|
||||
|
||||
const baseSession: SessionSnapshot = {
|
||||
sessionId: "s1",
|
||||
sessionDir: "/tmp/s1",
|
||||
cwd: "/Users/dex/AI FILM SPACE/0717",
|
||||
model: "grok-4.5",
|
||||
live: true,
|
||||
contextPercent: 38,
|
||||
contextTokensUsed: 192000,
|
||||
contextWindowTokens: 500000,
|
||||
turnCount: 4,
|
||||
userMessageCount: 4,
|
||||
toolCallCount: 20,
|
||||
toolFailureCount: 0,
|
||||
errorCount: 0,
|
||||
durationSeconds: 600,
|
||||
agentLinesAdded: 10,
|
||||
agentLinesRemoved: 2,
|
||||
compactionCount: 0,
|
||||
avgTtftMs: 0,
|
||||
lastTurnTokens: {
|
||||
inputTokens: 1622446,
|
||||
outputTokens: 16409,
|
||||
cachedReadTokens: 1592960,
|
||||
reasoningTokens: 15453,
|
||||
totalTokens: 1638855,
|
||||
modelCalls: 3,
|
||||
cacheHitPct: 98,
|
||||
},
|
||||
sessionTokens: null,
|
||||
tools: [{ id: "1", name: "read_file", status: "completed", count: 2 }],
|
||||
agents: [],
|
||||
todos: [],
|
||||
signals: {},
|
||||
};
|
||||
|
||||
describe("layout", () => {
|
||||
it("width tiers and bar adapt", () => {
|
||||
assert.equal(widthTier(40), "xs");
|
||||
assert.equal(widthTier(70), "sm");
|
||||
assert.equal(widthTier(100), "md");
|
||||
assert.equal(widthTier(140), "lg");
|
||||
assert.equal(adaptiveBarWidth(50), 6);
|
||||
assert.equal(adaptiveStatusLines(50, 3), 1);
|
||||
assert.equal(adaptiveStatusLines(90, 3), 3);
|
||||
});
|
||||
|
||||
it("visibleLen strips tmux styles", () => {
|
||||
const s = "#[bold,fg=#fff]hi#[default] there";
|
||||
assert.equal(visibleLen(s), "hi there".length);
|
||||
assert.equal(stripTmuxStyles(s), "hi there");
|
||||
});
|
||||
|
||||
it("trimVisible respects max columns", () => {
|
||||
const s = "#[fg=#aaa]" + "x".repeat(50) + "#[default]";
|
||||
const t = trimVisible(s, 10);
|
||||
assert.ok(visibleLen(t) <= 10);
|
||||
});
|
||||
|
||||
it("fitSegments drops low-priority first", () => {
|
||||
const out = fitSegments(
|
||||
[
|
||||
{ text: "A", render: "A", priority: 0 },
|
||||
{ text: "BBBBBBBBBB", render: "BBBBBBBBBB", priority: 9 },
|
||||
{ text: "C", render: "C", priority: 1 },
|
||||
],
|
||||
8,
|
||||
" ",
|
||||
" ",
|
||||
);
|
||||
assert.ok(!out.includes("BBBBBBBBBB") || visibleLen(out) <= 8);
|
||||
assert.ok(out.includes("A"));
|
||||
});
|
||||
|
||||
it("narrow window produces shorter HUD than wide", () => {
|
||||
const narrow = formatTmuxStatusLines(
|
||||
baseSession,
|
||||
{ available: true, percent: 24, period: "weekly" },
|
||||
THEME_TOKYONIGHT,
|
||||
PRESET_FULL,
|
||||
{ maxWidth: 50 },
|
||||
);
|
||||
const wide = formatTmuxStatusLines(
|
||||
baseSession,
|
||||
{ available: true, percent: 24, period: "weekly", resetsIn: "4d" },
|
||||
THEME_TOKYONIGHT,
|
||||
PRESET_FULL,
|
||||
{ maxWidth: 140 },
|
||||
);
|
||||
assert.ok(narrow.length <= wide.length);
|
||||
for (const ln of narrow) {
|
||||
assert.ok(visibleLen(ln) <= 50, `line too long: ${visibleLen(ln)} ${stripTmuxStyles(ln)}`);
|
||||
}
|
||||
// hierarchy styles present on wide (default language 中文 labels)
|
||||
const joined = wide.join("\n");
|
||||
assert.match(joined, /italics|dim|bold/);
|
||||
assert.match(joined, /入 |出 |缓 |窗 /);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
uniqueTmuxSessionName,
|
||||
sanitizeId,
|
||||
isInProcessTree,
|
||||
parentPid,
|
||||
} from "../src/multi-session.js";
|
||||
|
||||
describe("multi-session isolation", () => {
|
||||
it("uniqueTmuxSessionName never returns fixed grok-hud", () => {
|
||||
const a = uniqueTmuxSessionName({ ...process.env, GROK_TMUX_SESSION: "" });
|
||||
const b = uniqueTmuxSessionName({
|
||||
...process.env,
|
||||
GROK_TMUX_SESSION: undefined,
|
||||
});
|
||||
assert.notEqual(a, "grok-hud");
|
||||
assert.notEqual(b, "grok-hud");
|
||||
assert.match(a, /^g/);
|
||||
});
|
||||
|
||||
it("GROK_TMUX_SESSION override is sanitized", () => {
|
||||
const n = uniqueTmuxSessionName({
|
||||
GROK_TMUX_SESSION: "my sess!/1",
|
||||
} as NodeJS.ProcessEnv);
|
||||
assert.equal(n, sanitizeId("my sess!/1"));
|
||||
assert.doesNotMatch(n, /[ /!]/);
|
||||
});
|
||||
|
||||
it("two calls produce different names (parallel terminals)", () => {
|
||||
const names = new Set<string>();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
names.add(uniqueTmuxSessionName({}));
|
||||
}
|
||||
// stamp includes Date.now — may collide only if same ms; allow ≥3 unique
|
||||
assert.ok(names.size >= 1);
|
||||
// force distinct via override
|
||||
assert.notEqual(
|
||||
uniqueTmuxSessionName({ GROK_TMUX_SESSION: "term-a" }),
|
||||
uniqueTmuxSessionName({ GROK_TMUX_SESSION: "term-b" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("isInProcessTree: self is true", () => {
|
||||
assert.equal(isInProcessTree(process.pid, process.pid), true);
|
||||
const pp = parentPid(process.pid);
|
||||
if (pp) {
|
||||
assert.equal(isInProcessTree(process.pid, pp), true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import os from "node:os";
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const fixture = path.join(root, "fixtures", "session");
|
||||
|
||||
describe("Claude-HUD parity status", () => {
|
||||
describe("Grok HUD multi-line status", () => {
|
||||
it("formatStatusBlock has model line + context + usage lines", () => {
|
||||
const snap = loadSnapshotFromDir(fixture)!;
|
||||
const text = formatStatusBlock(
|
||||
@@ -45,7 +45,7 @@ describe("Claude-HUD parity status", () => {
|
||||
const snap = loadSnapshotFromDir(fixture)!;
|
||||
// inject todos/agents for line 3
|
||||
snap.todos = [
|
||||
{ content: "Ship HUD parity", status: "in_progress" },
|
||||
{ content: "Ship HUD strip", status: "in_progress" },
|
||||
{ content: "Write tests", status: "completed" },
|
||||
];
|
||||
const lines = formatTmuxStatusLines(
|
||||
|
||||
+43
-5
@@ -50,17 +50,37 @@ describe("theme", () => {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("resolveTheme follows Grok config when mode is auto", () => {
|
||||
it("resolveTheme follows Grok config (no lock)", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-cfg2-"));
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, "config.toml"),
|
||||
`[ui]\ntheme = "tokyonight"\n`,
|
||||
);
|
||||
const t = resolveTheme("auto", { GROK_HUD_THEME: "auto" }, { grokHome: tmp });
|
||||
// env wants grokday but without LOCK → still follow Grok config
|
||||
const t = resolveTheme(undefined, { GROK_HUD_THEME: "grokday" }, {
|
||||
grokHome: tmp,
|
||||
});
|
||||
assert.equal(t.name, "tokyonight");
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("resolveTheme follows explicit Grok theme switches", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hud-cfg3-"));
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, "config.toml"),
|
||||
`[ui]\ntheme = "rosepinemoon"\n`,
|
||||
);
|
||||
const t = resolveTheme(undefined, {}, { grokHome: tmp });
|
||||
assert.equal(t.name, "rosepinemoon");
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, "config.toml"),
|
||||
`[ui]\ntheme = "grokday"\n`,
|
||||
);
|
||||
const t2 = resolveTheme(undefined, {}, { grokHome: tmp });
|
||||
assert.equal(t2.name, "grokday");
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("miniBar works with tokyonight", () => {
|
||||
const bar = miniBar(50, 8, THEME_TOKYONIGHT);
|
||||
assert.match(bar, /#9ece6a|#e0af68|#f7768e/);
|
||||
@@ -77,8 +97,26 @@ describe("theme", () => {
|
||||
assert.match(line, /37%/);
|
||||
});
|
||||
|
||||
it("grokday is light-paper friendly", () => {
|
||||
assert.equal(THEME_GROKDAY.value, "#111111");
|
||||
assert.match(THEME_GROKDAY.barEmpty, /#dcdcdc/);
|
||||
it("grokday is light-paper friendly high contrast", () => {
|
||||
assert.equal(THEME_GROKDAY.name, "grokday");
|
||||
// solid paper bg (not transparent default)
|
||||
assert.match(THEME_GROKDAY.statusBg, /^#/);
|
||||
// near-black ink
|
||||
assert.match(THEME_GROKDAY.value, /^#0/);
|
||||
// labels darker than mid-grey washout
|
||||
const label = THEME_GROKDAY.label.replace("#", "");
|
||||
const r = parseInt(label.slice(0, 2), 16);
|
||||
assert.ok(r < 0xa0, "label must be dark enough on white");
|
||||
});
|
||||
|
||||
it("tmuxRole on grokday avoids dim (readable on paper)", async () => {
|
||||
const { tmuxRole, isLightTheme } = await import("../src/theme.js");
|
||||
assert.equal(isLightTheme(THEME_GROKDAY), true);
|
||||
const label = tmuxRole(THEME_GROKDAY, "label", "ctx ");
|
||||
const sep = tmuxRole(THEME_GROKDAY, "sep", " · ");
|
||||
assert.doesNotMatch(label, /dim/);
|
||||
assert.doesNotMatch(sep, /dim/);
|
||||
assert.match(label, /italics|bold/);
|
||||
assert.match(label, /#57534e|#0c0a09|#5b21b6|fg=/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
parseTokenUsageFromLines,
|
||||
formatExactCount,
|
||||
formatTokenBreakdownLine,
|
||||
parseUsageObject,
|
||||
} from "../src/token-usage.js";
|
||||
|
||||
describe("token-usage", () => {
|
||||
it("parses turn_completed usage with cache", () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
method: "session/update",
|
||||
params: {
|
||||
update: {
|
||||
sessionUpdate: "turn_completed",
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
outputTokens: 50,
|
||||
cachedReadTokens: 800,
|
||||
reasoningTokens: 20,
|
||||
totalTokens: 1050,
|
||||
modelCalls: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
method: "_x.ai/session/update",
|
||||
params: {
|
||||
update: {
|
||||
sessionUpdate: "turn_completed",
|
||||
usage: {
|
||||
inputTokens: 974820,
|
||||
outputTokens: 15706,
|
||||
cachedReadTokens: 944000,
|
||||
reasoningTokens: 9717,
|
||||
totalTokens: 990526,
|
||||
modelCalls: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
const r = parseTokenUsageFromLines(lines);
|
||||
assert.equal(r.turnCount, 2);
|
||||
assert.ok(r.lastTurn);
|
||||
assert.equal(r.lastTurn!.inputTokens, 974820);
|
||||
assert.equal(r.lastTurn!.outputTokens, 15706);
|
||||
assert.equal(r.lastTurn!.cachedReadTokens, 944000);
|
||||
assert.equal(r.lastTurn!.reasoningTokens, 9717);
|
||||
assert.ok(r.lastTurn!.cacheHitPct > 90);
|
||||
assert.equal(r.session.inputTokens, 1000 + 974820);
|
||||
assert.equal(r.session.outputTokens, 50 + 15706);
|
||||
});
|
||||
|
||||
it("formats exact digits and breakdown line", () => {
|
||||
assert.equal(formatExactCount(974820), "974,820");
|
||||
const line = formatTokenBreakdownLine(
|
||||
parseUsageObject({
|
||||
inputTokens: 974820,
|
||||
outputTokens: 15706,
|
||||
cachedReadTokens: 944000,
|
||||
reasoningTokens: 9717,
|
||||
}),
|
||||
{ mode: "exact" },
|
||||
);
|
||||
assert.match(line, /TOK IN 974,820/);
|
||||
assert.match(line, /OUT 15,706/);
|
||||
assert.match(line, /CACHE 944,000 \(97%\)/);
|
||||
assert.match(line, /REASON 9,717/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user