@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.
Files changed (218) hide show
  1. package/README.md +70 -194
  2. package/dist/cli.js +39149 -36991
  3. package/package.json +9 -2
  4. package/dist/__tests__/architecture-guard.test.js +0 -122
  5. package/dist/__tests__/arg-validator.test.js +0 -205
  6. package/dist/__tests__/ast-extractor.test.js +0 -203
  7. package/dist/__tests__/auto-bootstrap.test.js +0 -280
  8. package/dist/__tests__/background-indexer.test.js +0 -228
  9. package/dist/__tests__/blast-radius-engine.test.js +0 -200
  10. package/dist/__tests__/bridge-isolation.test.js +0 -37
  11. package/dist/__tests__/budget-enforcer.test.js +0 -53
  12. package/dist/__tests__/cfg-test-detection-perf.test.js +0 -82
  13. package/dist/__tests__/change-narrative.test.js +0 -190
  14. package/dist/__tests__/check-commit.test.js +0 -258
  15. package/dist/__tests__/checksum.test.js +0 -34
  16. package/dist/__tests__/commit-watcher.test.js +0 -154
  17. package/dist/__tests__/community-detection.test.js +0 -179
  18. package/dist/__tests__/community-tools.test.js +0 -299
  19. package/dist/__tests__/components.test.js +0 -449
  20. package/dist/__tests__/compression-log.test.js +0 -174
  21. package/dist/__tests__/compression-quality-monitor.test.js +0 -40
  22. package/dist/__tests__/config-healer.test.js +0 -165
  23. package/dist/__tests__/context-ledger.test.js +0 -58
  24. package/dist/__tests__/convention-detector.test.js +0 -99
  25. package/dist/__tests__/convention-learner.test.js +0 -86
  26. package/dist/__tests__/correction-detector.test.js +0 -330
  27. package/dist/__tests__/daemon-autostart-install.test.js +0 -283
  28. package/dist/__tests__/daemon-bridge.test.js +0 -222
  29. package/dist/__tests__/daemon-dashboard.test.js +0 -202
  30. package/dist/__tests__/daemon-registry.test.js +0 -240
  31. package/dist/__tests__/daemon-supervisor.test.js +0 -318
  32. package/dist/__tests__/daemon-version-check.test.js +0 -275
  33. package/dist/__tests__/decision-point-detector.test.js +0 -98
  34. package/dist/__tests__/deep-link.test.js +0 -143
  35. package/dist/__tests__/disallowed-tools.test.js +0 -115
  36. package/dist/__tests__/drift-tracker.test.js +0 -582
  37. package/dist/__tests__/durability-scorer.test.js +0 -152
  38. package/dist/__tests__/efficiency-tracker.test.js +0 -65
  39. package/dist/__tests__/enrich.test.js +0 -144
  40. package/dist/__tests__/entity-rewind.test.js +0 -248
  41. package/dist/__tests__/ephemeral.test.js +0 -111
  42. package/dist/__tests__/exploration-cost.test.js +0 -93
  43. package/dist/__tests__/fact-generator.test.js +0 -197
  44. package/dist/__tests__/file-l0-graph.test.js +0 -244
  45. package/dist/__tests__/file-logger.test.js +0 -82
  46. package/dist/__tests__/file-outline.test.js +0 -141
  47. package/dist/__tests__/file-read-protocol.test.js +0 -188
  48. package/dist/__tests__/format-encoder.test.js +0 -233
  49. package/dist/__tests__/git-attribution.test.js +0 -259
  50. package/dist/__tests__/graph-temporal-joiner.test.js +0 -219
  51. package/dist/__tests__/health-grade-enhanced.test.js +0 -138
  52. package/dist/__tests__/health-map-data.test.js +0 -173
  53. package/dist/__tests__/helpers/mcp-harness.js +0 -45
  54. package/dist/__tests__/helpers/mcp-harness.test.js +0 -68
  55. package/dist/__tests__/hook-dedup.test.js +0 -112
  56. package/dist/__tests__/hook-runner.test.js +0 -253
  57. package/dist/__tests__/indexer-cfg.test.js +0 -185
  58. package/dist/__tests__/indexer-cross-file.test.js +0 -172
  59. package/dist/__tests__/indexer-extraction.test.js +0 -245
  60. package/dist/__tests__/indexer-incremental.test.js +0 -232
  61. package/dist/__tests__/indexer-language-expansion.test.js +0 -165
  62. package/dist/__tests__/init-push.test.js +0 -131
  63. package/dist/__tests__/instruction-writer.test.js +0 -179
  64. package/dist/__tests__/intelligence-integration.test.js +0 -217
  65. package/dist/__tests__/intent-correlator.test.js +0 -175
  66. package/dist/__tests__/intent-detector.test.js +0 -235
  67. package/dist/__tests__/intent-encoder.test.js +0 -167
  68. package/dist/__tests__/java-build-tool-detection.test.js +0 -174
  69. package/dist/__tests__/layer3-sprint-q.test.js +0 -160
  70. package/dist/__tests__/layer3-sprint-r.test.js +0 -91
  71. package/dist/__tests__/layer3-sprint-s.test.js +0 -183
  72. package/dist/__tests__/layer3-sprint-t.test.js +0 -201
  73. package/dist/__tests__/layer3-sprint-u.test.js +0 -174
  74. package/dist/__tests__/layer4-sprint-ba2.test.js +0 -354
  75. package/dist/__tests__/layer4-sprint-ba4.test.js +0 -84
  76. package/dist/__tests__/layer4-sprint-vs.test.js +0 -105
  77. package/dist/__tests__/ledger-chains.test.js +0 -162
  78. package/dist/__tests__/lifecycle-machine.test.js +0 -226
  79. package/dist/__tests__/local-chat-provider.test.js +0 -170
  80. package/dist/__tests__/local-convention-detector.test.js +0 -308
  81. package/dist/__tests__/local-embeddings.test.js +0 -422
  82. package/dist/__tests__/local-graph.test.js +0 -540
  83. package/dist/__tests__/local-indexer.test.js +0 -228
  84. package/dist/__tests__/local-intelligence-l3.test.js +0 -332
  85. package/dist/__tests__/local-llm.test.js +0 -253
  86. package/dist/__tests__/local-mode-offline.test.js +0 -187
  87. package/dist/__tests__/local-mode-stats.test.js +0 -273
  88. package/dist/__tests__/local-mode-tui.test.js +0 -343
  89. package/dist/__tests__/local-parse.test.js +0 -199
  90. package/dist/__tests__/log-tailer.test.js +0 -208
  91. package/dist/__tests__/loop-breaker.test.js +0 -276
  92. package/dist/__tests__/loop-miner.test.js +0 -226
  93. package/dist/__tests__/mcp-config.test.js +0 -126
  94. package/dist/__tests__/mcp-content-json.test.js +0 -10
  95. package/dist/__tests__/mcp-envelope.test.js +0 -124
  96. package/dist/__tests__/metrics-store.test.js +0 -223
  97. package/dist/__tests__/native-watcher.test.js +0 -191
  98. package/dist/__tests__/navigation-hooks-agent-aware.test.js +0 -145
  99. package/dist/__tests__/negative-knowledge.test.js +0 -116
  100. package/dist/__tests__/network-boundary.test.js +0 -190
  101. package/dist/__tests__/network-firewall.test.js +0 -112
  102. package/dist/__tests__/nudge-invariants.test.js +0 -160
  103. package/dist/__tests__/nudge-v2.test.js +0 -225
  104. package/dist/__tests__/offline-rewind.test.js +0 -251
  105. package/dist/__tests__/open-threads.test.js +0 -89
  106. package/dist/__tests__/output-compressor.test.js +0 -93
  107. package/dist/__tests__/pending-violations.test.js +0 -112
  108. package/dist/__tests__/persistence-effectiveness.test.js +0 -143
  109. package/dist/__tests__/provider-factory.test.js +0 -42
  110. package/dist/__tests__/providers.test.js +0 -24
  111. package/dist/__tests__/proxy.test.js +0 -314
  112. package/dist/__tests__/query-router.test.js +0 -1018
  113. package/dist/__tests__/reasoning-quality-route.test.js +0 -138
  114. package/dist/__tests__/redactor.test.js +0 -120
  115. package/dist/__tests__/resource-monitor.test.js +0 -57
  116. package/dist/__tests__/response-envelope.test.js +0 -100
  117. package/dist/__tests__/risk-classifier.test.js +0 -101
  118. package/dist/__tests__/risk-signal-scope.test.js +0 -75
  119. package/dist/__tests__/rule-evaluator.test.js +0 -280
  120. package/dist/__tests__/scip-decoder.test.js +0 -49
  121. package/dist/__tests__/scip-downloader.test.js +0 -201
  122. package/dist/__tests__/scip-merger.test.js +0 -103
  123. package/dist/__tests__/search-index.test.js +0 -422
  124. package/dist/__tests__/semantic-enrichment.test.js +0 -360
  125. package/dist/__tests__/session-brief-builder.test.js +0 -187
  126. package/dist/__tests__/session-context.test.js +0 -221
  127. package/dist/__tests__/session-continuity.test.js +0 -144
  128. package/dist/__tests__/session-dedup.test.js +0 -74
  129. package/dist/__tests__/session-event-wiring.test.js +0 -206
  130. package/dist/__tests__/session-events.test.js +0 -149
  131. package/dist/__tests__/session-legend.test.js +0 -20
  132. package/dist/__tests__/session-persistence.test.js +0 -131
  133. package/dist/__tests__/session-resume-block.test.js +0 -107
  134. package/dist/__tests__/session-resume.test.js +0 -97
  135. package/dist/__tests__/session-summary-writer.test.js +0 -134
  136. package/dist/__tests__/shadow-ledger.test.js +0 -203
  137. package/dist/__tests__/shell-classifier.test.js +0 -151
  138. package/dist/__tests__/shell-compression-floor.test.js +0 -189
  139. package/dist/__tests__/shell-compression-v2.test.js +0 -339
  140. package/dist/__tests__/shell-compressor.test.js +0 -35
  141. package/dist/__tests__/shell-hooks.test.js +0 -128
  142. package/dist/__tests__/shell-strategies.test.js +0 -644
  143. package/dist/__tests__/shell-tee.test.js +0 -133
  144. package/dist/__tests__/signal-dedup.test.js +0 -158
  145. package/dist/__tests__/signal-reinforcer.test.js +0 -77
  146. package/dist/__tests__/signal-scorer.test.js +0 -251
  147. package/dist/__tests__/signal-show-store.test.js +0 -108
  148. package/dist/__tests__/smart-truncate.test.js +0 -215
  149. package/dist/__tests__/snapshot-v2.test.js +0 -113
  150. package/dist/__tests__/sprint-l1-local-mode.test.js +0 -130
  151. package/dist/__tests__/sprint-l10-boot.test.js +0 -220
  152. package/dist/__tests__/sprint-l9-offline-commands.test.js +0 -189
  153. package/dist/__tests__/sprint-q-persistent-context.test.js +0 -198
  154. package/dist/__tests__/sprint-s1-wiring.test.js +0 -215
  155. package/dist/__tests__/sprint-s2-wiring.test.js +0 -256
  156. package/dist/__tests__/sprint-s3-wiring.test.js +0 -195
  157. package/dist/__tests__/sprint-s4-wiring.test.js +0 -213
  158. package/dist/__tests__/sprint-s6-hooks.test.js +0 -222
  159. package/dist/__tests__/sprint-s7-persistent.test.js +0 -263
  160. package/dist/__tests__/sprint-s8-value.test.js +0 -167
  161. package/dist/__tests__/sprint-s9-behavioral.test.js +0 -179
  162. package/dist/__tests__/sprint3-intelligence.test.js +0 -297
  163. package/dist/__tests__/sprint5-mcp-server.test.js +0 -136
  164. package/dist/__tests__/startup-display.test.js +0 -302
  165. package/dist/__tests__/startup-log-file.test.js +0 -97
  166. package/dist/__tests__/stash-manager.test.js +0 -229
  167. package/dist/__tests__/state-detector.test.js +0 -92
  168. package/dist/__tests__/status-dashboard.test.js +0 -142
  169. package/dist/__tests__/temporal-facts.test.js +0 -292
  170. package/dist/__tests__/temporal-routes.test.js +0 -142
  171. package/dist/__tests__/test-detector.test.js +0 -174
  172. package/dist/__tests__/theme.test.js +0 -72
  173. package/dist/__tests__/timeline-agents.test.js +0 -122
  174. package/dist/__tests__/timeline-bootstrap.test.js +0 -176
  175. package/dist/__tests__/timeline-filters.test.js +0 -193
  176. package/dist/__tests__/timeline-markers.test.js +0 -151
  177. package/dist/__tests__/timeline-routes.test.js +0 -156
  178. package/dist/__tests__/timeline-store.test.js +0 -171
  179. package/dist/__tests__/token-counter.test.js +0 -86
  180. package/dist/__tests__/token-estimator.test.js +0 -96
  181. package/dist/__tests__/token-flow-api.test.js +0 -239
  182. package/dist/__tests__/token-flow-instrumentation.test.js +0 -437
  183. package/dist/__tests__/token-flow-persistence.test.js +0 -356
  184. package/dist/__tests__/token-flow-routes.test.js +0 -199
  185. package/dist/__tests__/token-flow.test.js +0 -695
  186. package/dist/__tests__/tool-clusters.test.js +0 -177
  187. package/dist/__tests__/transport-mux.test.js +0 -283
  188. package/dist/__tests__/turn-segmenter.test.js +0 -166
  189. package/dist/__tests__/uninstall.test.js +0 -141
  190. package/dist/__tests__/warm-start-policy.test.js +0 -271
  191. package/dist/__tests__/wire-cap-nudge.test.js +0 -77
  192. package/dist/__tests__/worker-pool.test.js +0 -101
  193. package/dist/ui/assets/index-7gl3mIuY.css +0 -1
  194. package/dist/ui/assets/index-CX4FCWGT.js +0 -10
  195. package/dist/ui/assets/rolldown-runtime-S-ySWqyJ.js +0 -1
  196. package/dist/ui/assets/vis-network-NIJHUFI3.js +0 -908
  197. package/dist/ui/fonts/jetbrains-mono-latin-400-normal.woff +0 -0
  198. package/dist/ui/icon-wordmark.png +0 -0
  199. package/dist/ui/icon-wordmark.svg +0 -30
  200. package/dist/ui/icon.png +0 -0
  201. package/dist/ui/icon.svg +0 -25
  202. package/dist/ui/index.html +0 -15
  203. package/dist/ui/prototype-sandbox/index.html +0 -257
  204. package/dist/ui/screenshots/activity.png +0 -0
  205. package/dist/ui/screenshots/code-base-intelligence.png +0 -0
  206. package/dist/ui/screenshots/dashboard.png +0 -0
  207. package/dist/ui/screenshots/project-memory.png +0 -0
  208. package/dist/ui/screenshots/reasoning-quality.png +0 -0
  209. package/dist/ui/screenshots/reasoning-session.png +0 -0
  210. package/dist/ui/screenshots/token-session.png +0 -0
  211. package/dist/ui/screenshots/token-trace-main.png +0 -0
  212. package/dist/ui/screenshots/token-turn.png +0 -0
  213. package/dist/ui/unerr-wordmark.png +0 -0
  214. package/dist/ui/unerr-wordmark.svg +0 -9
  215. package/dist/ui/unerr.png +0 -0
  216. package/dist/ui/unerr.svg +0 -25
  217. package/dist/ui/web-app-manifest-192x192.png +0 -0
  218. 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
- });