fix(context): strict threshold validation + refresh hard-fail + CLI tests

Cycle-2 of bad82b3 per QA REJECT-AND-REVISE.

QA Fact 1 (missing tests): added 10 CLI context tests covering
human output, JSON shape, --rig scoping, --threshold filtering
with stale/unknown retention, sort order, stale-vs-ok, unknown
honesty, and invalid threshold rejection.

QA Fact 2 (threshold parsing bug): replaced bare parseInt with
strict Number() + isInteger + 0-100 range check. Invalid input
(abc, -1, 101) now exits 2 with copy matching rigx-context
oracle: "must be an integer percentage".

QA Fact 3 (refresh failure semantics): refresh failure is now a
hard error — exits 2 with fact/detail/fix copy. No silent stale
data display. Non-refresh reads continue to work normally.

Tests: context.test.ts 10/10; CLI 456/457 (preexisting chatroom);
daemon 1812/1812; UI 396/396; tsc clean all workspaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.6 committed 2026-04-23 03:20:28 -07:00
1 parent bad82b3a7b
commit a5d5fc3eae
2 files changed
+227 -23

No files matched your search

+30 -23
View File
@@ -158,10 +158,23 @@ Examples:
cmd
.option("--json", "JSON output for agents")
.option("--rig <name>", "Show one rig only")
.option("--threshold <pct>", "Show seats at or above this percentage", parseInt)
.option("--threshold <pct>", "Show seats at or above this percentage")
.option("--refresh", "Re-sample context usage before displaying")
.action(async (opts: { json?: boolean; rig?: string; threshold?: number; refresh?: boolean }) => {
.action(async (opts: { json?: boolean; rig?: string; threshold?: string; refresh?: boolean }) => {
const deps = getDepsF();
// Strict threshold validation — reject non-integer input
let threshold: number | null = null;
if (opts.threshold != null) {
const parsed = Number(opts.threshold);
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
console.error("rig context: --threshold must be an integer percentage (0-100)");
process.exitCode = 2;
return;
}
threshold = parsed;
}
const status = await getDaemonStatus(deps.lifecycleDeps);
if (status.state !== "running" || status.healthy === false) {
console.error("Daemon is not running. Start it with: rig daemon start");
@@ -184,26 +197,25 @@ Examples:
return;
}
// Refresh once globally if requested (pollOnce samples all active Claude sessions)
let refreshFailed = false;
// Refresh once globally if requested (pollOnce samples all active Claude sessions).
// Refresh failure is a hard error — do not silently display stale data as if refreshed.
if (opts.refresh && targetRigs.length > 0) {
const firstRig = targetRigs[0]!;
try {
const refreshResult = await client.get(`/api/rigs/${firstRig.rigId}/nodes?refresh=true`);
if (refreshResult.status >= 400) {
refreshFailed = true;
if (!opts.json) {
console.error("Context refresh failed. Displaying stale data.");
console.error(`Detail: ${JSON.stringify(refreshResult.data)}`);
console.error("");
}
}
} catch {
refreshFailed = true;
if (!opts.json) {
console.error("Context refresh failed. Displaying stale data.");
console.error("");
console.error("Context refresh failed. Data may be stale.");
console.error(`Detail: ${JSON.stringify(refreshResult.data)}`);
console.error("Fix: retry without --refresh to see stale data, or check daemon logs.");
process.exitCode = 2;
return;
}
} catch (refreshErr) {
console.error("Context refresh failed. Data may be stale.");
console.error(`Detail: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`);
console.error("Fix: retry without --refresh to see stale data, or check daemon logs.");
process.exitCode = 2;
return;
}
}
@@ -219,7 +231,7 @@ Examples:
const seats = sortSeats(
allNodes
.map(analyze)
.filter((s) => isVisible(s, opts.threshold ?? null))
.filter((s) => isVisible(s, threshold))
);
const summary = {
@@ -233,12 +245,7 @@ Examples:
};
if (opts.json) {
const output: Record<string, unknown> = { seats, summary };
if (refreshFailed) {
output.refreshFailed = true;
output.refreshError = "Context refresh failed. Data may be stale.";
}
console.log(JSON.stringify(output, null, 2));
console.log(JSON.stringify({ seats, summary }, null, 2));
return;
}
+197
View File
@@ -0,0 +1,197 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { contextCommand, type ContextDeps } from "../src/commands/context.js";
function makeDeps(overrides?: {
daemonState?: string;
rigNames?: string[];
nodesByRig?: Record<string, unknown[]>;
}): ContextDeps {
const rigs = (overrides?.rigNames ?? ["test-rig"]).map((name, i) => ({
rigId: `rig-${i}`, name, nodeCount: 1, runningCount: 1, status: "running", uptime: "1h", latestSnapshot: null,
}));
const nodesByRig = overrides?.nodesByRig ?? {
"rig-0": [
{
rigId: "rig-0", rigName: "test-rig", logicalId: "dev.impl", canonicalSessionName: "dev-impl@test-rig",
nodeKind: "agent", runtime: "claude-code", sessionStatus: "running", startupStatus: "ready",
restoreOutcome: "n-a", tmuxAttachCommand: null, resumeCommand: null, latestError: null,
contextUsage: {
usedPercentage: 85, remainingPercentage: 15, contextWindowSize: 1000000,
source: "claude_statusline_json", availability: "known",
sampledAt: new Date(Date.now() - 60_000).toISOString(), fresh: true,
},
},
{
rigId: "rig-0", rigName: "test-rig", logicalId: "dev.qa", canonicalSessionName: "dev-qa@test-rig",
nodeKind: "agent", runtime: "codex", sessionStatus: "running", startupStatus: "ready",
restoreOutcome: "n-a", tmuxAttachCommand: null, resumeCommand: null, latestError: null,
contextUsage: { usedPercentage: null, remainingPercentage: null, contextWindowSize: null, source: null, availability: "unknown", sampledAt: null, fresh: false },
},
{
rigId: "rig-0", rigName: "test-rig", logicalId: "dev.synth", canonicalSessionName: "dev-synth@test-rig",
nodeKind: "agent", runtime: "claude-code", sessionStatus: "running", startupStatus: "ready",
restoreOutcome: "n-a", tmuxAttachCommand: null, resumeCommand: null, latestError: null,
contextUsage: {
usedPercentage: 50, remainingPercentage: 50, contextWindowSize: 1000000,
source: "claude_statusline_json", availability: "known",
sampledAt: new Date(Date.now() - 3600_000).toISOString(), fresh: false,
},
},
],
};
return {
lifecycleDeps: {} as ContextDeps["lifecycleDeps"],
clientFactory: () => ({
get: vi.fn(async (path: string) => {
if (path === "/api/ps") return { status: 200, data: rigs };
for (const [key, nodes] of Object.entries(nodesByRig)) {
if (path.includes(key)) return { status: 200, data: nodes };
}
return { status: 200, data: [] };
}),
}) as unknown as ReturnType<ContextDeps["clientFactory"]>,
};
}
// Patch getDaemonStatus + getDaemonUrl for tests
vi.mock("../src/daemon-lifecycle.js", async () => {
const actual = await vi.importActual<Record<string, unknown>>("../src/daemon-lifecycle.js");
return {
...actual,
getDaemonStatus: vi.fn(async () => ({ state: "running", healthy: true, pid: 1234, port: 7433 })),
getDaemonUrl: vi.fn(() => "http://localhost:7433"),
};
});
describe("rig context", () => {
let logs: string[];
let errors: string[];
beforeEach(() => {
logs = [];
errors = [];
vi.spyOn(console, "log").mockImplementation((...args) => logs.push(args.join(" ")));
vi.spyOn(console, "error").mockImplementation((...args) => errors.push(args.join(" ")));
process.exitCode = undefined;
});
it("human output includes seat table with status + fleet summary", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig"]);
const output = logs.join("\n");
expect(output).toContain("CONTEXT USAGE");
expect(output).toContain("dev-impl@test-rig");
expect(output).toContain("CRITICAL");
expect(output).toContain("dev-qa@test-rig");
expect(output).toContain("unknown");
expect(output).toContain("dev-synth@test-rig");
expect(output).toContain("stale");
expect(output).toContain("FLEET SUMMARY:");
});
it("JSON output includes seats array + summary with correct fields", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--json"]);
const json = JSON.parse(logs.join(""));
expect(json.seats).toBeInstanceOf(Array);
expect(json.seats.length).toBe(3);
expect(json.summary).toBeDefined();
expect(json.summary.total).toBe(3);
const critical = json.seats.find((s: { session: string }) => s.session === "dev-impl@test-rig");
expect(critical.urgency).toBe("critical");
expect(critical.freshness).toBe("fresh");
expect(critical.status).toBe("critical");
expect(critical.usedPercentage).toBe(85);
const unknown = json.seats.find((s: { session: string }) => s.session === "dev-qa@test-rig");
expect(unknown.urgency).toBe("unknown");
expect(unknown.status).toBe("unknown");
const stale = json.seats.find((s: { session: string }) => s.session === "dev-synth@test-rig");
expect(stale.urgency).toBe("low");
expect(stale.freshness).toBe("stale");
expect(stale.status).toBe("stale");
});
it("sort order: critical > warning > stale > ok > unknown", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--json"]);
const json = JSON.parse(logs.join(""));
const sessions = json.seats.map((s: { session: string }) => s.session);
// critical first, then stale (low+stale), then unknown
expect(sessions).toEqual(["dev-impl@test-rig", "dev-synth@test-rig", "dev-qa@test-rig"]);
});
it("--rig filters to one rig", async () => {
const cmd = contextCommand(makeDeps({ rigNames: ["rig-a", "rig-b"] }));
await cmd.parseAsync(["node", "rig", "--rig", "rig-a", "--json"]);
const output = logs.join("\n");
// Should not error — even if node data is empty for the filtered rig
expect(process.exitCode).toBeUndefined();
});
it("--rig with unknown rig name exits 1", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--rig", "nonexistent"]);
expect(process.exitCode).toBe(1);
expect(errors.join(" ")).toContain("not found");
});
it("--threshold filters seats but keeps unknown and stale visible", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--threshold", "80", "--json"]);
const json = JSON.parse(logs.join(""));
const sessions = json.seats.map((s: { session: string }) => s.session);
// 85% critical (above threshold) + 50% stale (always visible) + unknown (always visible)
expect(sessions).toContain("dev-impl@test-rig");
expect(sessions).toContain("dev-synth@test-rig"); // stale — always visible
expect(sessions).toContain("dev-qa@test-rig"); // unknown — always visible
});
it("--threshold abc rejects with nonzero exit", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--threshold", "abc"]);
expect(process.exitCode).toBe(2);
expect(errors.join(" ")).toContain("integer percentage");
});
it("--threshold -1 rejects with nonzero exit", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--threshold", "-1"]);
expect(process.exitCode).toBe(2);
});
it("low + fresh = ok; low + stale = stale (never ok)", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--json"]);
const json = JSON.parse(logs.join(""));
const stale = json.seats.find((s: { session: string }) => s.session === "dev-synth@test-rig");
// 50% (low urgency) + stale freshness => status must be "stale", NOT "ok"
expect(stale.status).toBe("stale");
expect(stale.displayStatus).toBe("stale");
});
it("unknown seats have honest unknown status", async () => {
const cmd = contextCommand(makeDeps());
await cmd.parseAsync(["node", "rig", "--json"]);
const json = JSON.parse(logs.join(""));
const unknown = json.seats.find((s: { session: string }) => s.session === "dev-qa@test-rig");
expect(unknown.usedPercentage).toBeNull();
expect(unknown.urgency).toBe("unknown");
expect(unknown.status).toBe("unknown");
expect(unknown.fresh).toBe(false);
});
});