@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,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
|
-
});
|