mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
fix(usage): address code-review findings in plan-usage telemetry
Review of the plan-usage chip feature (commits since 1.0.0) surfaced several issues; this fixes all confirmed findings: - HIGH: applyStatusLineConfig clobbered a user's hand-authored statusLine on the enable path (the isOurs guard only protected disable). Now bails out when an existing statusLine isn't ours, on both the enable and disable paths. - MED: StatusTelemetrySchema used z.optional() (rejects null) on Claude's undocumented statusline fields — a single stray null 400'd the entire POST and silently killed the chip's data feed. Switched the modeled fields to .nullish(). - MED: dropping the Token Count / Show Cost header toggles left their features reading settings.showTokenCount/showCost, but saveAppSettings rebuilds settings fresh from the DOM, dropping those keys and resetting them to defaults on every save (re-enabling the token chip with no UI to turn it off). Preserve the prior stored preference. - telemetrySignature keyed on contextUsedPercentage (never displayed) and the raw unrounded %, churning a redundant SSE broadcast + localStorage write + identical chip re-render on every assistant message. Now keys on the rounded displayed window values only. - Plan-usage chip flashed hidden on load (no server-side reveal): renderIndexHtml now strips header-plan-usage--hidden when enabled, matching btn-multimonitor; fixes the FOUC and makes the "server renders initial state" comments accurate. - Serialize all settings.local.json read-modify-write writers in hooks-config via a shared per-path mutex (previously lock-free; concurrent session-create + settings-toggle on the same repo could lose writes). - Hardened the chip's innerHTML against any future string field; removed the dead _latestPlanUsage field; clamped ctx% in the footer formatter; corrected the session-create comment (the path is add-only by design — a per-repo settings file is shared by sibling sessions). - Tests: new test/routes/status-telemetry-routes.test.ts (route behavior, dedup, null-tolerance) + NaN/Infinity/fractional and signature-churn unit tests; made server-index-title.test.ts deterministic against the ambient settings.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Route tests for POST /api/status-telemetry — the Claude statusline exporter
|
||||
* endpoint that feeds the header "Plan Usage Limits" chip.
|
||||
*
|
||||
* Covers: broadcast on real telemetry + footer print-through, unknown-session
|
||||
* skip, per-session change-detection (dedup), rebroadcast on a displayed change,
|
||||
* NO rebroadcast on context-only drift, null-tolerance of Claude's undocumented
|
||||
* fields (the .nullish() schema — the project's recurring .optional()/null trap),
|
||||
* and 400 on a malformed body.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createRouteTestHarness, type RouteTestHarness } from './_route-test-utils.js';
|
||||
import { registerStatusTelemetryRoutes } from '../../src/web/routes/status-telemetry-routes.js';
|
||||
import { SessionStatusTelemetry } from '../../src/web/sse-events.js';
|
||||
|
||||
const SID = 'test-session-1'; // default id created by createMockRouteContext
|
||||
|
||||
const REAL = {
|
||||
rate_limits: {
|
||||
five_hour: { used_percentage: 15, resets_at: 1781409000 },
|
||||
seven_day: { used_percentage: 34, resets_at: 1781827200 },
|
||||
},
|
||||
context_window: { used_percentage: 56, total_input_tokens: 562411, total_output_tokens: 1188 },
|
||||
cost: { total_cost_usd: 0.0415 },
|
||||
model: { display_name: 'Opus 4.8 (1M context)' },
|
||||
};
|
||||
|
||||
describe('POST /api/status-telemetry', () => {
|
||||
let h: RouteTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
h = await createRouteTestHarness(registerStatusTelemetryRoutes, { sessionId: SID });
|
||||
});
|
||||
|
||||
const post = (body: unknown) => h.app.inject({ method: 'POST', url: '/api/status-telemetry', payload: body });
|
||||
|
||||
it('broadcasts plan-usage telemetry and returns the session-status footer', async () => {
|
||||
const res = await post({ sessionId: SID, data: REAL });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers['content-type']).toContain('text/plain');
|
||||
expect(res.body).toBe('Opus 4.8 (1M context) in:562,411 out:1,188 ctx:56%');
|
||||
expect(h.ctx.broadcast).toHaveBeenCalledTimes(1);
|
||||
const [event, payload] = h.ctx.broadcast.mock.calls[0];
|
||||
expect(event).toBe(SessionStatusTelemetry);
|
||||
expect(payload).toMatchObject({
|
||||
sessionId: SID,
|
||||
fiveHour: { usedPercentage: 15 },
|
||||
sevenDay: { usedPercentage: 34 },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not broadcast for an unknown session; returns the brand footer', async () => {
|
||||
const res = await post({ sessionId: 'does-not-exist', data: REAL });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toBe('codeman');
|
||||
expect(h.ctx.broadcast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dedups identical telemetry — rebroadcasts only once', async () => {
|
||||
await post({ sessionId: SID, data: REAL });
|
||||
await post({ sessionId: SID, data: REAL });
|
||||
expect(h.ctx.broadcast).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rebroadcasts when a displayed window percentage changes', async () => {
|
||||
await post({ sessionId: SID, data: REAL });
|
||||
const moved = {
|
||||
...REAL,
|
||||
rate_limits: { ...REAL.rate_limits, five_hour: { used_percentage: 16, resets_at: 1781409000 } },
|
||||
};
|
||||
await post({ sessionId: SID, data: moved });
|
||||
expect(h.ctx.broadcast).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does NOT rebroadcast on context-only drift (the chip never shows context %)', async () => {
|
||||
await post({ sessionId: SID, data: REAL });
|
||||
await post({ sessionId: SID, data: { ...REAL, context_window: { ...REAL.context_window, used_percentage: 91 } } });
|
||||
expect(h.ctx.broadcast).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("tolerates null in Claude's undocumented fields (no 400) and ignores them", async () => {
|
||||
const res = await post({
|
||||
sessionId: SID,
|
||||
data: {
|
||||
rate_limits: { five_hour: { used_percentage: 20, resets_at: 1781409000 }, seven_day: null },
|
||||
cost: { total_cost_usd: null },
|
||||
model: { display_name: null },
|
||||
context_window: { used_percentage: null, total_input_tokens: null, total_output_tokens: null },
|
||||
},
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(h.ctx.broadcast).toHaveBeenCalledTimes(1);
|
||||
const [, payload] = h.ctx.broadcast.mock.calls[0];
|
||||
expect(payload).toMatchObject({ fiveHour: { usedPercentage: 20 } });
|
||||
expect(payload.sevenDay).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects a malformed body (missing sessionId) with 400', async () => {
|
||||
const res = await post({ data: REAL });
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
@@ -21,11 +21,11 @@
|
||||
* Port: N/A (no server start)
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { readFileSync, mkdtempSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { hostname as osHostname } from 'node:os';
|
||||
import { hostname as osHostname, tmpdir } from 'node:os';
|
||||
import { WebServer } from '../src/web/server.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -40,6 +40,21 @@ async function render(host?: string): Promise<string> {
|
||||
}
|
||||
|
||||
describe('WebServer index.html <title> templating (#82)', () => {
|
||||
// renderIndexHtml reads the ambient settings.json (for the gesture bundle and
|
||||
// the header-toggle marker-class strips, e.g. showPlanUsageLimits /
|
||||
// showMultiMonitorButton). Point it at an empty data dir so this test is
|
||||
// deterministic regardless of the developer's real settings — otherwise an
|
||||
// enabled toggle would strip a marker class and break the byte-identical
|
||||
// assertion below. getDataDir() reads CODEMAN_DATA_DIR fresh per call.
|
||||
const _prevDataDir = process.env.CODEMAN_DATA_DIR;
|
||||
beforeAll(() => {
|
||||
process.env.CODEMAN_DATA_DIR = mkdtempSync(join(tmpdir(), 'codeman-title-test-'));
|
||||
});
|
||||
afterAll(() => {
|
||||
if (_prevDataDir === undefined) delete process.env.CODEMAN_DATA_DIR;
|
||||
else process.env.CODEMAN_DATA_DIR = _prevDataDir;
|
||||
});
|
||||
|
||||
it('substitutes the bare <title>Codeman</title> with codeman:<host>', async () => {
|
||||
const html = await render('laptop');
|
||||
expect(html).toContain('<title>codeman:laptop</title>');
|
||||
|
||||
@@ -68,6 +68,24 @@ describe('parseStatusTelemetry', () => {
|
||||
it('ignores a zero/negative reset timestamp', () => {
|
||||
expect(parseStatusTelemetry({ rate_limits: { five_hour: { used_percentage: 10, resets_at: 0 } } })).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps a NaN percentage as 0 and drops a window with a non-finite reset', () => {
|
||||
const t = parseStatusTelemetry({
|
||||
rate_limits: {
|
||||
five_hour: { used_percentage: NaN, resets_at: 1781409000 },
|
||||
seven_day: { used_percentage: 40, resets_at: Infinity },
|
||||
},
|
||||
});
|
||||
expect(t!.fiveHour).toEqual({ usedPercentage: 0, resetAt: 1781409000 * 1000 });
|
||||
expect(t!.sevenDay).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rounds a fractional resets_at to whole milliseconds', () => {
|
||||
const t = parseStatusTelemetry({
|
||||
rate_limits: { five_hour: { used_percentage: 10, resets_at: 1781409000.7 } },
|
||||
});
|
||||
expect(t!.fiveHour?.resetAt).toBe(Math.round(1781409000.7 * 1000));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSessionStatus', () => {
|
||||
@@ -118,4 +136,27 @@ describe('telemetrySignature', () => {
|
||||
})!;
|
||||
expect(telemetrySignature(moved)).not.toBe(telemetrySignature(a));
|
||||
});
|
||||
|
||||
it('ignores contextUsedPercentage (not displayed) so it does not churn each message', () => {
|
||||
const base = { rate_limits: { five_hour: { used_percentage: 15, resets_at: 1781409000 } } };
|
||||
const a = parseStatusTelemetry({ ...base, context_window: { used_percentage: 56 } })!;
|
||||
const b = parseStatusTelemetry({ ...base, context_window: { used_percentage: 91 } })!;
|
||||
expect(telemetrySignature(a)).toBe(telemetrySignature(b));
|
||||
});
|
||||
|
||||
it('keys on the ROUNDED window percentage (matches the chip) — sub-integer drift is ignored', () => {
|
||||
const sig = (p: number) =>
|
||||
telemetrySignature(
|
||||
parseStatusTelemetry({ rate_limits: { five_hour: { used_percentage: p, resets_at: 1781409000 } } })!
|
||||
);
|
||||
expect(sig(15.1)).toBe(sig(15.4)); // both render as 15%
|
||||
expect(sig(15.1)).not.toBe(sig(15.6)); // 15% vs 16%
|
||||
});
|
||||
|
||||
it('excludes cost/model (not shown in the chip) from the signature', () => {
|
||||
const base = { rate_limits: { five_hour: { used_percentage: 15, resets_at: 1781409000 } } };
|
||||
const a = parseStatusTelemetry({ ...base, cost: { total_cost_usd: 0.01 }, model: { display_name: 'A' } })!;
|
||||
const b = parseStatusTelemetry({ ...base, cost: { total_cost_usd: 9.99 }, model: { display_name: 'B' } })!;
|
||||
expect(telemetrySignature(a)).toBe(telemetrySignature(b));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user