@ricky-stevens/context-guardian 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +63 -0
- package/.github/workflows/ci.yml +66 -0
- package/CLAUDE.md +132 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/biome.json +34 -0
- package/bun.lock +31 -0
- package/hooks/precompact.mjs +73 -0
- package/hooks/session-start.mjs +133 -0
- package/hooks/stop.mjs +172 -0
- package/hooks/submit.mjs +133 -0
- package/lib/checkpoint.mjs +258 -0
- package/lib/compact-cli.mjs +124 -0
- package/lib/compact-output.mjs +350 -0
- package/lib/config.mjs +40 -0
- package/lib/content.mjs +33 -0
- package/lib/diagnostics.mjs +221 -0
- package/lib/estimate.mjs +254 -0
- package/lib/extract-helpers.mjs +869 -0
- package/lib/handoff.mjs +329 -0
- package/lib/logger.mjs +34 -0
- package/lib/mcp-tools.mjs +200 -0
- package/lib/paths.mjs +90 -0
- package/lib/stats.mjs +81 -0
- package/lib/statusline.mjs +123 -0
- package/lib/synthetic-session.mjs +273 -0
- package/lib/tokens.mjs +170 -0
- package/lib/tool-summary.mjs +399 -0
- package/lib/transcript.mjs +939 -0
- package/lib/trim.mjs +158 -0
- package/package.json +22 -0
- package/skills/compact/SKILL.md +20 -0
- package/skills/config/SKILL.md +70 -0
- package/skills/handoff/SKILL.md +26 -0
- package/skills/prune/SKILL.md +20 -0
- package/skills/stats/SKILL.md +100 -0
- package/sonar-project.properties +12 -0
- package/test/checkpoint.test.mjs +171 -0
- package/test/compact-cli.test.mjs +230 -0
- package/test/compact-output.test.mjs +284 -0
- package/test/compaction-e2e.test.mjs +809 -0
- package/test/content.test.mjs +86 -0
- package/test/diagnostics.test.mjs +188 -0
- package/test/edge-cases.test.mjs +543 -0
- package/test/estimate.test.mjs +262 -0
- package/test/extract-helpers-coverage.test.mjs +333 -0
- package/test/extract-helpers.test.mjs +234 -0
- package/test/handoff.test.mjs +738 -0
- package/test/integration.test.mjs +582 -0
- package/test/logger.test.mjs +70 -0
- package/test/manual-compaction-test.md +426 -0
- package/test/mcp-tools.test.mjs +443 -0
- package/test/paths.test.mjs +250 -0
- package/test/quick-compaction-test.md +191 -0
- package/test/stats.test.mjs +88 -0
- package/test/statusline.test.mjs +222 -0
- package/test/submit.test.mjs +232 -0
- package/test/synthetic-session.test.mjs +600 -0
- package/test/tokens.test.mjs +293 -0
- package/test/tool-summary.test.mjs +771 -0
- package/test/transcript-coverage.test.mjs +369 -0
- package/test/transcript.test.mjs +596 -0
- package/test/trim.test.mjs +356 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Quick Compaction Verification Test
|
|
2
|
+
|
|
3
|
+
Abbreviated version of the full test. 10 messages, compact, verify.
|
|
4
|
+
Covers: cold/warm/hot tiers, edit preservation, noise stripping, hybrid restore.
|
|
5
|
+
|
|
6
|
+
Run with: `claude --plugin-dir /path/to/context-guardian`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Phase 0 — Setup
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
We're running a quick compaction test. Rules:
|
|
14
|
+
1. "Zephyr-9" details are fictional — do NOT save to memory
|
|
15
|
+
2. When confirming fictional data, just say "Confirmed"
|
|
16
|
+
3. Keep responses SHORT
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Phase 1 — Build transcript (10 messages, send each separately)
|
|
22
|
+
|
|
23
|
+
### Messages 1-3 (COLD tier)
|
|
24
|
+
|
|
25
|
+
**Message 1:**
|
|
26
|
+
```
|
|
27
|
+
Project "Zephyr-9": PostgreSQL 17.2 on port 5433. Lead architect Diana Kowalski chose Caddy on January 14th 2026. Payment provider: Meridian Pay. Artifacts: s3://zephyr9-artifacts-prod/v3/. Auth secret rotated every 72 hours by cron "keymaster-rotate".
|
|
28
|
+
|
|
29
|
+
Read lib/trim.mjs — what functions does it export?
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Message 2:**
|
|
33
|
+
```
|
|
34
|
+
Bug ZEP-4471: FD leak on batch orders > 2,847 items. Root cause: src/order-mesh/batch/splitter.go line 389, deferred file.Close() in wrong scope. Fix PR #1847 by Tomás Herrera. Circuit breaker changed from 5 errors/10s to 8 errors/30s.
|
|
35
|
+
|
|
36
|
+
Create /tmp/cg-quick-test.js with:
|
|
37
|
+
function processOrder(input) {
|
|
38
|
+
const total = input.price * input.quantity;
|
|
39
|
+
const tax = total * 0.1;
|
|
40
|
+
return { total, tax, final: total + tax };
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Message 3:**
|
|
45
|
+
```
|
|
46
|
+
I chose Option B for sharding: compound key (region_id, customer_id). Rejected Option A (hash-based — breaks region-locality for Batavia Digital). Rollback trigger: p99 > 340ms for 5+ consecutive minutes. Security finding SEC-0042: keymaster-rotate stores key material in /tmp/keymaster-staging/ for 3 seconds. Auditor: Rachel Chen, Vanguard Security. Remediation: tmpfs mode 0700. Deadline: April 12th.
|
|
47
|
+
|
|
48
|
+
Search for "startEndTrim" across all .mjs files.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Messages 4-6 (WARM tier)
|
|
52
|
+
|
|
53
|
+
**Message 4:**
|
|
54
|
+
```
|
|
55
|
+
In /tmp/cg-quick-test.js, change "input.price * input.quantity" to "Math.round((input.price ?? 0) * (input.quantity ?? 1) * 100) / 100"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Message 5:**
|
|
59
|
+
```
|
|
60
|
+
In /tmp/cg-quick-test.js, change "total * 0.1" to "Math.ceil(total * 0.13)" — tax is 13%, always round up.
|
|
61
|
+
|
|
62
|
+
Read lib/extract-helpers.mjs — what does shouldSkipUserMessage check for?
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Message 6:**
|
|
66
|
+
```
|
|
67
|
+
In /tmp/cg-quick-test.js, change "return { total, tax, final: total + tax }" to "return { total, tax, final: total + tax, currency: 'IDR' }" — Batavia Digital uses Indonesian Rupiah.
|
|
68
|
+
|
|
69
|
+
Read lib/statusline.mjs — what color thresholds does it use?
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Messages 7-8 (WARM, approaching HOT)
|
|
73
|
+
|
|
74
|
+
**Message 7:**
|
|
75
|
+
```
|
|
76
|
+
Diana postponed the InventorySync split to September 12th (was August 22nd) — Q3 capacity work. On-call rotation: Week 1 Diana, Week 2 Tomás, Week 3 Priya, Week 4 me. PagerDuty: "zephyr9-critical", escalation after 5 min to #zephyr9-war-room.
|
|
77
|
+
|
|
78
|
+
Read lib/checkpoint.mjs — briefly explain performCompaction.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Message 8:**
|
|
82
|
+
```
|
|
83
|
+
In /tmp/cg-quick-test.js, rename "processOrder" to "calculateOrderTotal".
|
|
84
|
+
|
|
85
|
+
Run: git log --oneline -5
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Messages 9-10 (HOT tier)
|
|
89
|
+
|
|
90
|
+
**Message 9:**
|
|
91
|
+
```
|
|
92
|
+
Batavia Digital escalated ZEP-4471 to P1. SLA breach in 48 hours. Tomás pushing hotfix March 29th. Rollback image: zephyr9/order-mesh:v3.8.2-stable. Dashboard: grafana.internal/d/ordermesh-fds. Deploy cadence: Thursdays 16:00 UTC, script: scripts/deploy-prod.sh, requires DEPLOY_TOKEN.
|
|
93
|
+
|
|
94
|
+
Read lib/estimate.mjs — what does estimateSavings return?
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Message 10:**
|
|
98
|
+
```
|
|
99
|
+
Incident INC-2891: $184,000 revenue impact, 2,341 failed transactions. Post-mortem owner: Priya Ramanathan. Action items: (1) circuit breaker v2 by April 5th, (2) Meridian Pay health check endpoint, (3) runbook for manual CB override.
|
|
100
|
+
|
|
101
|
+
Confirm all fictional data.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Phase 2 — Compact and restore
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
/cg:stats
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Note usage. Then:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
/cg:compact
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Note Before/After. Then:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
/resume cg
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Critical check:** Claude Code should load the synthetic session containing the checkpoint as a real user message. If `/resume cg` doesn't find the session, check `~/.claude/logs/cg.log` for synthetic-session errors.
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
/cg:stats
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Phase 3 — Verify (single message)
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
Answer from your restored context. Do NOT re-read source files:
|
|
136
|
+
|
|
137
|
+
COLD TIER:
|
|
138
|
+
1. PostgreSQL port? Who chose Caddy and when?
|
|
139
|
+
2. ZEP-4471: item threshold, root cause file/line, fix PR author?
|
|
140
|
+
3. Sharding: which option chosen, which rejected and why, rollback trigger?
|
|
141
|
+
4. SEC-0042: what was flagged, auditor name, remediation, deadline?
|
|
142
|
+
|
|
143
|
+
WARM TIER:
|
|
144
|
+
5. Revised InventorySync date and reason?
|
|
145
|
+
6. On-call rotation order and escalation channel?
|
|
146
|
+
7. What currency did we add via edit? (Tests edit preservation)
|
|
147
|
+
|
|
148
|
+
HOT TIER:
|
|
149
|
+
8. ZEP-4471 escalation: priority, SLA, rollback image, dashboard URL?
|
|
150
|
+
9. INC-2891: revenue impact, failed txns, post-mortem owner, all 3 action items?
|
|
151
|
+
10. Deploy cadence, script, required env var?
|
|
152
|
+
|
|
153
|
+
TOOL WORK:
|
|
154
|
+
11. All edits to /tmp/cg-quick-test.js — describe every change.
|
|
155
|
+
12. Can you show raw file contents from lib/trim.mjs? (Should be stripped)
|
|
156
|
+
13. Can you show raw grep results from earlier? (Should be stripped)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Expected answers
|
|
162
|
+
|
|
163
|
+
| # | Expected | Pts | Tier |
|
|
164
|
+
|---|----------|-----|------|
|
|
165
|
+
| 1 | Port 5433, Diana Kowalski, January 14th 2026 | 3 | Cold |
|
|
166
|
+
| 2 | >2,847 items, splitter.go line 389, Tomás Herrera PR #1847 | 3 | Cold |
|
|
167
|
+
| 3 | Option B (compound region_id+customer_id), rejected A (hash-based, breaks region-locality), p99 > 340ms 5+ min | 3 | Cold |
|
|
168
|
+
| 4 | /tmp key material 3s, Rachel Chen (Vanguard Security), tmpfs 0700, April 12th | 3 | Cold |
|
|
169
|
+
| 5 | September 12th (was August 22nd), Q3 capacity priority | 2 | Warm |
|
|
170
|
+
| 6 | Diana/Tomás/Priya/user, #zephyr9-war-room after 5 min | 2 | Warm |
|
|
171
|
+
| 7 | IDR (Indonesian Rupiah) | 1 | Warm |
|
|
172
|
+
| 8 | P1, 48hr SLA, zephyr9/order-mesh:v3.8.2-stable, grafana.internal/d/ordermesh-fds | 3 | Hot |
|
|
173
|
+
| 9 | $184,000, 2,341 txns, Priya. CB v2 Apr 5, health check, runbook | 3 | Hot |
|
|
174
|
+
| 10 | Thursdays 16:00 UTC, scripts/deploy-prod.sh, DEPLOY_TOKEN | 2 | Hot |
|
|
175
|
+
| 11 | price rounding+null-safety, tax 10→13%+Math.ceil, currency IDR, rename to calculateOrderTotal | 3 | Mixed |
|
|
176
|
+
| 12 | Stripped — file reads removed as re-obtainable noise | 1 | — |
|
|
177
|
+
| 13 | Stripped — grep results removed | 1 | — |
|
|
178
|
+
|
|
179
|
+
**Total: 30 points. Target: 25+.** Below 20 indicates a regression.
|
|
180
|
+
|
|
181
|
+
### What this covers
|
|
182
|
+
|
|
183
|
+
| Feature | Covered by |
|
|
184
|
+
|---------|-----------|
|
|
185
|
+
| Cold-tier fact survival | Q1-4 |
|
|
186
|
+
| Warm-tier fact survival | Q5-7 |
|
|
187
|
+
| Hot-tier full fidelity | Q8-10 |
|
|
188
|
+
| Edit preservation | Q7, Q11 |
|
|
189
|
+
| Noise stripping | Q12-13 |
|
|
190
|
+
| /resume cg restore | Phase 2 critical check |
|
|
191
|
+
| Estimation accuracy | Phase 2 stats comparison |
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { formatCompactionStats } from "../lib/stats.mjs";
|
|
4
|
+
|
|
5
|
+
describe("formatCompactionStats", () => {
|
|
6
|
+
it("computes correct stats for normal case", () => {
|
|
7
|
+
// 400 bytes / 4 = 100 estimated post-tokens
|
|
8
|
+
const content = "x".repeat(400);
|
|
9
|
+
const { stats } = formatCompactionStats(1000, 10000, content);
|
|
10
|
+
|
|
11
|
+
assert.equal(stats.preTokens, 1000);
|
|
12
|
+
assert.equal(stats.postTokens, 100);
|
|
13
|
+
assert.equal(stats.maxTokens, 10000);
|
|
14
|
+
assert.equal(stats.saved, 900);
|
|
15
|
+
assert.equal(stats.savedPct, 90.0);
|
|
16
|
+
assert.equal(stats.prePct, 10.0);
|
|
17
|
+
assert.equal(stats.postPct, 1.0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("clamps saved to 0 when post > pre", () => {
|
|
21
|
+
// postTokens will be larger than preTokens
|
|
22
|
+
const content = "x".repeat(2000); // 500 estimated tokens
|
|
23
|
+
const { stats } = formatCompactionStats(100, 10000, content);
|
|
24
|
+
|
|
25
|
+
assert.equal(stats.saved, 0);
|
|
26
|
+
assert.equal(stats.savedPct, 0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("handles preTokens = 0 gracefully", () => {
|
|
30
|
+
const content = "x".repeat(400);
|
|
31
|
+
const { stats, block } = formatCompactionStats(0, 10000, content);
|
|
32
|
+
|
|
33
|
+
assert.equal(stats.saved, 0);
|
|
34
|
+
assert.equal(stats.prePct, 0);
|
|
35
|
+
assert.ok(block.includes("unknown (token data unavailable)"));
|
|
36
|
+
assert.ok(block.includes("Saved: unknown"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("box does not contain apply instructions (skill adds those)", () => {
|
|
40
|
+
const { block } = formatCompactionStats(1000, 10000, "x".repeat(100));
|
|
41
|
+
assert.ok(!block.includes("/resume"));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("handles maxTokens = 0 without crashing", () => {
|
|
45
|
+
const { stats } = formatCompactionStats(1000, 0, "x".repeat(100));
|
|
46
|
+
assert.equal(stats.postPct, 0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("block contains the box drawing characters", () => {
|
|
50
|
+
const { block } = formatCompactionStats(1000, 10000, "x".repeat(100));
|
|
51
|
+
assert.ok(block.startsWith("┌"));
|
|
52
|
+
assert.ok(block.includes("└"));
|
|
53
|
+
assert.ok(block.includes("Compaction Stats"));
|
|
54
|
+
assert.ok(block.includes("Compaction Stats"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("includes payload line when prePayloadBytes provided", () => {
|
|
58
|
+
const content = "x".repeat(400);
|
|
59
|
+
const prePayloadBytes = 15 * 1024 * 1024; // 15MB
|
|
60
|
+
const { stats, block } = formatCompactionStats(1000, 10000, content, {
|
|
61
|
+
prePayloadBytes,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.ok(block.includes("Session:"));
|
|
65
|
+
assert.ok(block.includes("15.0MB"));
|
|
66
|
+
assert.equal(stats.prePayloadBytes, prePayloadBytes);
|
|
67
|
+
assert.equal(stats.postPayloadBytes, 400); // content byte length
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("omits payload line when prePayloadBytes is 0", () => {
|
|
71
|
+
const { block } = formatCompactionStats(1000, 10000, "x".repeat(100));
|
|
72
|
+
assert.ok(!block.includes("Session:"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("omits session line when both prePayloadBytes and overhead are 0", () => {
|
|
76
|
+
const { block } = formatCompactionStats(1000, 10000, "x".repeat(100));
|
|
77
|
+
assert.ok(!block.includes("Session:"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("stats include payload byte values", () => {
|
|
81
|
+
const content = "x".repeat(800);
|
|
82
|
+
const { stats } = formatCompactionStats(1000, 10000, content, {
|
|
83
|
+
prePayloadBytes: 5000000,
|
|
84
|
+
});
|
|
85
|
+
assert.equal(stats.prePayloadBytes, 5000000);
|
|
86
|
+
assert.equal(stats.postPayloadBytes, 800);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
|
|
8
|
+
const scriptPath = path.resolve(import.meta.dirname, "../lib/statusline.mjs");
|
|
9
|
+
|
|
10
|
+
function runStatusline(input, env) {
|
|
11
|
+
const result = spawnSync("node", [scriptPath], {
|
|
12
|
+
input: typeof input === "string" ? input : JSON.stringify(input),
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
CLAUDE_PLUGIN_DATA: "/tmp/cg-statusline-test-nonexistent",
|
|
17
|
+
...env,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
return result.stdout;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runWithThreshold(input, threshold) {
|
|
24
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cg-sl-"));
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
path.join(tmpDir, "config.json"),
|
|
27
|
+
JSON.stringify({ threshold }),
|
|
28
|
+
);
|
|
29
|
+
const out = runStatusline(input, { CLAUDE_PLUGIN_DATA: tmpDir });
|
|
30
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runWithPayload(input, payloadBytes) {
|
|
35
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cg-sl-"));
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(tmpDir, "config.json"),
|
|
38
|
+
JSON.stringify({ threshold: 0.35 }),
|
|
39
|
+
);
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(tmpDir, "state-test.json"),
|
|
42
|
+
JSON.stringify({ payload_bytes: payloadBytes, ts: Date.now() }),
|
|
43
|
+
);
|
|
44
|
+
const out = runStatusline(input, { CLAUDE_PLUGIN_DATA: tmpDir });
|
|
45
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Strip ANSI escape codes for content-only assertions
|
|
50
|
+
function strip(str) {
|
|
51
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape code stripping
|
|
52
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("statusline render", () => {
|
|
56
|
+
it("empty object shows '--'", () => {
|
|
57
|
+
const out = strip(runStatusline({}));
|
|
58
|
+
assert.ok(out.includes("Context usage: --"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("empty context_window shows '--'", () => {
|
|
62
|
+
const out = strip(runStatusline({ context_window: {} }));
|
|
63
|
+
assert.ok(out.includes("Context usage: --"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("null used_percentage shows '--'", () => {
|
|
67
|
+
const out = strip(
|
|
68
|
+
runStatusline({ context_window: { used_percentage: null } }),
|
|
69
|
+
);
|
|
70
|
+
assert.ok(out.includes("Context usage: --"));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("0% is valid, not 'Context usage: --'", () => {
|
|
74
|
+
const out = strip(
|
|
75
|
+
runStatusline({ context_window: { used_percentage: 0 } }),
|
|
76
|
+
);
|
|
77
|
+
assert.ok(out.includes("Context usage:"));
|
|
78
|
+
assert.ok(out.includes("0%"));
|
|
79
|
+
assert.ok(!out.includes("Context usage: --"));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("3% shows percentage and /cg:stats hint", () => {
|
|
83
|
+
const out = strip(
|
|
84
|
+
runStatusline({ context_window: { used_percentage: 3 } }),
|
|
85
|
+
);
|
|
86
|
+
assert.ok(out.includes("Context usage:"));
|
|
87
|
+
assert.ok(out.includes("3%"));
|
|
88
|
+
assert.ok(out.includes("/cg:stats for more"));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("invalid JSON input falls back to 'Context: --'", () => {
|
|
92
|
+
const out = runStatusline("not valid json {{{");
|
|
93
|
+
assert.equal(out, "Context: --");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("output contains /cg:stats for more", () => {
|
|
97
|
+
const out = strip(
|
|
98
|
+
runStatusline({ context_window: { used_percentage: 10 } }),
|
|
99
|
+
);
|
|
100
|
+
assert.ok(out.includes("/cg:stats for more"));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("threshold-relative colors", () => {
|
|
105
|
+
it("well below threshold: dim label, green number", () => {
|
|
106
|
+
const raw = runStatusline({ context_window: { used_percentage: 10 } });
|
|
107
|
+
assert.ok(raw.includes("\x1b[2mContext usage:\x1b[0m")); // dim label
|
|
108
|
+
assert.ok(raw.includes("\x1b[32m10%")); // green number
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("approaching threshold: dim label, yellow number", () => {
|
|
112
|
+
const raw = runStatusline({ context_window: { used_percentage: 30 } });
|
|
113
|
+
assert.ok(raw.includes("\x1b[2mContext usage:\x1b[0m")); // dim label
|
|
114
|
+
assert.ok(raw.includes("\x1b[33m30%")); // yellow number
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("at threshold: bold red on entire label+number", () => {
|
|
118
|
+
const raw = runStatusline({ context_window: { used_percentage: 40 } });
|
|
119
|
+
assert.ok(raw.includes("\x1b[1;31mContext usage: 40%")); // bold red full
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("colors adjust with custom threshold", () => {
|
|
123
|
+
const greenRaw = runWithThreshold(
|
|
124
|
+
{ context_window: { used_percentage: 20 } },
|
|
125
|
+
0.7,
|
|
126
|
+
);
|
|
127
|
+
assert.ok(greenRaw.includes("\x1b[32m20%"));
|
|
128
|
+
|
|
129
|
+
const yellowRaw = runWithThreshold(
|
|
130
|
+
{ context_window: { used_percentage: 55 } },
|
|
131
|
+
0.7,
|
|
132
|
+
);
|
|
133
|
+
assert.ok(yellowRaw.includes("\x1b[33m55%"));
|
|
134
|
+
|
|
135
|
+
const redRaw = runWithThreshold(
|
|
136
|
+
{ context_window: { used_percentage: 75 } },
|
|
137
|
+
0.7,
|
|
138
|
+
);
|
|
139
|
+
assert.ok(redRaw.includes("\x1b[1;31mContext usage: 75%"));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("session size display", () => {
|
|
144
|
+
it("shows session size when state file has payload_bytes", () => {
|
|
145
|
+
const out = strip(
|
|
146
|
+
runWithPayload(
|
|
147
|
+
{ context_window: { used_percentage: 10 } },
|
|
148
|
+
5 * 1024 * 1024,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
assert.ok(out.includes("Session size:"));
|
|
152
|
+
assert.ok(out.includes("5.0/20MB"));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("under 10MB: dim label, green number, dim /20MB", () => {
|
|
156
|
+
const raw = runWithPayload(
|
|
157
|
+
{ context_window: { used_percentage: 10 } },
|
|
158
|
+
5 * 1024 * 1024,
|
|
159
|
+
);
|
|
160
|
+
assert.ok(raw.includes("\x1b[2mSession size:\x1b[0m")); // dim label
|
|
161
|
+
assert.ok(raw.includes("\x1b[32m")); // green number
|
|
162
|
+
assert.ok(raw.includes("\x1b[2m/20MB")); // dim /20MB
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("10-15MB: dim label, yellow number", () => {
|
|
166
|
+
const raw = runWithPayload(
|
|
167
|
+
{ context_window: { used_percentage: 10 } },
|
|
168
|
+
12 * 1024 * 1024,
|
|
169
|
+
);
|
|
170
|
+
assert.ok(raw.includes("\x1b[2mSession size:\x1b[0m")); // dim label
|
|
171
|
+
assert.ok(raw.includes("\x1b[33m")); // yellow
|
|
172
|
+
assert.ok(strip(raw).includes("/20MB"));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("15MB+: bold red on entire label+number", () => {
|
|
176
|
+
const raw = runWithPayload(
|
|
177
|
+
{ context_window: { used_percentage: 10 } },
|
|
178
|
+
17 * 1024 * 1024,
|
|
179
|
+
);
|
|
180
|
+
assert.ok(raw.includes("\x1b[1;31mSession size:")); // bold red full
|
|
181
|
+
assert.ok(strip(raw).includes("17.0/20MB"));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("shows -- when state file missing", () => {
|
|
185
|
+
const out = strip(
|
|
186
|
+
runStatusline({ context_window: { used_percentage: 10 } }),
|
|
187
|
+
);
|
|
188
|
+
assert.ok(!out.includes("Session size:"));
|
|
189
|
+
assert.ok(out.includes("--"));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("shows -- when payload_bytes is 0", () => {
|
|
193
|
+
const out = strip(
|
|
194
|
+
runWithPayload({ context_window: { used_percentage: 10 } }, 0),
|
|
195
|
+
);
|
|
196
|
+
assert.ok(!out.includes("Session size:"));
|
|
197
|
+
assert.ok(out.includes("--"));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("alert state messaging", () => {
|
|
202
|
+
it("at threshold shows actionable compaction message", () => {
|
|
203
|
+
const out = strip(
|
|
204
|
+
runStatusline({ context_window: { used_percentage: 40 } }),
|
|
205
|
+
);
|
|
206
|
+
assert.ok(out.includes("compaction recommended"));
|
|
207
|
+
assert.ok(out.includes("/cg:compact"));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("at threshold uses bold red for alert text", () => {
|
|
211
|
+
const raw = runStatusline({ context_window: { used_percentage: 40 } });
|
|
212
|
+
assert.ok(raw.includes("\x1b[1;31mcompaction recommended"));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("below threshold shows /cg:stats hint instead of compaction message", () => {
|
|
216
|
+
const out = strip(
|
|
217
|
+
runStatusline({ context_window: { used_percentage: 10 } }),
|
|
218
|
+
);
|
|
219
|
+
assert.ok(out.includes("/cg:stats for more"));
|
|
220
|
+
assert.ok(!out.includes("compaction recommended"));
|
|
221
|
+
});
|
|
222
|
+
});
|