@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,86 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { contentBytesOf, flattenContent } from "../lib/content.mjs";
|
|
4
|
+
|
|
5
|
+
describe("flattenContent", () => {
|
|
6
|
+
it("returns empty string for null/undefined", () => {
|
|
7
|
+
assert.equal(flattenContent(null), "");
|
|
8
|
+
assert.equal(flattenContent(undefined), "");
|
|
9
|
+
assert.equal(flattenContent(""), "");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns string content as-is", () => {
|
|
13
|
+
assert.equal(flattenContent("hello world"), "hello world");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("extracts text blocks from array content", () => {
|
|
17
|
+
const content = [
|
|
18
|
+
{ type: "text", text: "first" },
|
|
19
|
+
{ type: "tool_use", id: "t1", name: "Read", input: {} },
|
|
20
|
+
{ type: "text", text: "second" },
|
|
21
|
+
];
|
|
22
|
+
assert.equal(flattenContent(content), "first\nsecond");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns empty string for array with no text blocks", () => {
|
|
26
|
+
const content = [
|
|
27
|
+
{ type: "tool_use", id: "t1", name: "Read", input: {} },
|
|
28
|
+
{ type: "tool_result", tool_use_id: "t1", content: "result" },
|
|
29
|
+
];
|
|
30
|
+
assert.equal(flattenContent(content), "");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns empty string for non-string non-array types", () => {
|
|
34
|
+
assert.equal(flattenContent(42), "");
|
|
35
|
+
assert.equal(flattenContent({}), "");
|
|
36
|
+
assert.equal(flattenContent(true), "");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles single text block in array", () => {
|
|
40
|
+
assert.equal(flattenContent([{ type: "text", text: "only" }]), "only");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("contentBytesOf", () => {
|
|
45
|
+
it("returns 0 for null/undefined", () => {
|
|
46
|
+
assert.equal(contentBytesOf(null), 0);
|
|
47
|
+
assert.equal(contentBytesOf(undefined), 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("counts bytes for string content", () => {
|
|
51
|
+
assert.equal(contentBytesOf("hello"), 5);
|
|
52
|
+
// Multi-byte: é is 2 bytes in UTF-8
|
|
53
|
+
assert.equal(contentBytesOf("café"), 5);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("counts text bytes from array blocks", () => {
|
|
57
|
+
const content = [
|
|
58
|
+
{ type: "text", text: "hello" },
|
|
59
|
+
{ type: "text", text: "world" },
|
|
60
|
+
];
|
|
61
|
+
assert.equal(contentBytesOf(content), 10);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("counts tool input bytes", () => {
|
|
65
|
+
const content = [{ type: "tool_use", input: { key: "val" } }];
|
|
66
|
+
const expected = Buffer.byteLength(JSON.stringify({ key: "val" }), "utf8");
|
|
67
|
+
assert.equal(contentBytesOf(content), expected);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("handles nested content recursively", () => {
|
|
71
|
+
const content = [
|
|
72
|
+
{ type: "tool_result", content: [{ type: "text", text: "nested" }] },
|
|
73
|
+
];
|
|
74
|
+
assert.equal(contentBytesOf(content), 6);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns 0 for non-string non-array", () => {
|
|
78
|
+
assert.equal(contentBytesOf(42), 0);
|
|
79
|
+
assert.equal(contentBytesOf({}), 0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("skips blocks without text or input", () => {
|
|
83
|
+
const content = [{ type: "thinking", thinking: "hmm" }];
|
|
84
|
+
assert.equal(contentBytesOf(content), 0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
|
|
8
|
+
// diagnostics.mjs is a CLI script (not a module with exports).
|
|
9
|
+
// It reads env/args and writes JSON to stdout. We test it by spawning.
|
|
10
|
+
|
|
11
|
+
const DIAG_PATH = path.resolve("lib/diagnostics.mjs");
|
|
12
|
+
|
|
13
|
+
let tmpDir;
|
|
14
|
+
let dataDir;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cg-diag-"));
|
|
18
|
+
dataDir = path.join(tmpDir, "data");
|
|
19
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function runDiag(args = [], env = {}) {
|
|
27
|
+
const stdout = execFileSync("node", [DIAG_PATH, ...args], {
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
timeout: 5000,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
CLAUDE_PLUGIN_DATA: dataDir,
|
|
33
|
+
HOME: os.homedir(),
|
|
34
|
+
...env,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return JSON.parse(stdout);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ===========================================================================
|
|
41
|
+
// Basic output structure
|
|
42
|
+
// ===========================================================================
|
|
43
|
+
|
|
44
|
+
describe("diagnostics output", () => {
|
|
45
|
+
it("returns valid JSON with checks array", () => {
|
|
46
|
+
const result = runDiag(["test-session"]);
|
|
47
|
+
assert.ok(Array.isArray(result.checks));
|
|
48
|
+
assert.ok(result.checks.length > 0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("each check has name, ok, and detail fields", () => {
|
|
52
|
+
const result = runDiag(["test-session"]);
|
|
53
|
+
for (const check of result.checks) {
|
|
54
|
+
assert.ok(
|
|
55
|
+
typeof check.name === "string",
|
|
56
|
+
`name should be string: ${JSON.stringify(check)}`,
|
|
57
|
+
);
|
|
58
|
+
assert.ok(
|
|
59
|
+
typeof check.ok === "boolean",
|
|
60
|
+
`ok should be boolean: ${JSON.stringify(check)}`,
|
|
61
|
+
);
|
|
62
|
+
assert.ok(
|
|
63
|
+
typeof check.detail === "string",
|
|
64
|
+
`detail should be string: ${JSON.stringify(check)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
// data_dir check
|
|
72
|
+
// ===========================================================================
|
|
73
|
+
|
|
74
|
+
describe("data_dir check", () => {
|
|
75
|
+
it("passes when data dir is writable", () => {
|
|
76
|
+
const result = runDiag(["test-session"]);
|
|
77
|
+
const check = result.checks.find((c) => c.name === "data_dir");
|
|
78
|
+
assert.ok(check);
|
|
79
|
+
assert.equal(check.ok, true);
|
|
80
|
+
// detail contains whichever data dir the diagnostics resolved to
|
|
81
|
+
assert.ok(typeof check.detail === "string" && check.detail.length > 0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ===========================================================================
|
|
86
|
+
// state_file check
|
|
87
|
+
// ===========================================================================
|
|
88
|
+
|
|
89
|
+
describe("state_file check", () => {
|
|
90
|
+
it("fails when state file is missing", () => {
|
|
91
|
+
const result = runDiag(["nonexistent-session"]);
|
|
92
|
+
const check = result.checks.find((c) => c.name === "state_file");
|
|
93
|
+
assert.ok(check);
|
|
94
|
+
assert.equal(check.ok, false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("passes when state file exists", () => {
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
path.join(dataDir, "state-my-session.json"),
|
|
100
|
+
JSON.stringify({ transcript_path: "/tmp/fake.jsonl" }),
|
|
101
|
+
);
|
|
102
|
+
const result = runDiag(["my-session"]);
|
|
103
|
+
const check = result.checks.find((c) => c.name === "state_file");
|
|
104
|
+
assert.ok(check);
|
|
105
|
+
assert.equal(check.ok, true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ===========================================================================
|
|
110
|
+
// transcript check
|
|
111
|
+
// ===========================================================================
|
|
112
|
+
|
|
113
|
+
describe("transcript check", () => {
|
|
114
|
+
it("fails when no state file exists (skipped)", () => {
|
|
115
|
+
const result = runDiag(["no-state"]);
|
|
116
|
+
const check = result.checks.find((c) => c.name === "transcript");
|
|
117
|
+
assert.ok(check);
|
|
118
|
+
assert.equal(check.ok, false);
|
|
119
|
+
assert.ok(
|
|
120
|
+
check.detail.includes("no state file") ||
|
|
121
|
+
check.detail.includes("Skipped"),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("passes when transcript file exists", () => {
|
|
126
|
+
const transcriptPath = path.join(tmpDir, "transcript.jsonl");
|
|
127
|
+
fs.writeFileSync(transcriptPath, "{}");
|
|
128
|
+
fs.writeFileSync(
|
|
129
|
+
path.join(dataDir, "state-tx-session.json"),
|
|
130
|
+
JSON.stringify({ transcript_path: transcriptPath }),
|
|
131
|
+
);
|
|
132
|
+
const result = runDiag(["tx-session"]);
|
|
133
|
+
const check = result.checks.find((c) => c.name === "transcript");
|
|
134
|
+
assert.ok(check);
|
|
135
|
+
assert.equal(check.ok, true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("fails when transcript path in state does not exist on disk", () => {
|
|
139
|
+
fs.writeFileSync(
|
|
140
|
+
path.join(dataDir, "state-bad-tx.json"),
|
|
141
|
+
JSON.stringify({ transcript_path: "/nonexistent/path.jsonl" }),
|
|
142
|
+
);
|
|
143
|
+
const result = runDiag(["bad-tx"]);
|
|
144
|
+
const check = result.checks.find((c) => c.name === "transcript");
|
|
145
|
+
assert.ok(check);
|
|
146
|
+
assert.equal(check.ok, false);
|
|
147
|
+
assert.ok(check.detail.includes("Not found"));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ===========================================================================
|
|
152
|
+
// plugin_root check
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
|
|
155
|
+
describe("plugin_root check", () => {
|
|
156
|
+
it("passes (inferred from diagnostics.mjs location)", () => {
|
|
157
|
+
const result = runDiag(["test-session"]);
|
|
158
|
+
const check = result.checks.find((c) => c.name === "plugin_root");
|
|
159
|
+
assert.ok(check);
|
|
160
|
+
assert.equal(check.ok, true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ===========================================================================
|
|
165
|
+
// hooks check
|
|
166
|
+
// ===========================================================================
|
|
167
|
+
|
|
168
|
+
describe("hooks check", () => {
|
|
169
|
+
it("passes when all 4 hook files exist", () => {
|
|
170
|
+
const result = runDiag(["test-session"]);
|
|
171
|
+
const check = result.checks.find((c) => c.name === "hooks");
|
|
172
|
+
assert.ok(check);
|
|
173
|
+
assert.equal(check.ok, true);
|
|
174
|
+
assert.ok(check.detail.includes("4 hook files"));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ===========================================================================
|
|
179
|
+
// Always exits 0
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
|
|
182
|
+
describe("exit behaviour", () => {
|
|
183
|
+
it("always exits 0 even with missing session", () => {
|
|
184
|
+
// If it threw, execFileSync would throw too
|
|
185
|
+
const result = runDiag(["completely-bogus-session-id"]);
|
|
186
|
+
assert.ok(Array.isArray(result.checks));
|
|
187
|
+
});
|
|
188
|
+
});
|