@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,222 +0,0 @@
1
- /**
2
- * DM-3: MCP Bridge Integration tests.
3
- *
4
- * Tests cover:
5
- * - client.ts: sendRequest, sendFireAndForget, probeDaemon
6
- * - bootstrap.ts: waitForDaemonReady (poll-only, no spawn)
7
- * - mcpBoot socket discovery order (repo sock → unerrd if running + registered)
8
- * - Explicit error exits when no process / repo not registered
9
- * - Bridge lifecycle: connect/disconnect through daemon
10
- * - Activity throttling
11
- * - Module isolation (no intelligence imports)
12
- */
13
- import { readFileSync, } from "node:fs";
14
- import { resolve } from "node:path";
15
- import { describe, expect, it } from "vitest";
16
- // ── Client module tests ────────────────────────────────────────────
17
- describe("Daemon client (client.ts)", () => {
18
- it("exports all required methods", async () => {
19
- const client = await import("../daemon/client.js");
20
- expect(typeof client.sendRequest).toBe("function");
21
- expect(typeof client.sendFireAndForget).toBe("function");
22
- expect(typeof client.ensureRepo).toBe("function");
23
- expect(typeof client.connectRepo).toBe("function");
24
- expect(typeof client.disconnectRepo).toBe("function");
25
- expect(typeof client.sendActivity).toBe("function");
26
- expect(typeof client.getStatus).toBe("function");
27
- expect(typeof client.probeDaemon).toBe("function");
28
- expect(typeof client.daemonSockPath).toBe("function");
29
- });
30
- it("daemonSockPath returns path under ~/.unerr/", async () => {
31
- const { daemonSockPath } = await import("../daemon/client.js");
32
- const p = daemonSockPath();
33
- expect(p).toContain(".unerr");
34
- expect(p).toContain("unerrd.sock");
35
- });
36
- it("probeDaemon returns false for nonexistent socket", async () => {
37
- const { probeDaemon } = await import("../daemon/client.js");
38
- const result = await probeDaemon("/tmp/nonexistent-unerr-test.sock");
39
- expect(result).toBe(false);
40
- });
41
- it("sendRequest rejects on nonexistent socket", async () => {
42
- const { sendRequest } = await import("../daemon/client.js");
43
- await expect(sendRequest("/tmp/nonexistent-unerr-test.sock", { cmd: "status" }, 1000)).rejects.toThrow();
44
- });
45
- it("sendFireAndForget does not throw on nonexistent socket", async () => {
46
- const { sendFireAndForget } = await import("../daemon/client.js");
47
- // Should not throw — it's fire-and-forget
48
- expect(() => sendFireAndForget("/tmp/nonexistent-unerr-test.sock", {
49
- cmd: "activity",
50
- repo: "/tmp/fake",
51
- })).not.toThrow();
52
- });
53
- });
54
- // ── Bootstrap module tests ─────────────────────────────────────────
55
- describe("Daemon bootstrap (bootstrap.ts)", () => {
56
- it("exports waitForDaemonReady and ensureDaemonRunning alias", async () => {
57
- const bootstrap = await import("../daemon/bootstrap.js");
58
- expect(typeof bootstrap.waitForDaemonReady).toBe("function");
59
- expect(typeof bootstrap.ensureDaemonRunning).toBe("function");
60
- expect(bootstrap.ensureDaemonRunning).toBe(bootstrap.waitForDaemonReady);
61
- });
62
- });
63
- // ── Module isolation tests ─────────────────────────────────────────
64
- describe("DM-3 module isolation", () => {
65
- it("client.ts imports only from daemon/ and node builtins", () => {
66
- const content = readFileSync(resolve(process.cwd(), "src/daemon/client.ts"), "utf-8");
67
- const forbidden = [
68
- /from\s+["']\.\.\/intelligence\//,
69
- /from\s+["']\.\.\/behaviors\//,
70
- /from\s+["']\.\.\/tracking\//,
71
- /from\s+["']\.\.\/proxy\//,
72
- ];
73
- for (const pattern of forbidden) {
74
- expect(content).not.toMatch(pattern);
75
- }
76
- // Must import from daemon/
77
- expect(content).toMatch(/from\s+["']\.\/registry/);
78
- expect(content).toMatch(/from\s+["']\.\/protocol/);
79
- });
80
- it("bootstrap.ts imports only from daemon/ and node builtins", () => {
81
- const content = readFileSync(resolve(process.cwd(), "src/daemon/bootstrap.ts"), "utf-8");
82
- const forbidden = [
83
- /from\s+["']\.\.\/intelligence\//,
84
- /from\s+["']\.\.\/behaviors\//,
85
- /from\s+["']\.\.\/tracking\//,
86
- /from\s+["']\.\.\/proxy\//,
87
- ];
88
- for (const pattern of forbidden) {
89
- expect(content).not.toMatch(pattern);
90
- }
91
- expect(content).toMatch(/from\s+["']\.\/client/);
92
- });
93
- it("bridge.ts still imports nothing from intelligence/", () => {
94
- const content = readFileSync(resolve(process.cwd(), "src/proxy/bridge.ts"), "utf-8");
95
- const forbidden = [
96
- /from\s+["']\.\.\/intelligence\//,
97
- /from\s+["']\.\.\/behaviors\//,
98
- /from\s+["']\.\.\/tracking\//,
99
- ];
100
- for (const pattern of forbidden) {
101
- expect(content).not.toMatch(pattern);
102
- }
103
- });
104
- });
105
- // ── Socket discovery flow tests ────────────────────────────────────
106
- describe("mcpBoot socket discovery", () => {
107
- it("cli.ts mcpBoot checks per-repo sock first", () => {
108
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
109
- // Step 1: per-repo proxy sock
110
- expect(content).toContain("proxy.sock");
111
- expect(content).toContain("probeResult.alive");
112
- // Step 2: unerrd (no auto-spawn, just probe + bridge)
113
- expect(content).toContain("probeDaemon");
114
- expect(content).toContain("ensureRepo");
115
- expect(content).toContain("connectRepo");
116
- expect(content).toContain("disconnectRepo");
117
- });
118
- it("mcpBoot does NOT auto-register repos (explicit registration only)", () => {
119
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
120
- // mcpBoot checks findRepo but never calls addRepo
121
- expect(content).toContain("findRepo(cwd)");
122
- expect(content).not.toContain("addRepo(cwd, {})");
123
- });
124
- it("mcpBoot includes activity throttle at 60s", () => {
125
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
126
- expect(content).toContain("ACTIVITY_THROTTLE_MS = 60_000");
127
- expect(content).toContain("sendActivity(daemonSock, cwd)");
128
- });
129
- });
130
- // ── Bridge lifecycle tests ─────────────────────────────────────────
131
- describe("Bridge connect/disconnect lifecycle", () => {
132
- it("mcpBoot calls connectRepo before bridging and disconnectRepo after", () => {
133
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
134
- // Ordering: connectRepo comes before startUdsBridge, disconnectRepo after
135
- const connectIdx = content.indexOf("await connectRepo(daemonSock, cwd)");
136
- const bridgeIdx = content.indexOf("await startUdsBridge(repoSockViaEnsure)");
137
- const disconnectIdx = content.indexOf("await disconnectRepo(daemonSock, cwd)");
138
- expect(connectIdx).toBeGreaterThan(-1);
139
- expect(bridgeIdx).toBeGreaterThan(-1);
140
- expect(disconnectIdx).toBeGreaterThan(-1);
141
- expect(connectIdx).toBeLessThan(bridgeIdx);
142
- expect(bridgeIdx).toBeLessThan(disconnectIdx);
143
- });
144
- });
145
- // ── Readiness polling design tests (bootstrap.ts) ─────────────────
146
- describe("Readiness polling (bootstrap.ts)", () => {
147
- it("does NOT spawn processes (poll-only, no child_process)", () => {
148
- const content = readFileSync(resolve(process.cwd(), "src/daemon/bootstrap.ts"), "utf-8");
149
- expect(content).not.toContain("detached: true");
150
- expect(content).not.toContain("child_process");
151
- expect(content).not.toContain("spawn(");
152
- });
153
- it("polls at 100ms intervals with 5s timeout", () => {
154
- const content = readFileSync(resolve(process.cwd(), "src/daemon/bootstrap.ts"), "utf-8");
155
- expect(content).toContain("POLL_INTERVAL_MS = 100");
156
- expect(content).toContain("WAIT_TIMEOUT_MS = 5_000");
157
- });
158
- it("uses probeDaemon for fast-path check", () => {
159
- const content = readFileSync(resolve(process.cwd(), "src/daemon/bootstrap.ts"), "utf-8");
160
- expect(content).toContain("probeDaemon(sock)");
161
- });
162
- it("exports waitForDaemonReady as primary + ensureDaemonRunning alias", () => {
163
- const content = readFileSync(resolve(process.cwd(), "src/daemon/bootstrap.ts"), "utf-8");
164
- expect(content).toContain("export async function waitForDaemonReady");
165
- expect(content).toContain("export const ensureDaemonRunning = waitForDaemonReady");
166
- });
167
- });
168
- // ── Protocol integration tests ─────────────────────────────────────
169
- describe("Client protocol integration", () => {
170
- it("client methods use correct cmd values", () => {
171
- const content = readFileSync(resolve(process.cwd(), "src/daemon/client.ts"), "utf-8");
172
- expect(content).toContain('cmd: "ensure"');
173
- expect(content).toContain('cmd: "connect"');
174
- expect(content).toContain('cmd: "disconnect"');
175
- expect(content).toContain('cmd: "activity"');
176
- expect(content).toContain('cmd: "status"');
177
- });
178
- it("sendRequest uses newline-delimited JSON", () => {
179
- const content = readFileSync(resolve(process.cwd(), "src/daemon/client.ts"), "utf-8");
180
- // Sends JSON with newline delimiter
181
- expect(content).toContain("JSON.stringify(request)}\\n");
182
- // Parses response up to newline
183
- expect(content).toContain('buffer.indexOf("\\n")');
184
- });
185
- it("sendRequest has configurable timeout", () => {
186
- const content = readFileSync(resolve(process.cwd(), "src/daemon/client.ts"), "utf-8");
187
- expect(content).toContain("timeoutMs = 30_000");
188
- });
189
- });
190
- // ── Race safety tests ──────────────────────────────────────────────
191
- describe("Race safety", () => {
192
- it("bootstrap.ts is poll-only (no file locks, no spawn)", () => {
193
- const content = readFileSync(resolve(process.cwd(), "src/daemon/bootstrap.ts"), "utf-8");
194
- expect(content).not.toContain("lockFile");
195
- expect(content).not.toContain("flock");
196
- expect(content).not.toContain("spawn(");
197
- expect(content).toContain("probeDaemon(sock)");
198
- });
199
- });
200
- // ── Error handling tests ───────────────────────────────────────────
201
- describe("Error handling", () => {
202
- it("mcpBoot exits 1 when no process found (no auto-spawn)", () => {
203
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
204
- expect(content).toContain("No unerr process found");
205
- expect(content).toContain("unerr daemon initialize");
206
- expect(content).toContain("process.exit(1)");
207
- });
208
- it("mcpBoot exits 1 when repo not registered", () => {
209
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
210
- expect(content).toContain("Repo not registered with unerrd");
211
- expect(content).toContain("unerr install <agent>");
212
- });
213
- it("mcpBoot exits 1 on ensure failure", () => {
214
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
215
- expect(content).toContain("Failed to ensure repo process");
216
- });
217
- it("mcpBoot handles daemon_dead from bridge", () => {
218
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/cli.ts"), "utf-8");
219
- expect(content).toContain('"daemon_dead"');
220
- expect(content).toContain("idle-stopped or crashed");
221
- });
222
- });
@@ -1,202 +0,0 @@
1
- /**
2
- * DM-4: Unified Dashboard tests.
3
- *
4
- * Tests cover:
5
- * - Daemon API module (api.ts): exports, route structure, isolation
6
- * - Router: global routes, repo-prefixed routes, backward compatibility
7
- * - Repo context provider
8
- * - UI pages: AllReposPage, DaemonPage exist and export components
9
- * - AppShell: daemon mode + standalone mode
10
- * - daemon.ts integration: HTTP server startup
11
- */
12
- import { readFileSync } from "node:fs";
13
- import { resolve } from "node:path";
14
- import { describe, expect, it } from "vitest";
15
- // ── Daemon API module tests ────────────────────────────────────────
16
- describe("Daemon API (api.ts)", () => {
17
- it("exports startDaemonApi", async () => {
18
- const api = await import("../daemon/api.js");
19
- expect(typeof api.startDaemonApi).toBe("function");
20
- });
21
- it("imports only from daemon/, server/ and node builtins + hono", () => {
22
- const content = readFileSync(resolve(process.cwd(), "src/daemon/api.ts"), "utf-8");
23
- const forbidden = [
24
- /from\s+["']\.\.\/intelligence\//,
25
- /from\s+["']\.\.\/behaviors\//,
26
- /from\s+["']\.\.\/tracking\//,
27
- ];
28
- for (const pattern of forbidden) {
29
- expect(content).not.toMatch(pattern);
30
- }
31
- expect(content).toContain('from "hono"');
32
- expect(content).toContain("process-manager");
33
- expect(content).toContain("registry");
34
- });
35
- it("defines all required API routes", () => {
36
- const content = readFileSync(resolve(process.cwd(), "src/daemon/api.ts"), "utf-8");
37
- expect(content).toContain('"/api/daemon"');
38
- expect(content).toContain('"/api/repos"');
39
- expect(content).toContain('"/api/repos/aggregate"');
40
- expect(content).toContain('"/api/repo/:label/*"');
41
- });
42
- it("serves SPA for non-API routes", () => {
43
- const content = readFileSync(resolve(process.cwd(), "src/daemon/api.ts"), "utf-8");
44
- expect(content).toContain("serveStatic");
45
- expect(content).toContain("spaIndex");
46
- expect(content).toContain("c.html(spaHtml)");
47
- });
48
- it("uses port 9847 for daemon dashboard", () => {
49
- const content = readFileSync(resolve(process.cwd(), "src/daemon/api.ts"), "utf-8");
50
- expect(content).toContain("DAEMON_PORT = 9847");
51
- });
52
- });
53
- // ── Router tests ───────────────────────────────────────────────────
54
- describe("Router (router.ts)", () => {
55
- it("defines GlobalRouteId and RepoRouteId types", () => {
56
- const content = readFileSync(resolve(process.cwd(), "src/ui/lib/router.ts"), "utf-8");
57
- expect(content).toContain("GlobalRouteId");
58
- expect(content).toContain("RepoRouteId");
59
- expect(content).toContain('"all-repos"');
60
- expect(content).toContain('"daemon"');
61
- });
62
- it("exports useParsedRoute and useRepoLabel hooks", () => {
63
- const content = readFileSync(resolve(process.cwd(), "src/ui/lib/router.ts"), "utf-8");
64
- expect(content).toContain("export function useParsedRoute");
65
- expect(content).toContain("export function useRepoLabel");
66
- });
67
- it("parseHash handles repo/<label>/<route> format", () => {
68
- const content = readFileSync(resolve(process.cwd(), "src/ui/lib/router.ts"), "utf-8");
69
- expect(content).toContain('first === "repo"');
70
- expect(content).toContain("segments[1]");
71
- expect(content).toContain("segments[2]");
72
- });
73
- it("navigateRoute generates repo-prefixed hashes", () => {
74
- const content = readFileSync(resolve(process.cwd(), "src/ui/lib/router.ts"), "utf-8");
75
- expect(content).toContain("`#/repo/${repoLabel}`");
76
- expect(content).toContain("`#/repo/${repoLabel}/${next}`");
77
- });
78
- it("preserves backward compatibility with bare routes", () => {
79
- const content = readFileSync(resolve(process.cwd(), "src/ui/lib/router.ts"), "utf-8");
80
- // Bare routes still work (standalone mode)
81
- expect(content).toContain("matchRepoRoute(first)");
82
- expect(content).toContain("repoLabel: null");
83
- });
84
- });
85
- // ── Repo context provider tests ────────────────────────────────────
86
- describe("Repo context (repo-context.ts)", () => {
87
- it("exports RepoContext and useRepoContext", () => {
88
- const content = readFileSync(resolve(process.cwd(), "src/ui/lib/repo-context.ts"), "utf-8");
89
- expect(content).toContain("export const RepoContext");
90
- expect(content).toContain("export function useRepoContext");
91
- expect(content).toContain("createContext");
92
- });
93
- it("includes isDaemonMode and apiBase fields", () => {
94
- const content = readFileSync(resolve(process.cwd(), "src/ui/lib/repo-context.ts"), "utf-8");
95
- expect(content).toContain("isDaemonMode: boolean");
96
- expect(content).toContain("apiBase: string");
97
- expect(content).toContain("label: string | null");
98
- });
99
- });
100
- // ── UI pages exist ─────────────────────────────────────────────────
101
- describe("UI pages", () => {
102
- it("AllReposPage.tsx exists and exports component", () => {
103
- const content = readFileSync(resolve(process.cwd(), "src/ui/pages/AllReposPage.tsx"), "utf-8");
104
- expect(content).toContain("export function AllReposPage");
105
- expect(content).toContain("/api/repos");
106
- expect(content).toContain("/api/repos/aggregate");
107
- });
108
- it("AllReposPage shows aggregated metrics", () => {
109
- const content = readFileSync(resolve(process.cwd(), "src/ui/pages/AllReposPage.tsx"), "utf-8");
110
- expect(content).toContain("totalEntities");
111
- expect(content).toContain("totalMemoryMb");
112
- expect(content).toContain("Tokens Saved");
113
- expect(content).toContain("Tool Calls");
114
- });
115
- it("DaemonPage.tsx exists and exports component", () => {
116
- const content = readFileSync(resolve(process.cwd(), "src/ui/pages/DaemonPage.tsx"), "utf-8");
117
- expect(content).toContain("export function DaemonPage");
118
- expect(content).toContain("/api/daemon");
119
- expect(content).toContain("Supervisor");
120
- });
121
- it("DaemonPage shows process table", () => {
122
- const content = readFileSync(resolve(process.cwd(), "src/ui/pages/DaemonPage.tsx"), "utf-8");
123
- expect(content).toContain("Managed Processes");
124
- expect(content).toContain("repo.pid");
125
- expect(content).toContain("repo.memory");
126
- });
127
- });
128
- // ── AppShell updates ───────────────────────────────────────────────
129
- describe("AppShell (layout)", () => {
130
- it("supports daemon mode with repo switcher", () => {
131
- const content = readFileSync(resolve(process.cwd(), "src/ui/components/layout/AppShell.tsx"), "utf-8");
132
- expect(content).toContain("isDaemonMode");
133
- expect(content).toContain("GLOBAL_NAV");
134
- expect(content).toContain("REPO_NAV");
135
- expect(content).toContain("repoLabel");
136
- });
137
- it("shows status dots for repo list", () => {
138
- const content = readFileSync(resolve(process.cwd(), "src/ui/components/layout/AppShell.tsx"), "utf-8");
139
- expect(content).toContain("bg-success");
140
- expect(content).toContain("bg-error");
141
- expect(content).toContain("r.status");
142
- });
143
- it("falls back to standalone nav when not in daemon mode", () => {
144
- const content = readFileSync(resolve(process.cwd(), "src/ui/components/layout/AppShell.tsx"), "utf-8");
145
- expect(content).toContain("{!isDaemonMode && (");
146
- });
147
- });
148
- // ── App.tsx integration ────────────────────────────────────────────
149
- describe("App.tsx integration", () => {
150
- it("detects daemon mode via /api/daemon", () => {
151
- const content = readFileSync(resolve(process.cwd(), "src/ui/App.tsx"), "utf-8");
152
- expect(content).toContain("isDaemonMode");
153
- expect(content).toContain('["daemon", "info"]');
154
- expect(content).toContain("/api/daemon");
155
- });
156
- it("renders global pages (AllReposPage, DaemonPage)", () => {
157
- const content = readFileSync(resolve(process.cwd(), "src/ui/App.tsx"), "utf-8");
158
- expect(content).toContain("<AllReposPage");
159
- expect(content).toContain("<DaemonPage");
160
- expect(content).toContain('case "all-repos"');
161
- expect(content).toContain('case "daemon"');
162
- });
163
- it("provides RepoContext", () => {
164
- const content = readFileSync(resolve(process.cwd(), "src/ui/App.tsx"), "utf-8");
165
- expect(content).toContain("RepoContext.Provider");
166
- expect(content).toContain("repoCtx");
167
- expect(content).toContain("apiBase");
168
- });
169
- it("passes repos list and repoLabel to AppShell", () => {
170
- const content = readFileSync(resolve(process.cwd(), "src/ui/App.tsx"), "utf-8");
171
- expect(content).toContain("repos={repos}");
172
- expect(content).toContain("repoLabel={parsed.repoLabel}");
173
- expect(content).toContain("isDaemonMode={isDaemonMode}");
174
- });
175
- });
176
- // ── daemon.ts HTTP integration ─────────────────────────────────────
177
- describe("daemon.ts HTTP integration", () => {
178
- it("starts the dashboard API server", () => {
179
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/daemon.ts"), "utf-8");
180
- expect(content).toContain("startDaemonApi");
181
- expect(content).toContain("apiHandle");
182
- expect(content).toContain("http://localhost");
183
- });
184
- it("closes HTTP server on shutdown", () => {
185
- const content = readFileSync(resolve(process.cwd(), "src/entrypoints/daemon.ts"), "utf-8");
186
- expect(content).toContain("apiHandle?.close()");
187
- });
188
- });
189
- // ── Proxy route design ─────────────────────────────────────────────
190
- describe("Proxy route design", () => {
191
- it("api.ts reads server.json for per-repo HTTP port", () => {
192
- const content = readFileSync(resolve(process.cwd(), "src/daemon/api.ts"), "utf-8");
193
- expect(content).toContain("server.json");
194
- expect(content).toContain("repoPort");
195
- expect(content).toContain("proxyToRepoHttp");
196
- });
197
- it("strips /api/repo/:label prefix before proxying", () => {
198
- const content = readFileSync(resolve(process.cwd(), "src/daemon/api.ts"), "utf-8");
199
- expect(content).toContain("c.req.path.replace");
200
- expect(content).toContain("remaining");
201
- });
202
- });
@@ -1,240 +0,0 @@
1
- /**
2
- * Tests for the daemon registry (DM-1 Tasks 2-5, 9).
3
- *
4
- * Uses temp dirs for both ~/.unerr (global) and fake repo paths.
5
- * Mocks `os.homedir()` to isolate from real user state.
6
- */
7
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
8
- import { tmpdir } from "node:os";
9
- import { join } from "node:path";
10
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
- // Mock homedir before importing registry (module-level side effect via globalDir)
12
- const testHome = join(tmpdir(), `unerr-test-home-${process.pid}-${Date.now()}`);
13
- vi.mock("node:os", async () => {
14
- const actual = await vi.importActual("node:os");
15
- return { ...actual, homedir: () => testHome };
16
- });
17
- import { addRepo, deriveLabel, detectChildConflicts, detectParentConflict, findRepo, listRepos, readNeedsInput, readRegistry, removeRepo, updateRepoSettings, writeNeedsInput, writeRegistry, } from "../daemon/registry.js";
18
- let testNum = 0;
19
- function makeRepo(name) {
20
- testNum++;
21
- const p = join(testHome, `test-${testNum}`, name);
22
- mkdirSync(p, { recursive: true });
23
- return p;
24
- }
25
- beforeEach(() => {
26
- mkdirSync(testHome, { recursive: true });
27
- // Reset registry for test isolation
28
- writeRegistry({ version: 1, repos: [] });
29
- });
30
- afterEach(() => {
31
- vi.restoreAllMocks();
32
- });
33
- describe("Registry CRUD", () => {
34
- it("adds a new repo and writes repos.json", () => {
35
- const repoPath = makeRepo("my-app");
36
- const result = addRepo(repoPath);
37
- expect(result.ok).toBe(true);
38
- if (!result.ok)
39
- return;
40
- expect(result.created).toBe(true);
41
- expect(result.entry.path).toBe(repoPath);
42
- expect(result.entry.label).toBe("my-app");
43
- expect(result.entry.idleTimeout).toBe(1800);
44
- const reg = readRegistry();
45
- expect(reg.repos).toHaveLength(1);
46
- expect(reg.repos[0].path).toBe(repoPath);
47
- });
48
- it("is idempotent — adding the same path twice returns existing entry", () => {
49
- const repoPath = makeRepo("idempotent");
50
- const first = addRepo(repoPath);
51
- expect(first.ok).toBe(true);
52
- if (!first.ok)
53
- return;
54
- expect(first.created).toBe(true);
55
- const second = addRepo(repoPath);
56
- expect(second.ok).toBe(true);
57
- if (!second.ok)
58
- return;
59
- expect(second.created).toBe(false);
60
- expect(second.entry.path).toBe(repoPath);
61
- });
62
- it("removes a repo", () => {
63
- const repoPath = makeRepo("to-remove");
64
- addRepo(repoPath);
65
- expect(listRepos()).toHaveLength(1);
66
- const removed = removeRepo(repoPath);
67
- expect(removed).toBe(true);
68
- expect(listRepos()).toHaveLength(0);
69
- });
70
- it("returns false when removing non-existent repo", () => {
71
- expect(removeRepo("/nonexistent")).toBe(false);
72
- });
73
- it("findRepo by path", () => {
74
- const repoPath = makeRepo("findable");
75
- addRepo(repoPath);
76
- const found = findRepo(repoPath);
77
- expect(found).toBeDefined();
78
- expect(found.path).toBe(repoPath);
79
- });
80
- it("findRepo by label", () => {
81
- const repoPath = makeRepo("by-label");
82
- addRepo(repoPath);
83
- const found = findRepo("by-label");
84
- expect(found).toBeDefined();
85
- expect(found.path).toBe(repoPath);
86
- });
87
- it("listRepos returns all registered repos", () => {
88
- addRepo(makeRepo("repo-a"));
89
- addRepo(makeRepo("repo-b"));
90
- addRepo(makeRepo("repo-c"));
91
- expect(listRepos()).toHaveLength(3);
92
- });
93
- });
94
- describe("Settings", () => {
95
- it("add with settings writes to registry and local config", () => {
96
- const repoPath = makeRepo("with-settings");
97
- const result = addRepo(repoPath, {
98
- idleTimeout: 60,
99
- javaBuildTool: "Gradle",
100
- });
101
- expect(result.ok).toBe(true);
102
- if (!result.ok)
103
- return;
104
- expect(result.entry.idleTimeout).toBe(60);
105
- expect(result.entry.settings.javaBuildTool).toBe("Gradle");
106
- // Verify local config mirror
107
- const localConfig = JSON.parse(readFileSync(join(repoPath, ".unerr", "config.json"), "utf-8"));
108
- expect(localConfig.javaBuildTool).toBe("Gradle");
109
- });
110
- it("updateRepoSettings patches and mirrors", () => {
111
- const repoPath = makeRepo("update-settings");
112
- addRepo(repoPath, { javaBuildTool: "Maven" });
113
- const updated = updateRepoSettings(repoPath, {
114
- javaBuildTool: "Gradle",
115
- idleTimeout: 120,
116
- });
117
- expect(updated).not.toBeNull();
118
- expect(updated.settings.javaBuildTool).toBe("Gradle");
119
- expect(updated.idleTimeout).toBe(120);
120
- // Verify persisted
121
- const reg = readRegistry();
122
- const persisted = reg.repos.find((r) => r.path === repoPath);
123
- expect(persisted.settings.javaBuildTool).toBe("Gradle");
124
- expect(persisted.idleTimeout).toBe(120);
125
- // Verify local mirror
126
- const localConfig = JSON.parse(readFileSync(join(repoPath, ".unerr", "config.json"), "utf-8"));
127
- expect(localConfig.javaBuildTool).toBe("Gradle");
128
- });
129
- it("updateRepoSettings returns null for unknown repo", () => {
130
- expect(updateRepoSettings("/nonexistent", { idleTimeout: 60 })).toBeNull();
131
- });
132
- });
133
- describe("Parent detection", () => {
134
- it("detects registered parent directory", () => {
135
- const parent = makeRepo("monorepo");
136
- const child = join(parent, "packages", "frontend");
137
- mkdirSync(child, { recursive: true });
138
- addRepo(parent);
139
- const repos = listRepos();
140
- const conflict = detectParentConflict(child, repos);
141
- expect(conflict).toBe(parent);
142
- });
143
- it("returns null when no parent conflict", () => {
144
- const a = makeRepo("standalone-a");
145
- const b = makeRepo("standalone-b");
146
- addRepo(a);
147
- const repos = listRepos();
148
- expect(detectParentConflict(b, repos)).toBeNull();
149
- });
150
- it("addRepo blocks registration when parent exists", () => {
151
- const parent = makeRepo("parent-block");
152
- const child = join(parent, "sub", "project");
153
- mkdirSync(child, { recursive: true });
154
- addRepo(parent);
155
- const result = addRepo(child);
156
- expect(result.ok).toBe(false);
157
- if (result.ok)
158
- return;
159
- expect(result.parentConflict).toBe(parent);
160
- });
161
- it("addRepo allows with skipParentCheck", () => {
162
- const parent = makeRepo("parent-allow");
163
- const child = join(parent, "sub", "project");
164
- mkdirSync(child, { recursive: true });
165
- addRepo(parent);
166
- const result = addRepo(child, {}, { skipParentCheck: true });
167
- expect(result.ok).toBe(true);
168
- });
169
- });
170
- describe("Child detection", () => {
171
- it("detects registered child directories", () => {
172
- const parent = makeRepo("new-parent");
173
- const childA = join(parent, "packages", "a");
174
- const childB = join(parent, "packages", "b");
175
- mkdirSync(childA, { recursive: true });
176
- mkdirSync(childB, { recursive: true });
177
- addRepo(childA, {}, { skipParentCheck: true });
178
- addRepo(childB, {}, { skipParentCheck: true });
179
- const repos = listRepos();
180
- const conflicts = detectChildConflicts(parent, repos);
181
- expect(conflicts).toHaveLength(2);
182
- expect(conflicts).toContain(childA);
183
- expect(conflicts).toContain(childB);
184
- });
185
- it("returns empty when no child conflicts", () => {
186
- const a = makeRepo("no-child-a");
187
- addRepo(a);
188
- const b = makeRepo("no-child-b");
189
- const repos = listRepos();
190
- expect(detectChildConflicts(b, repos)).toHaveLength(0);
191
- });
192
- });
193
- describe("Label generation", () => {
194
- it("uses basename for first repo", () => {
195
- expect(deriveLabel("/users/dev/my-app", [])).toBe("my-app");
196
- });
197
- it("uses parent-basename on collision", () => {
198
- const existing = [{ label: "my-app" }];
199
- expect(deriveLabel("/users/work/my-app", existing)).toBe("work-my-app");
200
- });
201
- it("uses numeric suffix on double collision", () => {
202
- const existing = [
203
- { label: "my-app" },
204
- { label: "work-my-app" },
205
- ];
206
- expect(deriveLabel("/other/work/my-app", existing)).toBe("my-app-2");
207
- });
208
- });
209
- describe("Needs-input signals", () => {
210
- it("write and read round-trip", () => {
211
- const repoPath = makeRepo("needs-input-test");
212
- const signals = [
213
- {
214
- type: "needs_input",
215
- key: "javaBuildTool",
216
- auto: "Gradle",
217
- alternatives: ["Maven"],
218
- reason: "gradlew wrapper present",
219
- },
220
- ];
221
- writeNeedsInput(repoPath, signals);
222
- const read = readNeedsInput(repoPath);
223
- expect(read).toEqual(signals);
224
- });
225
- it("returns empty array when no signals file", () => {
226
- expect(readNeedsInput("/nonexistent")).toEqual([]);
227
- });
228
- });
229
- describe("Ensures .unerr/ directory", () => {
230
- it("creates .unerr/ on add if missing", () => {
231
- const repoPath = makeRepo("ensure-dir");
232
- addRepo(repoPath);
233
- expect(existsSync(join(repoPath, ".unerr"))).toBe(true);
234
- });
235
- it("creates .unerr/config.json with settings", () => {
236
- const repoPath = makeRepo("config-file");
237
- addRepo(repoPath, { javaBuildTool: "Maven" });
238
- expect(existsSync(join(repoPath, ".unerr", "config.json"))).toBe(true);
239
- });
240
- });