@unerr-ai/unerr 0.1.6 → 0.1.7
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/README.md +70 -194
- package/dist/cli.js +39149 -36991
- package/package.json +9 -2
- package/dist/__tests__/architecture-guard.test.js +0 -122
- package/dist/__tests__/arg-validator.test.js +0 -205
- package/dist/__tests__/ast-extractor.test.js +0 -203
- package/dist/__tests__/auto-bootstrap.test.js +0 -280
- package/dist/__tests__/background-indexer.test.js +0 -228
- package/dist/__tests__/blast-radius-engine.test.js +0 -200
- package/dist/__tests__/bridge-isolation.test.js +0 -37
- package/dist/__tests__/budget-enforcer.test.js +0 -53
- package/dist/__tests__/cfg-test-detection-perf.test.js +0 -82
- package/dist/__tests__/change-narrative.test.js +0 -190
- package/dist/__tests__/check-commit.test.js +0 -258
- package/dist/__tests__/checksum.test.js +0 -34
- package/dist/__tests__/commit-watcher.test.js +0 -154
- package/dist/__tests__/community-detection.test.js +0 -179
- package/dist/__tests__/community-tools.test.js +0 -299
- package/dist/__tests__/components.test.js +0 -449
- package/dist/__tests__/compression-log.test.js +0 -174
- package/dist/__tests__/compression-quality-monitor.test.js +0 -40
- package/dist/__tests__/config-healer.test.js +0 -165
- package/dist/__tests__/context-ledger.test.js +0 -58
- package/dist/__tests__/convention-detector.test.js +0 -99
- package/dist/__tests__/convention-learner.test.js +0 -86
- package/dist/__tests__/correction-detector.test.js +0 -330
- package/dist/__tests__/daemon-autostart-install.test.js +0 -283
- package/dist/__tests__/daemon-bridge.test.js +0 -222
- package/dist/__tests__/daemon-dashboard.test.js +0 -202
- package/dist/__tests__/daemon-registry.test.js +0 -240
- package/dist/__tests__/daemon-supervisor.test.js +0 -318
- package/dist/__tests__/daemon-version-check.test.js +0 -275
- package/dist/__tests__/decision-point-detector.test.js +0 -98
- package/dist/__tests__/deep-link.test.js +0 -143
- package/dist/__tests__/disallowed-tools.test.js +0 -115
- package/dist/__tests__/drift-tracker.test.js +0 -582
- package/dist/__tests__/durability-scorer.test.js +0 -152
- package/dist/__tests__/efficiency-tracker.test.js +0 -65
- package/dist/__tests__/enrich.test.js +0 -144
- package/dist/__tests__/entity-rewind.test.js +0 -248
- package/dist/__tests__/ephemeral.test.js +0 -111
- package/dist/__tests__/exploration-cost.test.js +0 -93
- package/dist/__tests__/fact-generator.test.js +0 -197
- package/dist/__tests__/file-l0-graph.test.js +0 -244
- package/dist/__tests__/file-logger.test.js +0 -82
- package/dist/__tests__/file-outline.test.js +0 -141
- package/dist/__tests__/file-read-protocol.test.js +0 -188
- package/dist/__tests__/format-encoder.test.js +0 -233
- package/dist/__tests__/git-attribution.test.js +0 -259
- package/dist/__tests__/graph-temporal-joiner.test.js +0 -219
- package/dist/__tests__/health-grade-enhanced.test.js +0 -138
- package/dist/__tests__/health-map-data.test.js +0 -173
- package/dist/__tests__/helpers/mcp-harness.js +0 -45
- package/dist/__tests__/helpers/mcp-harness.test.js +0 -68
- package/dist/__tests__/hook-dedup.test.js +0 -112
- package/dist/__tests__/hook-runner.test.js +0 -253
- package/dist/__tests__/indexer-cfg.test.js +0 -185
- package/dist/__tests__/indexer-cross-file.test.js +0 -172
- package/dist/__tests__/indexer-extraction.test.js +0 -245
- package/dist/__tests__/indexer-incremental.test.js +0 -232
- package/dist/__tests__/indexer-language-expansion.test.js +0 -165
- package/dist/__tests__/init-push.test.js +0 -131
- package/dist/__tests__/instruction-writer.test.js +0 -179
- package/dist/__tests__/intelligence-integration.test.js +0 -217
- package/dist/__tests__/intent-correlator.test.js +0 -175
- package/dist/__tests__/intent-detector.test.js +0 -235
- package/dist/__tests__/intent-encoder.test.js +0 -167
- package/dist/__tests__/java-build-tool-detection.test.js +0 -174
- package/dist/__tests__/layer3-sprint-q.test.js +0 -160
- package/dist/__tests__/layer3-sprint-r.test.js +0 -91
- package/dist/__tests__/layer3-sprint-s.test.js +0 -183
- package/dist/__tests__/layer3-sprint-t.test.js +0 -201
- package/dist/__tests__/layer3-sprint-u.test.js +0 -174
- package/dist/__tests__/layer4-sprint-ba2.test.js +0 -354
- package/dist/__tests__/layer4-sprint-ba4.test.js +0 -84
- package/dist/__tests__/layer4-sprint-vs.test.js +0 -105
- package/dist/__tests__/ledger-chains.test.js +0 -162
- package/dist/__tests__/lifecycle-machine.test.js +0 -226
- package/dist/__tests__/local-chat-provider.test.js +0 -170
- package/dist/__tests__/local-convention-detector.test.js +0 -308
- package/dist/__tests__/local-embeddings.test.js +0 -422
- package/dist/__tests__/local-graph.test.js +0 -540
- package/dist/__tests__/local-indexer.test.js +0 -228
- package/dist/__tests__/local-intelligence-l3.test.js +0 -332
- package/dist/__tests__/local-llm.test.js +0 -253
- package/dist/__tests__/local-mode-offline.test.js +0 -187
- package/dist/__tests__/local-mode-stats.test.js +0 -273
- package/dist/__tests__/local-mode-tui.test.js +0 -343
- package/dist/__tests__/local-parse.test.js +0 -199
- package/dist/__tests__/log-tailer.test.js +0 -208
- package/dist/__tests__/loop-breaker.test.js +0 -276
- package/dist/__tests__/loop-miner.test.js +0 -226
- package/dist/__tests__/mcp-config.test.js +0 -126
- package/dist/__tests__/mcp-content-json.test.js +0 -10
- package/dist/__tests__/mcp-envelope.test.js +0 -124
- package/dist/__tests__/metrics-store.test.js +0 -223
- package/dist/__tests__/native-watcher.test.js +0 -191
- package/dist/__tests__/navigation-hooks-agent-aware.test.js +0 -145
- package/dist/__tests__/negative-knowledge.test.js +0 -116
- package/dist/__tests__/network-boundary.test.js +0 -190
- package/dist/__tests__/network-firewall.test.js +0 -112
- package/dist/__tests__/nudge-invariants.test.js +0 -160
- package/dist/__tests__/nudge-v2.test.js +0 -225
- package/dist/__tests__/offline-rewind.test.js +0 -251
- package/dist/__tests__/open-threads.test.js +0 -89
- package/dist/__tests__/output-compressor.test.js +0 -93
- package/dist/__tests__/pending-violations.test.js +0 -112
- package/dist/__tests__/persistence-effectiveness.test.js +0 -143
- package/dist/__tests__/provider-factory.test.js +0 -42
- package/dist/__tests__/providers.test.js +0 -24
- package/dist/__tests__/proxy.test.js +0 -314
- package/dist/__tests__/query-router.test.js +0 -1018
- package/dist/__tests__/reasoning-quality-route.test.js +0 -138
- package/dist/__tests__/redactor.test.js +0 -120
- package/dist/__tests__/resource-monitor.test.js +0 -57
- package/dist/__tests__/response-envelope.test.js +0 -100
- package/dist/__tests__/risk-classifier.test.js +0 -101
- package/dist/__tests__/risk-signal-scope.test.js +0 -75
- package/dist/__tests__/rule-evaluator.test.js +0 -280
- package/dist/__tests__/scip-decoder.test.js +0 -49
- package/dist/__tests__/scip-downloader.test.js +0 -201
- package/dist/__tests__/scip-merger.test.js +0 -103
- package/dist/__tests__/search-index.test.js +0 -422
- package/dist/__tests__/semantic-enrichment.test.js +0 -360
- package/dist/__tests__/session-brief-builder.test.js +0 -187
- package/dist/__tests__/session-context.test.js +0 -221
- package/dist/__tests__/session-continuity.test.js +0 -144
- package/dist/__tests__/session-dedup.test.js +0 -74
- package/dist/__tests__/session-event-wiring.test.js +0 -206
- package/dist/__tests__/session-events.test.js +0 -149
- package/dist/__tests__/session-legend.test.js +0 -20
- package/dist/__tests__/session-persistence.test.js +0 -131
- package/dist/__tests__/session-resume-block.test.js +0 -107
- package/dist/__tests__/session-resume.test.js +0 -97
- package/dist/__tests__/session-summary-writer.test.js +0 -134
- package/dist/__tests__/shadow-ledger.test.js +0 -203
- package/dist/__tests__/shell-classifier.test.js +0 -151
- package/dist/__tests__/shell-compression-floor.test.js +0 -189
- package/dist/__tests__/shell-compression-v2.test.js +0 -339
- package/dist/__tests__/shell-compressor.test.js +0 -35
- package/dist/__tests__/shell-hooks.test.js +0 -128
- package/dist/__tests__/shell-strategies.test.js +0 -644
- package/dist/__tests__/shell-tee.test.js +0 -133
- package/dist/__tests__/signal-dedup.test.js +0 -158
- package/dist/__tests__/signal-reinforcer.test.js +0 -77
- package/dist/__tests__/signal-scorer.test.js +0 -251
- package/dist/__tests__/signal-show-store.test.js +0 -108
- package/dist/__tests__/smart-truncate.test.js +0 -215
- package/dist/__tests__/snapshot-v2.test.js +0 -113
- package/dist/__tests__/sprint-l1-local-mode.test.js +0 -130
- package/dist/__tests__/sprint-l10-boot.test.js +0 -220
- package/dist/__tests__/sprint-l9-offline-commands.test.js +0 -189
- package/dist/__tests__/sprint-q-persistent-context.test.js +0 -198
- package/dist/__tests__/sprint-s1-wiring.test.js +0 -215
- package/dist/__tests__/sprint-s2-wiring.test.js +0 -256
- package/dist/__tests__/sprint-s3-wiring.test.js +0 -195
- package/dist/__tests__/sprint-s4-wiring.test.js +0 -213
- package/dist/__tests__/sprint-s6-hooks.test.js +0 -222
- package/dist/__tests__/sprint-s7-persistent.test.js +0 -263
- package/dist/__tests__/sprint-s8-value.test.js +0 -167
- package/dist/__tests__/sprint-s9-behavioral.test.js +0 -179
- package/dist/__tests__/sprint3-intelligence.test.js +0 -297
- package/dist/__tests__/sprint5-mcp-server.test.js +0 -136
- package/dist/__tests__/startup-display.test.js +0 -302
- package/dist/__tests__/startup-log-file.test.js +0 -97
- package/dist/__tests__/stash-manager.test.js +0 -229
- package/dist/__tests__/state-detector.test.js +0 -92
- package/dist/__tests__/status-dashboard.test.js +0 -142
- package/dist/__tests__/temporal-facts.test.js +0 -292
- package/dist/__tests__/temporal-routes.test.js +0 -142
- package/dist/__tests__/test-detector.test.js +0 -174
- package/dist/__tests__/theme.test.js +0 -72
- package/dist/__tests__/timeline-agents.test.js +0 -122
- package/dist/__tests__/timeline-bootstrap.test.js +0 -176
- package/dist/__tests__/timeline-filters.test.js +0 -193
- package/dist/__tests__/timeline-markers.test.js +0 -151
- package/dist/__tests__/timeline-routes.test.js +0 -156
- package/dist/__tests__/timeline-store.test.js +0 -171
- package/dist/__tests__/token-counter.test.js +0 -86
- package/dist/__tests__/token-estimator.test.js +0 -96
- package/dist/__tests__/token-flow-api.test.js +0 -239
- package/dist/__tests__/token-flow-instrumentation.test.js +0 -437
- package/dist/__tests__/token-flow-persistence.test.js +0 -356
- package/dist/__tests__/token-flow-routes.test.js +0 -199
- package/dist/__tests__/token-flow.test.js +0 -695
- package/dist/__tests__/tool-clusters.test.js +0 -177
- package/dist/__tests__/transport-mux.test.js +0 -283
- package/dist/__tests__/turn-segmenter.test.js +0 -166
- package/dist/__tests__/uninstall.test.js +0 -141
- package/dist/__tests__/warm-start-policy.test.js +0 -271
- package/dist/__tests__/wire-cap-nudge.test.js +0 -77
- package/dist/__tests__/worker-pool.test.js +0 -101
- package/dist/ui/assets/index-7gl3mIuY.css +0 -1
- package/dist/ui/assets/index-CX4FCWGT.js +0 -10
- package/dist/ui/assets/rolldown-runtime-S-ySWqyJ.js +0 -1
- package/dist/ui/assets/vis-network-NIJHUFI3.js +0 -908
- package/dist/ui/fonts/jetbrains-mono-latin-400-normal.woff +0 -0
- package/dist/ui/icon-wordmark.png +0 -0
- package/dist/ui/icon-wordmark.svg +0 -30
- package/dist/ui/icon.png +0 -0
- package/dist/ui/icon.svg +0 -25
- package/dist/ui/index.html +0 -15
- package/dist/ui/prototype-sandbox/index.html +0 -257
- package/dist/ui/screenshots/activity.png +0 -0
- package/dist/ui/screenshots/code-base-intelligence.png +0 -0
- package/dist/ui/screenshots/dashboard.png +0 -0
- package/dist/ui/screenshots/project-memory.png +0 -0
- package/dist/ui/screenshots/reasoning-quality.png +0 -0
- package/dist/ui/screenshots/reasoning-session.png +0 -0
- package/dist/ui/screenshots/token-session.png +0 -0
- package/dist/ui/screenshots/token-trace-main.png +0 -0
- package/dist/ui/screenshots/token-turn.png +0 -0
- package/dist/ui/unerr-wordmark.png +0 -0
- package/dist/ui/unerr-wordmark.svg +0 -9
- package/dist/ui/unerr.png +0 -0
- package/dist/ui/unerr.svg +0 -25
- package/dist/ui/web-app-manifest-192x192.png +0 -0
- package/dist/ui/web-app-manifest-512x512.png +0 -0
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck — test file
|
|
2
|
-
/**
|
|
3
|
-
* HealthMapData tests — health score computation and tree building.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { HealthMapData } from "../intelligence/health-map-data.js";
|
|
7
|
-
function createMockGraph(entities = [], conventions = []) {
|
|
8
|
-
return {
|
|
9
|
-
getLocalProjectStats: vi.fn(() => Promise.resolve({ entity_count: entities.length, edge_count: 0 })),
|
|
10
|
-
getCriticalNodes: vi.fn(() => Promise.resolve(entities)),
|
|
11
|
-
getEntitiesByFile: vi.fn((filePath) => Promise.resolve(entities.filter((e) => e.file_path === filePath))),
|
|
12
|
-
getConventions: vi.fn(() => Promise.resolve(conventions)),
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
function createMockFactStore(projectFacts = [], fileFacts = []) {
|
|
16
|
-
return {
|
|
17
|
-
recallByScope: vi.fn(() => Promise.resolve(projectFacts)),
|
|
18
|
-
recallForFile: vi.fn(() => Promise.resolve(fileFacts)),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
const ENTITY_A = {
|
|
22
|
-
key: "src/a.ts::funcA",
|
|
23
|
-
kind: "function",
|
|
24
|
-
name: "funcA",
|
|
25
|
-
file_path: "src/a.ts",
|
|
26
|
-
start_line: 1,
|
|
27
|
-
end_line: 10,
|
|
28
|
-
signature: "function funcA()",
|
|
29
|
-
body: "",
|
|
30
|
-
fan_in: 5,
|
|
31
|
-
fan_out: 3,
|
|
32
|
-
risk_level: "medium",
|
|
33
|
-
community: 0,
|
|
34
|
-
};
|
|
35
|
-
const ENTITY_B = {
|
|
36
|
-
key: "src/b.ts::funcB",
|
|
37
|
-
kind: "function",
|
|
38
|
-
name: "funcB",
|
|
39
|
-
file_path: "src/b.ts",
|
|
40
|
-
start_line: 1,
|
|
41
|
-
end_line: 20,
|
|
42
|
-
signature: "function funcB()",
|
|
43
|
-
body: "",
|
|
44
|
-
fan_in: 2,
|
|
45
|
-
fan_out: 1,
|
|
46
|
-
risk_level: "low",
|
|
47
|
-
community: 0,
|
|
48
|
-
};
|
|
49
|
-
const ENTITY_C = {
|
|
50
|
-
key: "src/a.ts::funcC",
|
|
51
|
-
kind: "function",
|
|
52
|
-
name: "funcC",
|
|
53
|
-
file_path: "src/a.ts",
|
|
54
|
-
start_line: 12,
|
|
55
|
-
end_line: 25,
|
|
56
|
-
signature: "function funcC()",
|
|
57
|
-
body: "",
|
|
58
|
-
fan_in: 15,
|
|
59
|
-
fan_out: 10,
|
|
60
|
-
risk_level: "high",
|
|
61
|
-
community: 1,
|
|
62
|
-
};
|
|
63
|
-
describe("HealthMapData", () => {
|
|
64
|
-
describe("buildTree", () => {
|
|
65
|
-
it("returns a root node with children grouped by directory", async () => {
|
|
66
|
-
const graph = createMockGraph([ENTITY_A, ENTITY_B, ENTITY_C]);
|
|
67
|
-
const healthMap = new HealthMapData(graph, null);
|
|
68
|
-
const tree = await healthMap.buildTree();
|
|
69
|
-
expect(tree.path).toBe(".");
|
|
70
|
-
expect(tree.children).toBeDefined();
|
|
71
|
-
expect(tree.children.length).toBeGreaterThan(0);
|
|
72
|
-
expect(tree.entity_count).toBe(3);
|
|
73
|
-
});
|
|
74
|
-
it("computes health scores between 0 and 1", async () => {
|
|
75
|
-
const graph = createMockGraph([ENTITY_A, ENTITY_B]);
|
|
76
|
-
const healthMap = new HealthMapData(graph, null);
|
|
77
|
-
const tree = await healthMap.buildTree();
|
|
78
|
-
expect(tree.health_score).toBeGreaterThanOrEqual(0);
|
|
79
|
-
expect(tree.health_score).toBeLessThanOrEqual(1);
|
|
80
|
-
});
|
|
81
|
-
it("assigns risk levels based on health score", async () => {
|
|
82
|
-
const graph = createMockGraph([ENTITY_A]);
|
|
83
|
-
const healthMap = new HealthMapData(graph, null);
|
|
84
|
-
const tree = await healthMap.buildTree();
|
|
85
|
-
expect(["low", "medium", "high", "critical"]).toContain(tree.risk_level);
|
|
86
|
-
});
|
|
87
|
-
it("includes coupling counts from fact store", async () => {
|
|
88
|
-
const graph = createMockGraph([ENTITY_A, ENTITY_B]);
|
|
89
|
-
const factStore = createMockFactStore([
|
|
90
|
-
{
|
|
91
|
-
fact_id: "1",
|
|
92
|
-
fact_type: "semantic",
|
|
93
|
-
subject: "coupling:src/a.ts↔src/b.ts",
|
|
94
|
-
content: "Often changed together",
|
|
95
|
-
effective_confidence: 0.7,
|
|
96
|
-
source: "session_analysis",
|
|
97
|
-
},
|
|
98
|
-
]);
|
|
99
|
-
const healthMap = new HealthMapData(graph, factStore);
|
|
100
|
-
const tree = await healthMap.buildTree();
|
|
101
|
-
// Both files should have coupling_count >= 1
|
|
102
|
-
const srcDir = tree.children?.find((c) => c.name === "src");
|
|
103
|
-
expect(srcDir).toBeDefined();
|
|
104
|
-
expect(srcDir.metrics.coupling_count).toBeGreaterThanOrEqual(1);
|
|
105
|
-
});
|
|
106
|
-
it("filters by rootPath when provided", async () => {
|
|
107
|
-
const graph = createMockGraph([
|
|
108
|
-
ENTITY_A,
|
|
109
|
-
{ ...ENTITY_B, file_path: "lib/b.ts", key: "lib/b.ts::funcB" },
|
|
110
|
-
]);
|
|
111
|
-
const healthMap = new HealthMapData(graph, null);
|
|
112
|
-
const tree = await healthMap.buildTree("src");
|
|
113
|
-
expect(tree.entity_count).toBe(1);
|
|
114
|
-
});
|
|
115
|
-
it("handles empty graph gracefully", async () => {
|
|
116
|
-
const graph = createMockGraph([]);
|
|
117
|
-
const healthMap = new HealthMapData(graph, null);
|
|
118
|
-
const tree = await healthMap.buildTree();
|
|
119
|
-
expect(tree.entity_count).toBe(0);
|
|
120
|
-
expect(tree.children).toEqual([]);
|
|
121
|
-
});
|
|
122
|
-
it("high risk entities lower the health score", async () => {
|
|
123
|
-
const graph1 = createMockGraph([ENTITY_B]); // low risk
|
|
124
|
-
const graph2 = createMockGraph([ENTITY_C]); // high risk
|
|
125
|
-
const map1 = new HealthMapData(graph1, null);
|
|
126
|
-
const map2 = new HealthMapData(graph2, null);
|
|
127
|
-
const tree1 = await map1.buildTree();
|
|
128
|
-
const tree2 = await map2.buildTree();
|
|
129
|
-
expect(tree1.health_score).toBeGreaterThan(tree2.health_score);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
describe("getFileHealth", () => {
|
|
133
|
-
it("returns health for a specific file", async () => {
|
|
134
|
-
const graph = createMockGraph([ENTITY_A, ENTITY_C]);
|
|
135
|
-
const healthMap = new HealthMapData(graph, null);
|
|
136
|
-
const result = await healthMap.getFileHealth("src/a.ts");
|
|
137
|
-
expect(result).not.toBeNull();
|
|
138
|
-
expect(result.path).toBe("src/a.ts");
|
|
139
|
-
expect(result.entity_count).toBe(2);
|
|
140
|
-
expect(result.health_score).toBeGreaterThanOrEqual(0);
|
|
141
|
-
expect(result.health_score).toBeLessThanOrEqual(1);
|
|
142
|
-
});
|
|
143
|
-
it("returns null for unknown file", async () => {
|
|
144
|
-
const graph = createMockGraph([ENTITY_A]);
|
|
145
|
-
const healthMap = new HealthMapData(graph, null);
|
|
146
|
-
const result = await healthMap.getFileHealth("src/unknown.ts");
|
|
147
|
-
expect(result).toBeNull();
|
|
148
|
-
});
|
|
149
|
-
it("includes change frequency from episodic facts", async () => {
|
|
150
|
-
const graph = createMockGraph([ENTITY_A]);
|
|
151
|
-
const factStore = createMockFactStore([], [
|
|
152
|
-
{
|
|
153
|
-
fact_id: "1",
|
|
154
|
-
fact_type: "episodic",
|
|
155
|
-
subject: "src/a.ts",
|
|
156
|
-
content: "Modified",
|
|
157
|
-
effective_confidence: 0.8,
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
fact_id: "2",
|
|
161
|
-
fact_type: "episodic",
|
|
162
|
-
subject: "src/a.ts",
|
|
163
|
-
content: "Modified again",
|
|
164
|
-
effective_confidence: 0.7,
|
|
165
|
-
},
|
|
166
|
-
]);
|
|
167
|
-
const healthMap = new HealthMapData(graph, factStore);
|
|
168
|
-
const result = await healthMap.getFileHealth("src/a.ts");
|
|
169
|
-
expect(result).not.toBeNull();
|
|
170
|
-
expect(result.metrics.change_frequency).toBe(2);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Test Harness — in-process MCP client/server for integration testing.
|
|
3
|
-
*
|
|
4
|
-
* Uses the MCP SDK's InMemoryTransport to create a linked pair of transports
|
|
5
|
-
* that communicate within the same process. No stdio, no ports, no forks.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const harness = await createMcpHarness();
|
|
9
|
-
* const tools = await harness.listTools();
|
|
10
|
-
* const result = await harness.callTool("get_function", { key: "src/foo.ts::myFn" });
|
|
11
|
-
* await harness.close();
|
|
12
|
-
*/
|
|
13
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
14
|
-
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
15
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
-
/**
|
|
17
|
-
* Create an in-process MCP test harness with linked client/server.
|
|
18
|
-
*
|
|
19
|
-
* The `setupServer` callback is where you register tool handlers
|
|
20
|
-
* (ListToolsRequestSchema, CallToolRequestSchema) on the server.
|
|
21
|
-
* This mirrors the real proxy's MCP setup from proxy.ts.
|
|
22
|
-
*/
|
|
23
|
-
export async function createMcpHarness(options = {}) {
|
|
24
|
-
const { serverName = "unerr-test", serverVersion = "0.0.1", setupServer, } = options;
|
|
25
|
-
const server = new Server({ name: serverName, version: serverVersion }, { capabilities: { tools: {} } });
|
|
26
|
-
if (setupServer) {
|
|
27
|
-
await setupServer(server);
|
|
28
|
-
}
|
|
29
|
-
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
30
|
-
const client = new Client({ name: "mcp-test-client", version: "0.0.1" }, { capabilities: {} });
|
|
31
|
-
await server.connect(serverTransport);
|
|
32
|
-
await client.connect(clientTransport);
|
|
33
|
-
const listTools = async () => {
|
|
34
|
-
const response = await client.listTools();
|
|
35
|
-
return response.tools;
|
|
36
|
-
};
|
|
37
|
-
const callTool = async (name, args = {}) => {
|
|
38
|
-
return await client.callTool({ name, arguments: args });
|
|
39
|
-
};
|
|
40
|
-
const close = async () => {
|
|
41
|
-
await client.close();
|
|
42
|
-
await server.close();
|
|
43
|
-
};
|
|
44
|
-
return { client, server, listTools, callTool, close };
|
|
45
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { createMcpHarness } from "./mcp-harness.js";
|
|
4
|
-
describe("MCP Test Harness", () => {
|
|
5
|
-
it("creates a linked client/server pair", async () => {
|
|
6
|
-
const harness = await createMcpHarness();
|
|
7
|
-
expect(harness.client).toBeDefined();
|
|
8
|
-
expect(harness.server).toBeDefined();
|
|
9
|
-
await harness.close();
|
|
10
|
-
});
|
|
11
|
-
it("lists tools from a configured server", async () => {
|
|
12
|
-
const harness = await createMcpHarness({
|
|
13
|
-
setupServer: (server) => {
|
|
14
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
15
|
-
tools: [
|
|
16
|
-
{
|
|
17
|
-
name: "test_tool",
|
|
18
|
-
description: "A test tool",
|
|
19
|
-
inputSchema: {
|
|
20
|
-
type: "object",
|
|
21
|
-
properties: {
|
|
22
|
-
input: { type: "string" },
|
|
23
|
-
},
|
|
24
|
-
required: ["input"],
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
}));
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
const tools = await harness.listTools();
|
|
32
|
-
expect(tools).toHaveLength(1);
|
|
33
|
-
expect(tools[0]?.name).toBe("test_tool");
|
|
34
|
-
await harness.close();
|
|
35
|
-
});
|
|
36
|
-
it("calls a tool and receives a response", async () => {
|
|
37
|
-
const harness = await createMcpHarness({
|
|
38
|
-
setupServer: (server) => {
|
|
39
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
40
|
-
tools: [
|
|
41
|
-
{
|
|
42
|
-
name: "echo",
|
|
43
|
-
description: "Echoes input",
|
|
44
|
-
inputSchema: {
|
|
45
|
-
type: "object",
|
|
46
|
-
properties: {
|
|
47
|
-
message: { type: "string" },
|
|
48
|
-
},
|
|
49
|
-
required: ["message"],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
}));
|
|
54
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
55
|
-
const args = request.params.arguments;
|
|
56
|
-
return {
|
|
57
|
-
content: [{ type: "text", text: `Echo: ${args.message}` }],
|
|
58
|
-
};
|
|
59
|
-
});
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
const result = await harness.callTool("echo", { message: "hello" });
|
|
63
|
-
const content = result.content;
|
|
64
|
-
expect(content).toHaveLength(1);
|
|
65
|
-
expect(content[0]?.text).toBe("Echo: hello");
|
|
66
|
-
await harness.close();
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hook-dedup — file-based TTL dedup for PostToolUse hooks.
|
|
3
|
-
*
|
|
4
|
-
* The module persists to `.unerr/state/hook-recent.json` relative to CWD, so
|
|
5
|
-
* tests chdir into a fresh temp dir per case and restore CWD on teardown.
|
|
6
|
-
*/
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
|
-
import * as os from "node:os";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
-
import { resetHookDedup, shouldEmitOnce } from "../hooks/hook-dedup.js";
|
|
12
|
-
const STATE_FILE = path.join(".unerr", "state", "hook-recent.json");
|
|
13
|
-
describe("hook-dedup", () => {
|
|
14
|
-
let tmpDir;
|
|
15
|
-
let prevCwd;
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
prevCwd = process.cwd();
|
|
18
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "unerr-hook-dedup-"));
|
|
19
|
-
process.chdir(tmpDir);
|
|
20
|
-
});
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
process.chdir(prevCwd);
|
|
23
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
-
});
|
|
25
|
-
describe("shouldEmitOnce", () => {
|
|
26
|
-
it("first call returns true and persists the key", () => {
|
|
27
|
-
expect(shouldEmitOnce("Read:/a/b.ts")).toBe(true);
|
|
28
|
-
expect(fs.existsSync(STATE_FILE)).toBe(true);
|
|
29
|
-
const map = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
30
|
-
expect(typeof map["Read:/a/b.ts"]).toBe("number");
|
|
31
|
-
});
|
|
32
|
-
it("second call within TTL returns false", () => {
|
|
33
|
-
expect(shouldEmitOnce("Read:/a/b.ts", 30_000)).toBe(true);
|
|
34
|
-
expect(shouldEmitOnce("Read:/a/b.ts", 30_000)).toBe(false);
|
|
35
|
-
expect(shouldEmitOnce("Read:/a/b.ts", 30_000)).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
it("call after TTL window returns true again", () => {
|
|
38
|
-
// Tiny TTL — wait it out instead of mocking the clock.
|
|
39
|
-
expect(shouldEmitOnce("Read:/a/b.ts", 10)).toBe(true);
|
|
40
|
-
expect(shouldEmitOnce("Read:/a/b.ts", 10)).toBe(false);
|
|
41
|
-
const waitUntil = Date.now() + 25;
|
|
42
|
-
// Busy wait — short enough not to slow the suite.
|
|
43
|
-
while (Date.now() < waitUntil) {
|
|
44
|
-
/* spin */
|
|
45
|
-
}
|
|
46
|
-
expect(shouldEmitOnce("Read:/a/b.ts", 10)).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
it("different keys are tracked independently", () => {
|
|
49
|
-
expect(shouldEmitOnce("Read:/a/b.ts")).toBe(true);
|
|
50
|
-
expect(shouldEmitOnce("Read:/a/c.ts")).toBe(true);
|
|
51
|
-
expect(shouldEmitOnce("Edit:/a/b.ts")).toBe(true);
|
|
52
|
-
// Each repeated within TTL is suppressed.
|
|
53
|
-
expect(shouldEmitOnce("Read:/a/b.ts")).toBe(false);
|
|
54
|
-
expect(shouldEmitOnce("Read:/a/c.ts")).toBe(false);
|
|
55
|
-
expect(shouldEmitOnce("Edit:/a/b.ts")).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
it("default TTL is 30s — repeated calls suppress", () => {
|
|
58
|
-
expect(shouldEmitOnce("k1")).toBe(true);
|
|
59
|
-
// No explicit ttlMs — should use the 30s default.
|
|
60
|
-
expect(shouldEmitOnce("k1")).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
it("persists timestamps across invocations (simulated via state file)", () => {
|
|
63
|
-
shouldEmitOnce("k:x", 30_000);
|
|
64
|
-
const before = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
65
|
-
expect(before["k:x"]).toBeGreaterThan(0);
|
|
66
|
-
// A second call within TTL should NOT overwrite the timestamp (it's
|
|
67
|
-
// suppressed before the write).
|
|
68
|
-
shouldEmitOnce("k:x", 30_000);
|
|
69
|
-
const after = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
70
|
-
expect(after["k:x"]).toBe(before["k:x"]);
|
|
71
|
-
});
|
|
72
|
-
it("degrades safely when state dir is missing", () => {
|
|
73
|
-
// No .unerr dir exists yet — first call must still return true and
|
|
74
|
-
// create the file.
|
|
75
|
-
expect(fs.existsSync(".unerr")).toBe(false);
|
|
76
|
-
expect(shouldEmitOnce("first")).toBe(true);
|
|
77
|
-
expect(fs.existsSync(STATE_FILE)).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
it("degrades safely on corrupt state file (treats as empty)", () => {
|
|
80
|
-
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
81
|
-
fs.writeFileSync(STATE_FILE, "{ not valid json");
|
|
82
|
-
// Corrupt file should not crash; first call returns true.
|
|
83
|
-
expect(shouldEmitOnce("k")).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe("resetHookDedup", () => {
|
|
87
|
-
it("clears the on-disk dedup file so next call emits again", () => {
|
|
88
|
-
expect(shouldEmitOnce("k", 30_000)).toBe(true);
|
|
89
|
-
expect(shouldEmitOnce("k", 30_000)).toBe(false);
|
|
90
|
-
resetHookDedup();
|
|
91
|
-
expect(shouldEmitOnce("k", 30_000)).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
it("is a no-op when the state file does not exist", () => {
|
|
94
|
-
expect(() => resetHookDedup()).not.toThrow();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
describe("prune behavior", () => {
|
|
98
|
-
it("prunes entries older than ttlMs * PRUNE_FACTOR on write", () => {
|
|
99
|
-
// Seed the state file with an old entry that should be pruned by the
|
|
100
|
-
// next write. PRUNE_FACTOR is 10, so with ttl=10ms the cutoff is 100ms
|
|
101
|
-
// before now.
|
|
102
|
-
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
103
|
-
const stale = Date.now() - 5_000; // 5s old — well past 100ms cutoff
|
|
104
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify({ "stale:key": stale, "fresh:key": Date.now() }));
|
|
105
|
-
// Use a small TTL so PRUNE_FACTOR * ttl = 100ms cutoff.
|
|
106
|
-
shouldEmitOnce("new:key", 10);
|
|
107
|
-
const map = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
108
|
-
expect(map["stale:key"]).toBeUndefined();
|
|
109
|
-
expect(map["new:key"]).toBeDefined();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { claudeCodeAdapter } from "../hooks/adapters/claude-code.js";
|
|
3
|
-
import { clineAdapter } from "../hooks/adapters/cline.js";
|
|
4
|
-
import { cursorAdapter } from "../hooks/adapters/cursor.js";
|
|
5
|
-
import { detectAdapter, enrich, nudge, passthrough, rewrite, runPostToolUseHook, runPreToolUseHook, runPromptSubmitHook, } from "../hooks/hook-runner.js";
|
|
6
|
-
// ── Adapter Detection ────────────────────────────────────────────────
|
|
7
|
-
describe("detectAdapter", () => {
|
|
8
|
-
it("detects Claude Code from hook_event_name", () => {
|
|
9
|
-
const payload = {
|
|
10
|
-
hook_event_name: "PreToolUse",
|
|
11
|
-
tool_input: { command: "ls" },
|
|
12
|
-
};
|
|
13
|
-
expect(detectAdapter(payload).name).toBe("claude-code");
|
|
14
|
-
});
|
|
15
|
-
it("detects Cursor from tool_name + cwd", () => {
|
|
16
|
-
const payload = {
|
|
17
|
-
tool_name: "Read",
|
|
18
|
-
tool_input: { file_path: "foo.ts" },
|
|
19
|
-
cwd: "/project",
|
|
20
|
-
};
|
|
21
|
-
expect(detectAdapter(payload).name).toBe("cursor");
|
|
22
|
-
});
|
|
23
|
-
it("detects Cline from tool + params", () => {
|
|
24
|
-
const payload = {
|
|
25
|
-
tool: "read_file",
|
|
26
|
-
params: { path: "foo.ts" },
|
|
27
|
-
event: "pre_tool",
|
|
28
|
-
};
|
|
29
|
-
expect(detectAdapter(payload).name).toBe("cline");
|
|
30
|
-
});
|
|
31
|
-
it("defaults to Claude Code for unknown payloads", () => {
|
|
32
|
-
expect(detectAdapter({}).name).toBe("claude-code");
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
// ── Claude Code Adapter ──────────────────────────────────────────────
|
|
36
|
-
describe("claudeCodeAdapter", () => {
|
|
37
|
-
it("normalizes tool input from tool_input", () => {
|
|
38
|
-
const payload = {
|
|
39
|
-
hook_event_name: "PreToolUse",
|
|
40
|
-
tool_input: { command: "ls" },
|
|
41
|
-
};
|
|
42
|
-
const n = claudeCodeAdapter.normalize(payload);
|
|
43
|
-
expect(n.toolInput).toEqual({ command: "ls" });
|
|
44
|
-
expect(n.event).toBe("PreToolUse");
|
|
45
|
-
});
|
|
46
|
-
it("formats passthrough as {}", () => {
|
|
47
|
-
expect(claudeCodeAdapter.formatPreToolUse(passthrough())).toBe("{}");
|
|
48
|
-
expect(claudeCodeAdapter.formatPostToolUse(passthrough())).toBe("{}");
|
|
49
|
-
});
|
|
50
|
-
it("formats nudge with systemMessage", () => {
|
|
51
|
-
const result = JSON.parse(claudeCodeAdapter.formatPreToolUse(nudge("Use search_code")));
|
|
52
|
-
expect(result.hookSpecificOutput.permissionDecision).toBe("allow");
|
|
53
|
-
expect(result.hookSpecificOutput.systemMessage).toBe("Use search_code");
|
|
54
|
-
});
|
|
55
|
-
it("formats rewrite with updatedInput", () => {
|
|
56
|
-
const result = JSON.parse(claudeCodeAdapter.formatPreToolUse(rewrite({ command: "unerr exec -- ls" })));
|
|
57
|
-
expect(result.hookSpecificOutput.updatedInput).toEqual({
|
|
58
|
-
command: "unerr exec -- ls",
|
|
59
|
-
});
|
|
60
|
-
expect(result.hookSpecificOutput.permissionDecision).toBe("allow");
|
|
61
|
-
});
|
|
62
|
-
it("formats PostToolUse enrich with additionalContext", () => {
|
|
63
|
-
const result = JSON.parse(claudeCodeAdapter.formatPostToolUse(enrich("Try get_entity")));
|
|
64
|
-
expect(result.hookSpecificOutput.hookEventName).toBe("PostToolUse");
|
|
65
|
-
expect(result.hookSpecificOutput.additionalContext).toBe("Try get_entity");
|
|
66
|
-
});
|
|
67
|
-
it("formats UserPromptSubmit with additionalContext", () => {
|
|
68
|
-
const result = JSON.parse(claudeCodeAdapter.formatPromptSubmit(enrich("Use unerr tools")));
|
|
69
|
-
expect(result.hookSpecificOutput.hookEventName).toBe("UserPromptSubmit");
|
|
70
|
-
expect(result.hookSpecificOutput.additionalContext).toBe("Use unerr tools");
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
// ── Cursor Adapter ───────────────────────────────────────────────────
|
|
74
|
-
describe("cursorAdapter", () => {
|
|
75
|
-
it("detects Cursor payload", () => {
|
|
76
|
-
expect(cursorAdapter.detect({
|
|
77
|
-
tool_name: "Read",
|
|
78
|
-
tool_input: {},
|
|
79
|
-
cwd: "/project",
|
|
80
|
-
})).toBe(true);
|
|
81
|
-
// Should NOT detect Claude Code
|
|
82
|
-
expect(cursorAdapter.detect({
|
|
83
|
-
hook_event_name: "PreToolUse",
|
|
84
|
-
tool_name: "Read",
|
|
85
|
-
cwd: "/project",
|
|
86
|
-
})).toBe(false);
|
|
87
|
-
});
|
|
88
|
-
it("normalizes tool input from tool_input field", () => {
|
|
89
|
-
const payload = {
|
|
90
|
-
tool_name: "Read",
|
|
91
|
-
tool_input: { file_path: "foo.ts" },
|
|
92
|
-
cwd: "/project",
|
|
93
|
-
};
|
|
94
|
-
const n = cursorAdapter.normalize(payload);
|
|
95
|
-
expect(n.toolInput).toEqual({ file_path: "foo.ts" });
|
|
96
|
-
expect(n.toolName).toBe("Read");
|
|
97
|
-
});
|
|
98
|
-
it("formats passthrough as { permission: allow }", () => {
|
|
99
|
-
const result = JSON.parse(cursorAdapter.formatPreToolUse(passthrough()));
|
|
100
|
-
expect(result.permission).toBe("allow");
|
|
101
|
-
});
|
|
102
|
-
it("formats nudge with agent_message", () => {
|
|
103
|
-
const result = JSON.parse(cursorAdapter.formatPreToolUse(nudge("Use file_outline")));
|
|
104
|
-
expect(result.permission).toBe("allow");
|
|
105
|
-
expect(result.agent_message).toBe("Use file_outline");
|
|
106
|
-
});
|
|
107
|
-
it("formats rewrite with updated_input", () => {
|
|
108
|
-
const result = JSON.parse(cursorAdapter.formatPreToolUse(rewrite({ command: "unerr exec -- ls" })));
|
|
109
|
-
expect(result.permission).toBe("allow");
|
|
110
|
-
expect(result.updated_input).toEqual({ command: "unerr exec -- ls" });
|
|
111
|
-
});
|
|
112
|
-
it("formats PostToolUse enrich with additional_context", () => {
|
|
113
|
-
const result = JSON.parse(cursorAdapter.formatPostToolUse(enrich("Use search_code")));
|
|
114
|
-
expect(result.additional_context).toBe("Use search_code");
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
// ── Cline Adapter ────────────────────────────────────────────────────
|
|
118
|
-
describe("clineAdapter", () => {
|
|
119
|
-
it("detects Cline payload", () => {
|
|
120
|
-
expect(clineAdapter.detect({ tool: "read_file", params: { path: "foo.ts" } })).toBe(true);
|
|
121
|
-
// Should NOT detect Claude Code
|
|
122
|
-
expect(clineAdapter.detect({
|
|
123
|
-
tool: "read_file",
|
|
124
|
-
params: {},
|
|
125
|
-
hook_event_name: "PreToolUse",
|
|
126
|
-
})).toBe(false);
|
|
127
|
-
});
|
|
128
|
-
it("normalizes tool names from Cline format", () => {
|
|
129
|
-
const payload = {
|
|
130
|
-
tool: "read_file",
|
|
131
|
-
params: { path: "foo.ts" },
|
|
132
|
-
event: "pre_tool",
|
|
133
|
-
};
|
|
134
|
-
const n = clineAdapter.normalize(payload);
|
|
135
|
-
expect(n.toolName).toBe("Read");
|
|
136
|
-
expect(n.toolInput.file_path).toBe("foo.ts");
|
|
137
|
-
expect(n.event).toBe("PreToolUse");
|
|
138
|
-
});
|
|
139
|
-
it("normalizes search_files to Grep", () => {
|
|
140
|
-
const payload = { tool: "search_files", params: { regex: "myFunc" } };
|
|
141
|
-
const n = clineAdapter.normalize(payload);
|
|
142
|
-
expect(n.toolName).toBe("Grep");
|
|
143
|
-
expect(n.toolInput.pattern).toBe("myFunc");
|
|
144
|
-
});
|
|
145
|
-
it("formats passthrough as { allow: true }", () => {
|
|
146
|
-
const result = JSON.parse(clineAdapter.formatPreToolUse(passthrough()));
|
|
147
|
-
expect(result.allow).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
it("formats nudge with context", () => {
|
|
150
|
-
const result = JSON.parse(clineAdapter.formatPreToolUse(nudge("Use graph tools")));
|
|
151
|
-
expect(result.allow).toBe(true);
|
|
152
|
-
expect(result.context).toBe("Use graph tools");
|
|
153
|
-
});
|
|
154
|
-
it("formats PostToolUse enrich with context", () => {
|
|
155
|
-
const result = JSON.parse(clineAdapter.formatPostToolUse(enrich("Try get_references")));
|
|
156
|
-
expect(result.context).toBe("Try get_references");
|
|
157
|
-
});
|
|
158
|
-
it("maps post_tool event to PostToolUse", () => {
|
|
159
|
-
const payload = {
|
|
160
|
-
tool: "execute_command",
|
|
161
|
-
params: { command: "ls" },
|
|
162
|
-
event: "post_tool",
|
|
163
|
-
};
|
|
164
|
-
const n = clineAdapter.normalize(payload);
|
|
165
|
-
expect(n.event).toBe("PostToolUse");
|
|
166
|
-
expect(n.toolName).toBe("Bash");
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
// ── Universal Runner ─────────────────────────────────────────────────
|
|
170
|
-
describe("runPreToolUseHook", () => {
|
|
171
|
-
it("returns {} for empty stdin", () => {
|
|
172
|
-
expect(runPreToolUseHook("", () => passthrough())).toBe("{}");
|
|
173
|
-
});
|
|
174
|
-
it("returns {} for invalid JSON", () => {
|
|
175
|
-
expect(runPreToolUseHook("not json", () => passthrough())).toBe("{}");
|
|
176
|
-
});
|
|
177
|
-
it("routes Claude Code payload through handler and formats correctly", () => {
|
|
178
|
-
const stdin = JSON.stringify({
|
|
179
|
-
hook_event_name: "PreToolUse",
|
|
180
|
-
tool_input: { file_path: "src/foo.ts" },
|
|
181
|
-
});
|
|
182
|
-
const result = JSON.parse(runPreToolUseHook(stdin, () => nudge("Use file_read")));
|
|
183
|
-
expect(result.hookSpecificOutput.systemMessage).toBe("Use file_read");
|
|
184
|
-
});
|
|
185
|
-
it("routes Cursor payload through handler and formats correctly", () => {
|
|
186
|
-
const stdin = JSON.stringify({
|
|
187
|
-
tool_name: "Read",
|
|
188
|
-
tool_input: { file_path: "src/foo.ts" },
|
|
189
|
-
cwd: "/project",
|
|
190
|
-
});
|
|
191
|
-
const result = JSON.parse(runPreToolUseHook(stdin, () => nudge("Use file_read")));
|
|
192
|
-
expect(result.permission).toBe("allow");
|
|
193
|
-
expect(result.agent_message).toBe("Use file_read");
|
|
194
|
-
});
|
|
195
|
-
it("routes Cline payload through handler and formats correctly", () => {
|
|
196
|
-
const stdin = JSON.stringify({
|
|
197
|
-
tool: "read_file",
|
|
198
|
-
params: { path: "src/foo.ts" },
|
|
199
|
-
event: "pre_tool",
|
|
200
|
-
});
|
|
201
|
-
const result = JSON.parse(runPreToolUseHook(stdin, () => nudge("Use file_read")));
|
|
202
|
-
expect(result.allow).toBe(true);
|
|
203
|
-
expect(result.context).toBe("Use file_read");
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
describe("runPostToolUseHook", () => {
|
|
207
|
-
it("routes Claude Code enrich correctly", () => {
|
|
208
|
-
const stdin = JSON.stringify({
|
|
209
|
-
hook_event_name: "PostToolUse",
|
|
210
|
-
tool_input: { file_path: "src/foo.ts" },
|
|
211
|
-
});
|
|
212
|
-
const result = JSON.parse(runPostToolUseHook(stdin, () => enrich("Try get_references")));
|
|
213
|
-
expect(result.hookSpecificOutput.additionalContext).toBe("Try get_references");
|
|
214
|
-
});
|
|
215
|
-
it("routes Cursor enrich correctly", () => {
|
|
216
|
-
const stdin = JSON.stringify({
|
|
217
|
-
tool_name: "Read",
|
|
218
|
-
tool_input: { file_path: "src/foo.ts" },
|
|
219
|
-
cwd: "/project",
|
|
220
|
-
});
|
|
221
|
-
const result = JSON.parse(runPostToolUseHook(stdin, () => enrich("Try get_references")));
|
|
222
|
-
expect(result.additional_context).toBe("Try get_references");
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
describe("runPromptSubmitHook", () => {
|
|
226
|
-
it("routes prompt submit for Claude Code", () => {
|
|
227
|
-
const stdin = JSON.stringify({
|
|
228
|
-
hook_event_name: "UserPromptSubmit",
|
|
229
|
-
user_message: "Add a new function to handle auth",
|
|
230
|
-
});
|
|
231
|
-
const result = JSON.parse(runPromptSubmitHook(stdin, () => enrich("Use unerr tools")));
|
|
232
|
-
expect(result.hookSpecificOutput.hookEventName).toBe("UserPromptSubmit");
|
|
233
|
-
expect(result.hookSpecificOutput.additionalContext).toBe("Use unerr tools");
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
// ── Result Constructors ──────────────────────────────────────────────
|
|
237
|
-
describe("result constructors", () => {
|
|
238
|
-
it("passthrough creates correct shape", () => {
|
|
239
|
-
expect(passthrough()).toEqual({ type: "passthrough" });
|
|
240
|
-
});
|
|
241
|
-
it("nudge creates correct shape", () => {
|
|
242
|
-
expect(nudge("msg")).toEqual({ type: "nudge", message: "msg" });
|
|
243
|
-
});
|
|
244
|
-
it("rewrite creates correct shape", () => {
|
|
245
|
-
expect(rewrite({ cmd: "x" })).toEqual({
|
|
246
|
-
type: "rewrite",
|
|
247
|
-
updatedInput: { cmd: "x" },
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
it("enrich creates correct shape", () => {
|
|
251
|
-
expect(enrich("ctx")).toEqual({ type: "enrich", message: "ctx" });
|
|
252
|
-
});
|
|
253
|
-
});
|