Files
Codeman/test/statusline-cli-flag.test.ts
T
timkjrandClaude Sonnet 5 d5b75af628 fix(statusline): sticky telemetry collection, footer print-through, EOF fix
Responds to Ark0N's review round on the ephemeral-CLI-flag statusline
injection rework:

- Rebase-detail fixes: registry-gated telemetry eligibility via
  getCli(mode)?.capabilities.statusLineTelemetry instead of a hardcoded
  mode === 'claude' check, using the capability flag master's CLI-registry
  refactor already declares for exactly this purpose.

- Design question settled: sticky (a). Rather than persisting the toggle
  as a new field and threading it through every session-creation path
  (cron, Ralph Loop API, quick-start), eliminated the per-session field
  entirely. readPlanUsageTelemetryEnabled() (hooks-config.ts) reads the
  existing showPlanUsageLimits setting fresh from settings.json at every
  claude create/respawn (TmuxManager.createSession/respawnPane) - no
  per-session state to survive a restart, and it applies uniformly to
  every creation path for free, since they all flow through the same
  TmuxManager methods.

  This required fixing a real bug found along the way: showPlanUsageLimits
  was not actually round-tripping through settings.json on save -
  settings-ui.js explicitly excluded it from the PUT body as a pure
  per-device display key. It now flows through normally (both true and
  false); the load-side per-device merge behavior is unchanged.

  Removed entirely as a result: the statusLineTelemetry field from
  CreateSessionSchema/SettingsUpdateSchema, CreateSessionOptions/
  RespawnPaneOptions, Session._statusLineTelemetry (this is what makes
  the restart-persistence bug moot rather than patched), and the
  frontend send sites.

- Footer print-through restored: the no-user-statusline branch of the
  exporter script now runs the telemetry POST in the foreground so its
  own stdout becomes the in-terminal footer, falling back to a plain
  "codeman" marker only on curl failure.

- Background-subshell EOF fix: the wrap-a-real-statusline branch closes
  stdin too, not just stdout/stderr (`>/dev/null 2>&1 </dev/null &`) -
  the un-redirected subshell process itself, not curl, was what held a
  reader-to-EOF's pipe open for however long curl took to finish. Added
  curl --max-time 5 so a hung (not just refused) Codeman cannot wedge
  the render.

Tests: real-shell-execution tests for the footer/EOF fixes (fake curl
stand-in on PATH, real sh subprocess spawns, real elapsed-time
measurements - verified non-vacuous against a hand-reconstructed
old-style script), unit tests for readPlanUsageTelemetryEnabled.
Adapted two existing tests whose payloads referenced the removed field.
Fixed during independent code review: a stray indentation break and a
test exercising the wrong (legacy) exporter code path.

Docs synced: CLAUDE.md, docs/usage-limits-display-plan.md (old
disk-based section marked superseded, kept for history),
docs/architecture-invariants.md.

Full suite green: 352 files, 6780 passed, 12 skipped, 0 failed.
tsc/lint/format:check/frontend-syntax all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 20:37:29 -05:00

97 lines
4.7 KiB
TypeScript

/**
* @fileoverview Tests for the plan-usage statusLine exporter riding an EPHEMERAL
* `claude --settings` CLI flag (buildSpawnCommand's statusLineCommand option),
* which superseded writing it into `.claude/settings.local.json` — see
* resolveStatusLineCliCommand in hooks-config.ts and its own tests. Verified
* live against a real Claude CLI (isolated tmux socket, 2026-08-31) that
* `--settings` accepts this exact shape and takes precedence over a file-based
* statusLine.
*
* Extracting and re-parsing the `--settings` argument goes through a REAL
* shell (bash -c) rather than a hand-rolled unescaper: the exporter command
* itself embeds both single and double quotes, so trusting anything but the
* shell's own quoting rules to reverse shellescape() would just be testing
* this file's guess at the algorithm, not the actual behavior a spawned pane
* sees.
*/
import { describe, it, expect } from 'vitest';
import { execFileSync } from 'node:child_process';
import { buildSpawnCommand } from '../src/tmux-manager.js';
const EXPORTER_CMD = 'curl -sk -X POST "$CODEMAN_API_URL/api/status-telemetry" --data @- 2>/dev/null || echo codeman';
/** Extract the `--settings <arg>` fragment from a built command and have a
* real shell resolve its quoting, printing the arg back out verbatim. */
function extractSettingsJson(cmd: string): unknown {
const idx = cmd.indexOf('--settings ');
expect(idx).toBeGreaterThan(-1);
const fragment = cmd.slice(idx);
const out = execFileSync('bash', ['-c', `set -- ${fragment}; printf '%s' "$2"`]).toString();
return JSON.parse(out);
}
describe('buildSpawnCommand statusLineCommand (claude mode)', () => {
it('omits --settings entirely when no statusLineCommand and no effort are given', () => {
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1' });
expect(cmd).not.toContain('--settings');
});
it('embeds the exporter command under a statusLine settings key', () => {
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', statusLineCommand: EXPORTER_CMD });
expect(cmd).toContain('--settings');
expect(extractSettingsJson(cmd)).toEqual({ statusLine: { type: 'command', command: EXPORTER_CMD } });
});
it('merges statusLine and ultracode into the SAME --settings object', () => {
const cmd = buildSpawnCommand({
mode: 'claude',
sessionId: 'sid-1',
effort: 'ultracode',
statusLineCommand: EXPORTER_CMD,
});
// Only one --settings flag total — never two (Claude Code accepts just one).
expect(cmd.match(/--settings/g)).toHaveLength(1);
expect(extractSettingsJson(cmd)).toEqual({
ultracode: true,
statusLine: { type: 'command', command: EXPORTER_CMD },
});
});
it('keeps a regular --effort flag separate from --settings when both are present', () => {
const cmd = buildSpawnCommand({
mode: 'claude',
sessionId: 'sid-1',
effort: 'high',
statusLineCommand: EXPORTER_CMD,
});
expect(cmd).toContain('--effort');
expect(cmd).toContain('--settings');
expect(extractSettingsJson(cmd)).toEqual({ statusLine: { type: 'command', command: EXPORTER_CMD } });
});
it('shell-escapes an exporter command containing single AND double quotes without breaking the flag', () => {
// The real exporter (generateStatusLineCommand) embeds both — a naive
// `'${value}'` wrap would be broken out of by the single quotes.
const tricky = `echo '{}'; printf '{"a":1}' | curl -sk`;
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', statusLineCommand: tricky });
expect(extractSettingsJson(cmd)).toEqual({ statusLine: { type: 'command', command: tricky } });
});
it('round-trips the REAL exporter script path unmodified (ensureStatusLineExporterScript)', async () => {
// What resolveStatusLineCliCommand actually hands to buildSpawnCommand at spawn
// time today is a bare script PATH (see that function's doc comment for why —
// never the raw curl command generateStatusLineCommand() builds, which only
// backs the legacy disk-write applyStatusLineConfig path now).
const { ensureStatusLineExporterScript } = await import('../src/hooks-config.js');
const real = await ensureStatusLineExporterScript();
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', statusLineCommand: real });
expect(extractSettingsJson(cmd)).toEqual({ statusLine: { type: 'command', command: real } });
});
it('never adds --settings for non-claude modes even if statusLineCommand is somehow set', () => {
const cmd = buildSpawnCommand({ mode: 'omp', sessionId: 'sid-1', statusLineCommand: EXPORTER_CMD } as never);
expect(cmd).not.toContain('--settings');
});
});