@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,190 +0,0 @@
1
- /**
2
- * Sprint L5.2 — Network Boundary Contract Tests
3
- *
4
- * Static contract tests verifying that every cloud-dependent service
5
- * is structurally disabled (null / NullProxy) in Local Mode. These
6
- * tests catch regressions at the contract level — even if a future
7
- * code change introduces a network call, these tests will catch it.
8
- *
9
- * A security-conscious user reading these tests should be convinced
10
- * that their data never leaves their machine in Local Mode.
11
- *
12
- * CONTRACT: TL-1, TL-3, TL-15
13
- */
14
- import { afterEach, describe, expect, it, vi } from "vitest";
15
- import { getBlockedCount, resetBlockedCount, seal, unseal, } from "../proxy/network-firewall.js";
16
- // ── Shared Mock Graph ──────────────────────────────────────────────
17
- function createMockGraph() {
18
- const mockEntity = {
19
- key: "fn::test",
20
- kind: "function",
21
- name: "testFn",
22
- file_path: "src/test.ts",
23
- start_line: 1,
24
- end_line: 10,
25
- signature: "(): void",
26
- body: "function testFn() {}",
27
- fan_in: 2,
28
- fan_out: 1,
29
- risk_level: "normal",
30
- };
31
- const mockDb = {
32
- run: vi.fn((_query) => ({
33
- rows: [],
34
- })),
35
- };
36
- return {
37
- db: mockDb,
38
- getEntity: vi.fn().mockReturnValue(mockEntity),
39
- getCallersOf: vi.fn().mockReturnValue([]),
40
- getCalleesOf: vi.fn().mockReturnValue([]),
41
- getEntitiesByFile: vi.fn().mockReturnValue([mockEntity]),
42
- searchEntities: vi.fn().mockReturnValue([mockEntity]),
43
- getImports: vi.fn().mockReturnValue([]),
44
- hasRules: vi.fn().mockReturnValue(true),
45
- getRules: vi.fn().mockReturnValue([]),
46
- getPatterns: vi
47
- .fn()
48
- .mockReturnValue([
49
- { name: "test-pattern", type: "naming", adherence: 0.9 },
50
- ]),
51
- hasJustifications: vi.fn().mockReturnValue(true),
52
- getBusinessContext: vi.fn().mockReturnValue(null),
53
- getConventions: vi.fn().mockReturnValue([]),
54
- getCrossBoundaryLinks: vi.fn().mockReturnValue([]),
55
- getCriticalNodes: vi.fn().mockReturnValue([]),
56
- getDriftEntitiesForFile: vi.fn().mockReturnValue([]),
57
- upsertDriftEntity: vi.fn(),
58
- removeDriftEntity: vi.fn(),
59
- clearDriftOverlay: vi.fn(),
60
- getDriftSummary: vi
61
- .fn()
62
- .mockReturnValue({ added: 0, modified: 0, deleted: 0, total: 0 }),
63
- healthCheck: vi.fn().mockReturnValue({ status: "ok", latencyMs: 1 }),
64
- isLoaded: vi.fn().mockReturnValue(true),
65
- loadSnapshot: vi.fn(),
66
- loadRules: vi.fn(),
67
- loadPatterns: vi.fn(),
68
- loadJustifications: vi.fn(),
69
- applyDelta: vi.fn().mockReturnValue({
70
- applied: 0,
71
- deleted: 0,
72
- edges: 0,
73
- justifications: 0,
74
- overlayExpired: 0,
75
- }),
76
- getLocalProjectStats: vi.fn().mockReturnValue({
77
- fileCount: 100,
78
- entityCount: 500,
79
- edgeCount: 1200,
80
- communityCount: 10,
81
- ruleCount: 5,
82
- }),
83
- getDeepDiveProjectState: vi.fn().mockReturnValue("none"),
84
- persistCorrections: vi.fn(),
85
- getRuleHealthSummary: vi.fn().mockReturnValue(null),
86
- getRuleExceptions: vi.fn().mockReturnValue([]),
87
- };
88
- }
89
- // ── Test Suite ─────────────────────────────────────────────────────
90
- describe("Network Boundary Contracts (L5.2)", () => {
91
- afterEach(() => {
92
- unseal();
93
- resetBlockedCount();
94
- });
95
- it("local-routed tools resolve without network calls", async () => {
96
- seal();
97
- const { QueryRouter } = await import("../intelligence/query-router.js");
98
- const graph = createMockGraph();
99
- const router = new QueryRouter(graph);
100
- router.setMode("local", "Local Mode");
101
- await router.execute("get_function", { key: "fn::test" });
102
- await router.execute("search_code", { query: "test" });
103
- await router.execute("get_callers", { key: "fn::test" });
104
- await router.execute("get_conventions", {});
105
- expect(getBlockedCount()).toBe(0);
106
- });
107
- it("Local Mode QueryRouter handles errors locally (TL-15)", async () => {
108
- seal();
109
- const { QueryRouter } = await import("../intelligence/query-router.js");
110
- const graph = createMockGraph();
111
- graph.getEntity = vi.fn().mockImplementation(() => {
112
- throw new Error("Simulated local failure");
113
- });
114
- const router = new QueryRouter(graph);
115
- router.setMode("local", "Local Mode");
116
- const result = await router.execute("get_function", { key: "nonexistent" });
117
- expect(result._meta.source).toBe("local");
118
- expect(result.content).toHaveProperty("error");
119
- expect(getBlockedCount()).toBe(0);
120
- });
121
- it("NetworkFirewall rejects app.unerr.dev when sealed", async () => {
122
- seal();
123
- const result = await globalThis
124
- .fetch("https://app.unerr.dev/api/repos/test/profile")
125
- .catch((e) => e);
126
- expect(result).toBeInstanceOf(Error);
127
- expect(result.message).toContain("[NetworkFirewall]");
128
- expect(getBlockedCount()).toBe(1);
129
- });
130
- it("NetworkFirewall rejects api.anthropic.com when sealed (no allowlist)", async () => {
131
- seal();
132
- const result = await globalThis
133
- .fetch("https://api.anthropic.com/v1/messages")
134
- .catch((e) => e);
135
- expect(result).toBeInstanceOf(Error);
136
- expect(result.message).toContain("[NetworkFirewall]");
137
- expect(getBlockedCount()).toBe(1);
138
- });
139
- it("NetworkFirewall allows localhost (Ollama, LM Studio) when sealed", async () => {
140
- seal();
141
- const localhostUrls = [
142
- "http://localhost:11434/api/tags",
143
- "http://127.0.0.1:1234/v1/models",
144
- "http://localhost:8080/health",
145
- ];
146
- for (const url of localhostUrls) {
147
- const result = await globalThis.fetch(url).catch((e) => e);
148
- if (result instanceof Error) {
149
- expect(result.message).not.toContain("[NetworkFirewall]");
150
- }
151
- }
152
- expect(getBlockedCount()).toBe(0);
153
- });
154
- it("local-routed tools resolve from CozoDB with zero network calls", async () => {
155
- seal();
156
- const { QueryRouter } = await import("../intelligence/query-router.js");
157
- const graph = createMockGraph();
158
- const router = new QueryRouter(graph);
159
- router.setMode("local", "Local Mode");
160
- const localOnlyTools = [
161
- { name: "get_function", args: { key: "fn::test" } },
162
- { name: "get_class", args: { key: "fn::test" } },
163
- { name: "get_file", args: { key: "src/test.ts" } },
164
- { name: "get_callers", args: { key: "fn::test" } },
165
- { name: "get_callees", args: { key: "fn::test" } },
166
- { name: "get_imports", args: { file_path: "src/test.ts" } },
167
- { name: "search_code", args: { query: "test" } },
168
- { name: "get_rules", args: {} },
169
- { name: "get_business_context", args: { key: "fn::test" } },
170
- { name: "get_conventions", args: {} },
171
- { name: "get_cross_boundary_links", args: {} },
172
- { name: "get_critical_nodes", args: {} },
173
- ];
174
- for (const tool of localOnlyTools) {
175
- const result = await router.execute(tool.name, tool.args);
176
- expect(result._meta.source).toBe("local");
177
- }
178
- expect(getBlockedCount()).toBe(0);
179
- });
180
- it("get_project_stats returns from local CozoDB (not cloud API)", async () => {
181
- seal();
182
- const { QueryRouter } = await import("../intelligence/query-router.js");
183
- const graph = createMockGraph();
184
- const router = new QueryRouter(graph);
185
- router.setMode("local", "Local Mode");
186
- const result = await router.execute("get_project_stats", {});
187
- expect(result._meta.source).toBe("local");
188
- expect(getBlockedCount()).toBe(0);
189
- });
190
- });
@@ -1,112 +0,0 @@
1
- /**
2
- * Tests for Sprint L8.4 — Network Firewall allowlist & anthropic-direct exception.
3
- */
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { addAllowedHost, addAllowedUrl, getBlockedCount, isSealed, resetBlockedCount, seal, unseal, } from "../proxy/network-firewall.js";
6
- describe("NetworkFirewall (L8.4)", () => {
7
- beforeEach(() => {
8
- unseal();
9
- resetBlockedCount();
10
- });
11
- afterEach(() => {
12
- unseal();
13
- resetBlockedCount();
14
- });
15
- it("addAllowedHost before seal allows that host", async () => {
16
- addAllowedHost("api.anthropic.com");
17
- seal();
18
- expect(isSealed()).toBe(true);
19
- // Should NOT block api.anthropic.com
20
- const resp = await globalThis
21
- .fetch("https://api.anthropic.com/v1/messages")
22
- .catch((e) => e);
23
- // If it's a network error (server not reachable), that's fine — it wasn't BLOCKED
24
- if (resp instanceof Error) {
25
- expect(resp.message).not.toContain("[NetworkFirewall]");
26
- }
27
- expect(getBlockedCount()).toBe(0);
28
- });
29
- it("addAllowedUrl extracts hostname and allows it", async () => {
30
- addAllowedUrl("http://localhost:11434");
31
- seal();
32
- // localhost is always allowed, but this tests the URL parsing path
33
- expect(isSealed()).toBe(true);
34
- expect(getBlockedCount()).toBe(0);
35
- });
36
- it("blocks non-allowlisted hosts after seal", async () => {
37
- seal();
38
- const result = await globalThis
39
- .fetch("https://app.unerr.dev/api/health")
40
- .catch((e) => e);
41
- expect(result).toBeInstanceOf(Error);
42
- expect(result.message).toContain("[NetworkFirewall]");
43
- expect(result.message).toContain("app.unerr.dev");
44
- expect(getBlockedCount()).toBe(1);
45
- });
46
- it("app.unerr.dev is NEVER in allowlist", async () => {
47
- addAllowedHost("api.anthropic.com");
48
- seal();
49
- const result = await globalThis
50
- .fetch("https://app.unerr.dev/api/health")
51
- .catch((e) => e);
52
- expect(result).toBeInstanceOf(Error);
53
- expect(result.message).toContain("[NetworkFirewall]");
54
- expect(getBlockedCount()).toBe(1);
55
- });
56
- it("addAllowedHost throws after seal (immutability)", () => {
57
- seal();
58
- expect(() => addAllowedHost("evil.com")).toThrow("firewall is already sealed");
59
- });
60
- it("addAllowedUrl throws after seal (immutability)", () => {
61
- seal();
62
- expect(() => addAllowedUrl("https://evil.com")).toThrow("firewall is already sealed");
63
- });
64
- it("localhost always allowed without explicit allowlist", async () => {
65
- seal();
66
- const result = await globalThis
67
- .fetch("http://localhost:11434/api/tags")
68
- .catch((e) => e);
69
- // Should not be a firewall error (may be ECONNREFUSED if Ollama isn't running)
70
- if (result instanceof Error) {
71
- expect(result.message).not.toContain("[NetworkFirewall]");
72
- }
73
- expect(getBlockedCount()).toBe(0);
74
- });
75
- it("seal() with allowedBaseUrls still works (backwards compat)", async () => {
76
- seal(["http://localhost:1234"]);
77
- expect(isSealed()).toBe(true);
78
- expect(getBlockedCount()).toBe(0);
79
- });
80
- it("anthropic-direct: api.anthropic.com allowed, other hosts blocked", async () => {
81
- addAllowedHost("api.anthropic.com");
82
- seal();
83
- // Anthropic allowed
84
- const anthropicResult = await globalThis
85
- .fetch("https://api.anthropic.com/v1/messages")
86
- .catch((e) => e);
87
- if (anthropicResult instanceof Error) {
88
- expect(anthropicResult.message).not.toContain("[NetworkFirewall]");
89
- }
90
- // Random host blocked
91
- const otherResult = await globalThis
92
- .fetch("https://evil.example.com/steal")
93
- .catch((e) => e);
94
- expect(otherResult).toBeInstanceOf(Error);
95
- expect(otherResult.message).toContain("[NetworkFirewall]");
96
- expect(getBlockedCount()).toBe(1);
97
- });
98
- it("unseal clears allowlist and restores fetch", () => {
99
- addAllowedHost("api.anthropic.com");
100
- seal();
101
- expect(isSealed()).toBe(true);
102
- unseal();
103
- expect(isSealed()).toBe(false);
104
- // After unseal + re-seal, anthropic should be blocked (allowlist cleared)
105
- seal();
106
- // No addAllowedHost this time — anthropic should be blocked
107
- void globalThis
108
- .fetch("https://api.anthropic.com/v1/messages")
109
- .catch(() => { });
110
- expect(getBlockedCount()).toBe(1);
111
- });
112
- });
@@ -1,160 +0,0 @@
1
- /**
2
- * Nudge clarity invariants — block any regression to the abstract-placeholder
3
- * or hedge-verb patterns documented in CLAUDE.md "Writing nudges and hints".
4
- *
5
- * Anything the agent reads as advisory (Consider/Verify/Review/Check) or
6
- * abstract (`:N`, `<name>`) creates retry loops or silent drops. These tests
7
- * fail loud if either pattern returns.
8
- */
9
- import { readFileSync } from "node:fs";
10
- import { join } from "node:path";
11
- import { describe, expect, it } from "vitest";
12
- import { SIGNAL_PREFIX_LEGEND } from "../proxy/response-envelope.js";
13
- const HEDGE_VERBS = [
14
- /\bConsider\b/,
15
- /\bVerify\b/,
16
- /\bReview\b/,
17
- /\bCheck\b/, // followed by space (avoid "Checked", "checkbox" in identifiers)
18
- /\bmay want to\b/i,
19
- ];
20
- const ROOT = join(__dirname, "..");
21
- function readSource(rel) {
22
- return readFileSync(join(ROOT, rel), "utf-8");
23
- }
24
- /**
25
- * Extract every `action: "..."` string literal from a source file. Skips
26
- * action: undefined and action: variable-reference forms.
27
- */
28
- function extractActionLiterals(src) {
29
- const out = [];
30
- // Match: action: "..." or action: `...`
31
- const re = /action:\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|`([^`\\]*(?:\\.[^`\\]*)*)`)/g;
32
- let m;
33
- m = re.exec(src);
34
- while (m !== null) {
35
- const literal = m[1] ?? m[2];
36
- if (literal)
37
- out.push(literal);
38
- m = re.exec(src);
39
- }
40
- return out;
41
- }
42
- describe("nudge invariants — SIGNAL_PREFIX_LEGEND", () => {
43
- it("contains no literal `:N` cursor placeholder", () => {
44
- // The page hint format example must use concrete-value language, not :N.
45
- // Concrete shape uses <nextValue>, <remaining>, etc. as descriptive
46
- // placeholders inside angle brackets — those are documentation form,
47
- // not emission form.
48
- expect(SIGNAL_PREFIX_LEGEND).not.toMatch(/:N\b/);
49
- });
50
- it("contains no bare `<name>` parameter placeholder", () => {
51
- // Angle-bracket descriptors are allowed in format documentation
52
- // (<tool>, <cursorArg>, <nextValue>) but the literal "<name>" was the
53
- // pre-rewrite stand-in for an entity that never got substituted.
54
- expect(SIGNAL_PREFIX_LEGEND).not.toContain("<name>");
55
- });
56
- it("uses imperative verbs, not hedge verbs", () => {
57
- for (const re of HEDGE_VERBS) {
58
- expect(SIGNAL_PREFIX_LEGEND).not.toMatch(re);
59
- }
60
- });
61
- });
62
- describe("nudge invariants — signal-scorer action strings", () => {
63
- const src = readSource("intelligence/signal-scorer.ts");
64
- const actions = extractActionLiterals(src);
65
- it("scorer file has multiple action: literals (sanity check)", () => {
66
- expect(actions.length).toBeGreaterThan(5);
67
- });
68
- it("no action string uses hedge verbs", () => {
69
- for (const action of actions) {
70
- for (const re of HEDGE_VERBS) {
71
- if (re.test(action)) {
72
- throw new Error(`signal-scorer action uses hedge verb (${re}): "${action}"`);
73
- }
74
- }
75
- }
76
- });
77
- it("no action string uses the deictic phrase 'this entity / pattern / file'", () => {
78
- for (const action of actions) {
79
- // The audit explicitly identified 'this entity', 'this pattern',
80
- // 'this file' as the recurring deictic anti-pattern.
81
- expect(action).not.toMatch(/\bthis (entity|pattern|file)\b/);
82
- }
83
- });
84
- });
85
- describe("nudge invariants — isError reaches the agent's MCP context", () => {
86
- const src = readSource("proxy/proxy.ts");
87
- const lines = src.split("\n");
88
- /**
89
- * For every line that writes a `[unerr]` error to stderr, the surrounding
90
- * window (next 12 lines) must include `isError: true`. This couples the
91
- * human-debug channel (stderr → .unerr/logs/) with the agent-context
92
- * channel (MCP CallToolResult.isError). The user's original concern:
93
- * "make sure isError writes to err stream AND err stream gets into the
94
- * coding agent context" — codified.
95
- */
96
- it("every stderr error log in proxy.ts pairs with an isError:true on the wire", () => {
97
- const orphans = [];
98
- for (let i = 0; i < lines.length; i++) {
99
- const raw = lines[i];
100
- if (!raw)
101
- continue;
102
- // Match the stderr error pattern we standardized on.
103
- if (!/process\.stderr\.write\([\s\S]*\[unerr\][^)]*(failed|threw|disabled|validation)/i.test(raw))
104
- continue;
105
- // Look ahead up to 12 lines for `isError: true`.
106
- const window = lines.slice(i, Math.min(lines.length, i + 13)).join("\n");
107
- if (!/isError:\s*true/.test(window)) {
108
- orphans.push({ line: i + 1, text: raw.trim().slice(0, 100) });
109
- }
110
- }
111
- if (orphans.length > 0) {
112
- const msg = orphans
113
- .map((o) => ` proxy.ts:${o.line} → ${o.text}`)
114
- .join("\n");
115
- throw new Error(`${orphans.length} stderr error log(s) in proxy.ts do not set isError:true within 12 lines. Pair every human-debug log with a wire isError so the agent sees the failure:\n${msg}`);
116
- }
117
- });
118
- it("contains the expected error-routing sites (sanity check)", () => {
119
- // Documents the four MCP error paths that must surface isError to MCP
120
- // clients. If anyone removes a path, this fires.
121
- expect(src).toMatch(/record_fact failed/);
122
- expect(src).toMatch(/recall_facts failed/);
123
- expect(src).toMatch(/tools\/call validation failed for/);
124
- expect(src).toMatch(/router\.execute\(\$\{name\}\) threw/);
125
- });
126
- });
127
- describe("nudge invariants — wire-cap nudges", () => {
128
- const src = readSource("proxy/wire-cap.ts");
129
- it("the buildPageHint template uses a numeric cursor (no `:N` in code)", () => {
130
- // The page-hint template now interpolates `${nextCursor}`. If anyone
131
- // ever reverts to a literal `:N`, this catches it.
132
- expect(src).not.toMatch(/\$\{cursorArg\}:N/);
133
- expect(src).not.toMatch(/`ur\|pg \$\{toolName\}[^`]*:N[`\s—]/);
134
- });
135
- it("PER_TOOL_CAPS.filterHint values are concrete (no `<name>` / `:T` / `:V` placeholders)", () => {
136
- // Page-hint format: `ur|pg <tool> +N — <cursor>:<n>/<filterHint>`. The
137
- // filterHint is appended verbatim. Literal placeholders (`<name>`, `:T`,
138
- // `:V`) train the agent to paste the placeholder instead of substituting
139
- // a real value — the exact anti-pattern this audit eliminated. Concrete
140
- // values or pipe-separated enums only.
141
- const capMatches = src.matchAll(/filterHint:\s*"([^"]+)"/g);
142
- const offenders = [];
143
- for (const match of capMatches) {
144
- const hint = match[1] ?? "";
145
- // Forbid angle-bracket placeholders (`<name>`, `<entity>`, etc).
146
- if (/<[^>]+>/.test(hint)) {
147
- offenders.push(`${hint} (contains <…> placeholder)`);
148
- continue;
149
- }
150
- // Forbid single-letter trailing placeholders (`fact_type:T`, `:V`).
151
- // A single-uppercase-letter value never reads as concrete.
152
- if (/:[A-Z](\s|$|\/|\|)/.test(hint)) {
153
- offenders.push(`${hint} (single-letter placeholder)`);
154
- }
155
- }
156
- if (offenders.length > 0) {
157
- throw new Error(`PER_TOOL_CAPS filterHint values must be concrete:\n${offenders.map((o) => ` - ${o}`).join("\n")}`);
158
- }
159
- });
160
- });