mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Ported from #416 (discussion #405): a statusline reading just `codeman` is what a hand-run claude in a managed repo showed, and it reads as a broken config rather than a footer. Three paths produced it and all three now yield an empty footer: the exporter's `|| echo codeman` fallback (now `curl -sfk ... || true`, with -f keeping an HTTP error body off stdout), the unknown-session answer of POST /api/status-telemetry, and formatSessionStatusText() with nothing to show. The exporter script marker moves to V4 so live installs pick the new content up on the next spawn. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
103 lines
4.3 KiB
TypeScript
103 lines
4.3 KiB
TypeScript
/**
|
|
* 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 an EMPTY footer, never a brand word', async () => {
|
|
const res = await post({ sessionId: 'does-not-exist', data: REAL });
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toBe('');
|
|
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);
|
|
});
|
|
});
|