@unerr-ai/unerr 0.1.5 → 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 +39151 -36992
  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-BsMTQdhX.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,199 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { detectLanguage, entityKey, extractEntities, } from "../intelligence/ast-extractor.js";
6
- describe("Local Parse — TypeScript AST Extraction (P5.6-ADV-01)", () => {
7
- let tmpDir;
8
- beforeEach(() => {
9
- tmpDir = path.join(os.tmpdir(), `unerr-local-parse-${Date.now()}`);
10
- fs.mkdirSync(tmpDir, { recursive: true });
11
- });
12
- afterEach(() => {
13
- fs.rmSync(tmpDir, { recursive: true, force: true });
14
- });
15
- it("extracts entities from TypeScript files matching expected output format", () => {
16
- const content = [
17
- "export function main(): void {",
18
- " console.log('hello')",
19
- "}",
20
- "",
21
- "export interface Config {",
22
- " port: number",
23
- " host: string",
24
- "}",
25
- "",
26
- "export class Server {",
27
- " start() {",
28
- " return true",
29
- " }",
30
- "}",
31
- ].join("\n");
32
- const entities = extractEntities(content, "src/index.ts");
33
- expect(entities.length).toBeGreaterThanOrEqual(3);
34
- const funcEntity = entities.find((e) => e.name === "main");
35
- expect(funcEntity).toBeDefined();
36
- expect(funcEntity?.kind).toBe("function");
37
- expect(funcEntity?.line_start).toBe(1);
38
- const ifaceEntity = entities.find((e) => e.name === "Config");
39
- expect(ifaceEntity).toBeDefined();
40
- expect(ifaceEntity?.kind).toBe("interface");
41
- const classEntity = entities.find((e) => e.name === "Server");
42
- expect(classEntity).toBeDefined();
43
- expect(classEntity?.kind).toBe("class");
44
- });
45
- it("entity keys match entityHash algorithm", () => {
46
- // entityKey(repoId, filePath, kind, name, signature?)
47
- // SHA-256 of null-byte-separated string, first 16 hex chars
48
- const key1 = entityKey("repo-1", "src/auth.ts", "function", "login", "(user: string)");
49
- const key2 = entityKey("repo-1", "src/auth.ts", "function", "login", "(user: string)");
50
- // Deterministic
51
- expect(key1).toBe(key2);
52
- // 16 hex chars
53
- expect(key1).toMatch(/^[0-9a-f]{16}$/);
54
- // Different inputs → different keys
55
- const key3 = entityKey("repo-1", "src/auth.ts", "function", "logout", "()");
56
- expect(key3).not.toBe(key1);
57
- });
58
- it("extracts entities from Python files", () => {
59
- const content = [
60
- "class AuthService:",
61
- " def __init__(self):",
62
- " self.token = None",
63
- "",
64
- " async def login(self, user: str) -> bool:",
65
- " return True",
66
- "",
67
- "def main():",
68
- " svc = AuthService()",
69
- ].join("\n");
70
- const entities = extractEntities(content, "src/auth.py");
71
- const cls = entities.find((e) => e.name === "AuthService");
72
- expect(cls).toBeDefined();
73
- expect(cls?.kind).toBe("class");
74
- const initFn = entities.find((e) => e.name === "__init__");
75
- expect(initFn).toBeDefined();
76
- expect(initFn?.kind).toBe("function");
77
- const mainFn = entities.find((e) => e.name === "main");
78
- expect(mainFn).toBeDefined();
79
- });
80
- it("extracts entities from Go files", () => {
81
- const content = [
82
- "package main",
83
- "",
84
- "type Server struct {",
85
- " port int",
86
- "}",
87
- "",
88
- "func (s *Server) Start() error {",
89
- " return nil",
90
- "}",
91
- "",
92
- "func main() {",
93
- " s := &Server{port: 8080}",
94
- " s.Start()",
95
- "}",
96
- ].join("\n");
97
- const entities = extractEntities(content, "main.go");
98
- const structEntity = entities.find((e) => e.name === "Server");
99
- expect(structEntity).toBeDefined();
100
- expect(structEntity?.kind).toBe("class");
101
- const methodEntity = entities.find((e) => e.name === "Server.Start");
102
- expect(methodEntity).toBeDefined();
103
- expect(methodEntity?.kind).toBe("method");
104
- expect(methodEntity?.parent_class).toBe("Server");
105
- const mainFn = entities.find((e) => e.name === "main");
106
- expect(mainFn).toBeDefined();
107
- });
108
- it("detects language from file extension", () => {
109
- expect(detectLanguage("src/index.ts")).toBe("typescript");
110
- expect(detectLanguage("src/app.tsx")).toBe("typescript");
111
- expect(detectLanguage("src/main.py")).toBe("python");
112
- expect(detectLanguage("main.go")).toBe("go");
113
- expect(detectLanguage("Main.java")).toBe("java");
114
- expect(detectLanguage("lib.rs")).toBe("rust");
115
- expect(detectLanguage("main.c")).toBe("c");
116
- expect(detectLanguage("main.cpp")).toBe("cpp");
117
- expect(detectLanguage("README.md")).toBeNull();
118
- expect(detectLanguage("Makefile")).toBeNull();
119
- });
120
- it("returns empty array for unsupported file types", () => {
121
- const entities = extractEntities("# Heading\nSome markdown", "README.md");
122
- expect(entities).toEqual([]);
123
- });
124
- it("each entity includes content_hash for dedup", () => {
125
- const content = "export function hello(): string {\n return 'world'\n}\n";
126
- const entities = extractEntities(content, "src/hello.ts");
127
- expect(entities.length).toBe(1);
128
- expect(entities[0]?.content_hash).toMatch(/^[0-9a-f]{16}$/);
129
- // Same content → same hash
130
- const entities2 = extractEntities(content, "src/hello.ts");
131
- expect(entities2[0]?.content_hash).toBe(entities[0]?.content_hash);
132
- // Different content → different hash
133
- const modified = "export function hello(): string {\n return 'changed'\n}\n";
134
- const entities3 = extractEntities(modified, "src/hello.ts");
135
- expect(entities3[0]?.content_hash).not.toBe(entities[0]?.content_hash);
136
- });
137
- it("handles large files without hanging", () => {
138
- // Generate 10K lines
139
- const lines = [];
140
- for (let i = 0; i < 10000; i++) {
141
- if (i % 100 === 0) {
142
- lines.push(`export function func_${i}(): void {`);
143
- lines.push(` console.log(${i})`);
144
- lines.push("}");
145
- }
146
- else {
147
- lines.push(`// line ${i}`);
148
- }
149
- }
150
- const content = lines.join("\n");
151
- const start = Date.now();
152
- const entities = extractEntities(content, "src/big.ts");
153
- const elapsed = Date.now() - start;
154
- expect(entities.length).toBe(100); // 10000/100 functions
155
- expect(elapsed).toBeLessThan(5000); // Should complete in <5s
156
- });
157
- it("builds graph-upload payload from extracted entities", () => {
158
- // Simulate what push.ts --local-parse does
159
- const repoId = "repo-123";
160
- const filePath = "src/auth.ts";
161
- const content = [
162
- "export class AuthService {",
163
- " login(user: string): boolean {",
164
- " return true",
165
- " }",
166
- "}",
167
- ].join("\n");
168
- const extracted = extractEntities(content, filePath);
169
- // Build EntityDoc-compatible objects
170
- const entities = extracted.map((e) => ({
171
- id: entityKey(repoId, filePath, e.kind, e.name, e.signature),
172
- kind: e.kind,
173
- name: e.name,
174
- file_path: filePath,
175
- start_line: e.line_start,
176
- end_line: e.line_end,
177
- signature: e.signature || undefined,
178
- }));
179
- // Validate shapes match graph-upload requirements
180
- for (const entity of entities) {
181
- expect(entity.id).toMatch(/^[0-9a-f]{16}$/);
182
- expect(entity.kind).toBeTruthy();
183
- expect(entity.name).toBeTruthy();
184
- expect(entity.file_path).toBe(filePath);
185
- }
186
- // Build edges
187
- const fileId = entityKey(repoId, filePath, "file", filePath);
188
- const edges = entities.map((e) => ({
189
- _from: `files/${fileId}`,
190
- _to: `${e.kind === "class" ? "classes" : "functions"}/${e.id}`,
191
- kind: "contains",
192
- }));
193
- expect(edges.length).toBe(entities.length);
194
- for (const edge of edges) {
195
- expect(edge._from).toContain("files/");
196
- expect(edge.kind).toBe("contains");
197
- }
198
- });
199
- });
@@ -1,208 +0,0 @@
1
- import { appendFileSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; // appendFileSync + writeFileSync still used for unerr.jsonl JSONL tests
2
- import os from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { closeMetricsStore, openMetricsStore, } from "../tracking/metrics-store.js";
6
- import { startupLog } from "../utils/startup-log.js";
7
- // Mock startupLog to capture output
8
- vi.mock("../utils/startup-log.js", () => {
9
- const calls = [];
10
- return {
11
- startupLog: {
12
- step: (...args) => calls.push({ method: "step", args }),
13
- done: (...args) => calls.push({ method: "done", args }),
14
- warn: (...args) => calls.push({ method: "warn", args }),
15
- error: (...args) => calls.push({ method: "error", args }),
16
- tokenFlow: (...args) => calls.push({ method: "tokenFlow", args }),
17
- fmt: {
18
- muted: (s) => s,
19
- dim: (s) => s,
20
- cyan: (s) => s,
21
- bold: (s) => s,
22
- },
23
- _calls: calls,
24
- _reset: () => {
25
- calls.length = 0;
26
- },
27
- },
28
- };
29
- });
30
- function getCalls() {
31
- return startupLog._calls;
32
- }
33
- function resetCalls() {
34
- startupLog._reset();
35
- }
36
- describe("log-tailer", () => {
37
- let tmpDir;
38
- let unerrDir;
39
- let logsDir;
40
- beforeEach(() => {
41
- tmpDir = join(os.tmpdir(), `unerr-tailer-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
42
- unerrDir = join(tmpDir, ".unerr");
43
- logsDir = join(unerrDir, "logs");
44
- mkdirSync(logsDir, { recursive: true });
45
- resetCalls();
46
- });
47
- afterEach(() => {
48
- closeMetricsStore(unerrDir);
49
- rmSync(tmpDir, { recursive: true, force: true });
50
- });
51
- it("startLogTailer returns a handle with close()", async () => {
52
- const { startLogTailer } = await import("../proxy/log-tailer.js");
53
- const handle = startLogTailer(tmpDir);
54
- expect(handle).toBeDefined();
55
- expect(typeof handle.close).toBe("function");
56
- handle.close();
57
- });
58
- it("polls compression rows from metrics.db", async () => {
59
- const { startLogTailer } = await import("../proxy/log-tailer.js");
60
- const handle = startLogTailer(tmpDir, { pollIntervalMs: 50 });
61
- // Insert after the tailer has captured its initial lastIds.
62
- openMetricsStore(unerrDir).insertCompression({
63
- ts: Date.now(),
64
- ts_iso: new Date().toISOString(),
65
- command: "ps aux",
66
- category: "tabular",
67
- confidence: 0.85,
68
- raw_bytes: 1000,
69
- compressed_bytes: 300,
70
- saved_pct: 70,
71
- omni_fallback: 0,
72
- tee_file: null,
73
- });
74
- await new Promise((resolve) => setTimeout(resolve, 250));
75
- const calls = getCalls();
76
- const compressionCall = calls.find((c) => c.method === "step" && String(c.args[0]).includes("ps aux"));
77
- expect(compressionCall).toBeDefined();
78
- handle.close();
79
- });
80
- it("polls token-flow rows and filters own-PID", async () => {
81
- const { startLogTailer } = await import("../proxy/log-tailer.js");
82
- const handle = startLogTailer(tmpDir, { pollIntervalMs: 50 });
83
- const store = openMetricsStore(unerrDir);
84
- const ts = Date.now();
85
- // Own PID — should be filtered out by printTokenFlowEntry
86
- store.insertTokenFlow({
87
- ts,
88
- ts_iso: new Date(ts).toISOString(),
89
- session_id: "s1",
90
- pid: process.pid,
91
- turn: 1,
92
- mechanism: "graph_query",
93
- tool: "get_callers",
94
- tokens_without: 150,
95
- tokens_with: 50,
96
- tokens_saved: 100,
97
- detail: null,
98
- });
99
- // Other PID — should be relayed
100
- store.insertTokenFlow({
101
- ts: ts + 1,
102
- ts_iso: new Date(ts + 1).toISOString(),
103
- session_id: "s1",
104
- pid: process.pid + 999,
105
- turn: 3,
106
- mechanism: "shell_compression",
107
- tool: "bash",
108
- tokens_without: 500,
109
- tokens_with: 150,
110
- tokens_saved: 350,
111
- detail: null,
112
- });
113
- await new Promise((resolve) => setTimeout(resolve, 250));
114
- const calls = getCalls();
115
- const tfCalls = calls.filter((c) => c.method === "tokenFlow");
116
- expect(tfCalls.length).toBe(1);
117
- const tfCall = tfCalls[0].args[0];
118
- expect(tfCall.mechanism).toBe("shell_compression");
119
- expect(tfCall.tokensSaved).toBe(350);
120
- handle.close();
121
- });
122
- it("filters own-PID entries from unerr.jsonl", async () => {
123
- const generalPath = join(logsDir, "unerr.jsonl");
124
- writeFileSync(generalPath, "");
125
- const { startLogTailer } = await import("../proxy/log-tailer.js");
126
- const handle = startLogTailer(tmpDir);
127
- const ownEntry = JSON.stringify({
128
- pid: process.pid,
129
- level: "warn",
130
- msg: "own-warning",
131
- });
132
- const otherEntry = JSON.stringify({
133
- pid: process.pid + 1,
134
- level: "warn",
135
- msg: "other-warning",
136
- });
137
- appendFileSync(generalPath, `${ownEntry}\n${otherEntry}\n`);
138
- await new Promise((resolve) => setTimeout(resolve, 3500));
139
- const calls = getCalls();
140
- const warnCalls = calls.filter((c) => c.method === "warn");
141
- expect(warnCalls.find((c) => String(c.args[0]).includes("own-warning"))).toBeUndefined();
142
- expect(warnCalls.find((c) => String(c.args[0]).includes("other-warning"))).toBeDefined();
143
- handle.close();
144
- });
145
- it("polls file-read rows for events with savings", async () => {
146
- const { startLogTailer } = await import("../proxy/log-tailer.js");
147
- const handle = startLogTailer(tmpDir, { pollIntervalMs: 50 });
148
- openMetricsStore(unerrDir).insertFileRead({
149
- ts: Date.now(),
150
- ts_iso: new Date().toISOString(),
151
- file: "src/proxy/proxy.ts",
152
- mode: "entity",
153
- total_lines: 2000,
154
- returned_lines: 45,
155
- saved_pct: 98,
156
- entity: null,
157
- token_estimate: null,
158
- });
159
- await new Promise((resolve) => setTimeout(resolve, 250));
160
- const calls = getCalls();
161
- const fileReadCall = calls.find((c) => c.method === "step" &&
162
- String(c.args[0]).includes("proxy.ts") &&
163
- String(c.args[0]).includes("entity"));
164
- expect(fileReadCall).toBeDefined();
165
- handle.close();
166
- });
167
- it("skips file-read entries with 0% savings", async () => {
168
- const { startLogTailer } = await import("../proxy/log-tailer.js");
169
- const handle = startLogTailer(tmpDir, { pollIntervalMs: 50 });
170
- openMetricsStore(unerrDir).insertFileRead({
171
- ts: Date.now(),
172
- ts_iso: new Date().toISOString(),
173
- file: "readme.md",
174
- mode: "full",
175
- total_lines: 20,
176
- returned_lines: 20,
177
- saved_pct: 0,
178
- entity: null,
179
- token_estimate: null,
180
- });
181
- await new Promise((resolve) => setTimeout(resolve, 250));
182
- const calls = getCalls();
183
- const fileReadCall = calls.find((c) => c.method === "step" && String(c.args[0]).includes("readme.md"));
184
- expect(fileReadCall).toBeUndefined();
185
- handle.close();
186
- });
187
- it("skips compression entries with 0% savings", async () => {
188
- const { startLogTailer } = await import("../proxy/log-tailer.js");
189
- const handle = startLogTailer(tmpDir, { pollIntervalMs: 50 });
190
- openMetricsStore(unerrDir).insertCompression({
191
- ts: Date.now(),
192
- ts_iso: new Date().toISOString(),
193
- command: "echo hello",
194
- category: "omni",
195
- confidence: 1,
196
- raw_bytes: 12,
197
- compressed_bytes: 12,
198
- saved_pct: 0,
199
- omni_fallback: 0,
200
- tee_file: null,
201
- });
202
- await new Promise((resolve) => setTimeout(resolve, 250));
203
- const calls = getCalls();
204
- const echoCall = calls.find((c) => c.method === "step" && String(c.args[0]).includes("echo hello"));
205
- expect(echoCall).toBeUndefined();
206
- handle.close();
207
- });
208
- });
@@ -1,276 +0,0 @@
1
- /**
2
- * BA-1.6: Loop Circuit Breaker tests.
3
- *
4
- * Verifies:
5
- * - 3 stuck patterns detected (repetitive_failure, context_poisoning, over_planning)
6
- * - Circuit breaker state transitions (closed → open → half_open → closed)
7
- * - Dollar calculation correctness
8
- * - TDD exemption (test files excluded from entity-retry count)
9
- * - $0.50 gate enforcement
10
- */
11
- import { describe, expect, it } from "vitest";
12
- import { LoopCircuitBreaker } from "../behaviors/loop-breaker.js";
13
- function makeCtx(overrides = {}) {
14
- return {
15
- toolName: "edit_file",
16
- args: { path: "src/payment.ts", content: "fix" },
17
- sessionId: "test-session",
18
- entityKey: "src/payment.ts::processPayment",
19
- filePath: "src/payment.ts",
20
- ...overrides,
21
- };
22
- }
23
- function makeErrorResult() {
24
- return {
25
- error: true,
26
- content: [{ type: "text", text: "TypeError: Cannot read property" }],
27
- };
28
- }
29
- function makeSuccessResult() {
30
- return {
31
- content: [{ type: "text", text: "File updated successfully" }],
32
- };
33
- }
34
- describe("Loop Circuit Breaker (BA-1.1)", () => {
35
- describe("Pattern Detection", () => {
36
- it("detects repetitive failure after 4 consecutive errors on same entity", async () => {
37
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
38
- for (let i = 0; i < 3; i++) {
39
- const ctx = makeCtx({
40
- result: { error: true, content: `TypeError on line ${45 + i}` },
41
- args: { path: "src/payment.ts", content: "retry logic v1" },
42
- });
43
- const output = await breaker.onPostToolUse(ctx);
44
- expect(output).toBeNull();
45
- }
46
- const finalCtx = makeCtx({
47
- result: { error: true, content: "TypeError on line 48" },
48
- args: { path: "src/payment.ts", content: "retry logic v1" },
49
- });
50
- const output = await breaker.onPostToolUse(finalCtx);
51
- expect(output).not.toBeNull();
52
- expect(output?.halt).toBe(true);
53
- expect(output?._context?.pattern).toBe("repetitive_failure");
54
- expect(output?._context?.reason).toContain("4 consecutive failed attempts");
55
- });
56
- it("detects context poisoning when all results are identical", async () => {
57
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
58
- const identicalResult = {
59
- error: true,
60
- content: [{ type: "text", text: "The exact same error every time" }],
61
- };
62
- for (let i = 0; i < 4; i++) {
63
- const ctx = makeCtx({
64
- result: identicalResult,
65
- args: {
66
- path: "src/payment.ts",
67
- content: `completely_different_approach_${i * 100}`,
68
- },
69
- });
70
- const output = await breaker.onPostToolUse(ctx);
71
- if (i < 3) {
72
- expect(output).toBeNull();
73
- }
74
- else {
75
- expect(output).not.toBeNull();
76
- expect(output?._context?.pattern).toBe("context_poisoning");
77
- }
78
- }
79
- });
80
- it("detects over-planning when attempts are spaced far apart", async () => {
81
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
82
- const now = Date.now();
83
- for (let i = 0; i < 4; i++) {
84
- const ctx = makeCtx({
85
- result: makeErrorResult(),
86
- args: {
87
- path: "src/payment.ts",
88
- action: `action_${i}`,
89
- variation: `v${i * 50}`,
90
- },
91
- });
92
- const originalDateNow = Date.now;
93
- Date.now = () => now + i * 20_000;
94
- try {
95
- await breaker.onPostToolUse(ctx);
96
- }
97
- finally {
98
- Date.now = originalDateNow;
99
- }
100
- }
101
- const stats = breaker.getSessionStats();
102
- expect(stats.loopsPrevented).toBeGreaterThanOrEqual(1);
103
- });
104
- });
105
- describe("Circuit Breaker States", () => {
106
- it("transitions from CLOSED to OPEN on loop detection", async () => {
107
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
108
- expect(breaker.getCircuitState("src/payment.ts::processPayment")).toBeNull();
109
- for (let i = 0; i < 4; i++) {
110
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
111
- }
112
- expect(breaker.getCircuitState("src/payment.ts::processPayment")).toBe("open");
113
- });
114
- it("blocks further attempts while circuit is OPEN", async () => {
115
- const breaker = new LoopCircuitBreaker({
116
- maxAttemptsPerEntity: 4,
117
- cooldownMs: 60_000,
118
- });
119
- for (let i = 0; i < 4; i++) {
120
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
121
- }
122
- const preResult = await breaker.onPreToolUse(makeCtx());
123
- expect(preResult).not.toBeNull();
124
- expect(preResult?.halt).toBe(true);
125
- expect(preResult?._context?.cooldown_remaining_s).toBeGreaterThan(0);
126
- });
127
- it("transitions to HALF_OPEN after cooldown expires", async () => {
128
- const breaker = new LoopCircuitBreaker({
129
- maxAttemptsPerEntity: 4,
130
- cooldownMs: 100,
131
- });
132
- for (let i = 0; i < 4; i++) {
133
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
134
- }
135
- await new Promise((r) => setTimeout(r, 150));
136
- const preResult = await breaker.onPreToolUse(makeCtx());
137
- expect(preResult).toBeNull();
138
- });
139
- it("returns to CLOSED from HALF_OPEN on success", async () => {
140
- const breaker = new LoopCircuitBreaker({
141
- maxAttemptsPerEntity: 4,
142
- cooldownMs: 100,
143
- });
144
- for (let i = 0; i < 4; i++) {
145
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
146
- }
147
- await new Promise((r) => setTimeout(r, 150));
148
- await breaker.onPreToolUse(makeCtx());
149
- await breaker.onPostToolUse(makeCtx({ result: makeSuccessResult() }));
150
- expect(breaker.getCircuitState("src/payment.ts::processPayment")).toBe("closed");
151
- });
152
- it("returns to OPEN from HALF_OPEN on failure", async () => {
153
- const breaker = new LoopCircuitBreaker({
154
- maxAttemptsPerEntity: 4,
155
- cooldownMs: 100,
156
- });
157
- for (let i = 0; i < 4; i++) {
158
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
159
- }
160
- await new Promise((r) => setTimeout(r, 150));
161
- await breaker.onPreToolUse(makeCtx());
162
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
163
- expect(breaker.getCircuitState("src/payment.ts::processPayment")).toBe("open");
164
- });
165
- });
166
- describe("TDD Exemption", () => {
167
- it("does NOT flag test file modifications as loop attempts", async () => {
168
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
169
- for (let i = 0; i < 6; i++) {
170
- const ctx = makeCtx({
171
- filePath: "src/__tests__/payment.test.ts",
172
- entityKey: "src/__tests__/payment.test.ts::testProcessPayment",
173
- result: makeErrorResult(),
174
- });
175
- const output = await breaker.onPostToolUse(ctx);
176
- expect(output).toBeNull();
177
- }
178
- });
179
- it("exempts .spec.ts files", async () => {
180
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
181
- for (let i = 0; i < 6; i++) {
182
- const ctx = makeCtx({
183
- filePath: "src/payment.spec.ts",
184
- entityKey: "src/payment.spec.ts::specProcessPayment",
185
- result: makeErrorResult(),
186
- });
187
- expect(await breaker.onPostToolUse(ctx)).toBeNull();
188
- }
189
- });
190
- });
191
- describe("Dollar Calculation", () => {
192
- it("includes dollar amount in guard moment output", async () => {
193
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
194
- let lastOutput = null;
195
- for (let i = 0; i < 4; i++) {
196
- lastOutput = await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
197
- }
198
- expect(lastOutput).not.toBeNull();
199
- expect(lastOutput?._meta?.dollars_saved).toBeDefined();
200
- expect(lastOutput?._meta?.tokens_saved).toBeGreaterThan(0);
201
- });
202
- it("accumulates savings across multiple loops", async () => {
203
- const breaker = new LoopCircuitBreaker({
204
- maxAttemptsPerEntity: 4,
205
- cooldownMs: 50,
206
- });
207
- for (let i = 0; i < 4; i++) {
208
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
209
- }
210
- const stats1 = breaker.getSessionStats();
211
- expect(stats1.loopsPrevented).toBe(1);
212
- expect(stats1.totalTokensSaved).toBeGreaterThan(0);
213
- await new Promise((r) => setTimeout(r, 100));
214
- await breaker.onPreToolUse(makeCtx({ entityKey: "src/checkout.ts::handleOrder" }));
215
- for (let i = 0; i < 4; i++) {
216
- await breaker.onPostToolUse(makeCtx({
217
- entityKey: "src/checkout.ts::handleOrder",
218
- filePath: "src/checkout.ts",
219
- result: makeErrorResult(),
220
- }));
221
- }
222
- const stats2 = breaker.getSessionStats();
223
- expect(stats2.loopsPrevented).toBe(2);
224
- expect(stats2.totalTokensSaved).toBeGreaterThan(stats1.totalTokensSaved);
225
- });
226
- });
227
- describe("$0.50 Gate", () => {
228
- it("only fires guard when estimated savings exceed $0.50", async () => {
229
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
230
- let guardFired = false;
231
- for (let i = 0; i < 4; i++) {
232
- const output = await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
233
- if (output?.guardMoment)
234
- guardFired = true;
235
- }
236
- expect(guardFired).toBe(true);
237
- });
238
- });
239
- describe("Session Stats", () => {
240
- it("tracks active and open circuits", async () => {
241
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
242
- for (let i = 0; i < 4; i++) {
243
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
244
- }
245
- const stats = breaker.getSessionStats();
246
- expect(stats.activeCircuits).toBe(1);
247
- expect(stats.openCircuits).toBe(1);
248
- expect(stats.loopsPrevented).toBe(1);
249
- });
250
- });
251
- describe("Edge Cases", () => {
252
- it("does nothing without entityKey", async () => {
253
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
254
- const ctx = makeCtx({ entityKey: undefined, result: makeErrorResult() });
255
- expect(await breaker.onPostToolUse(ctx)).toBeNull();
256
- });
257
- it("handles success results correctly (no false positive)", async () => {
258
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
259
- for (let i = 0; i < 6; i++) {
260
- const output = await breaker.onPostToolUse(makeCtx({ result: makeSuccessResult() }));
261
- expect(output).toBeNull();
262
- }
263
- });
264
- it("resets failure count on success between errors", async () => {
265
- const breaker = new LoopCircuitBreaker({ maxAttemptsPerEntity: 4 });
266
- for (let i = 0; i < 3; i++) {
267
- await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
268
- }
269
- await breaker.onPostToolUse(makeCtx({ result: makeSuccessResult() }));
270
- for (let i = 0; i < 3; i++) {
271
- const output = await breaker.onPostToolUse(makeCtx({ result: makeErrorResult() }));
272
- expect(output).toBeNull();
273
- }
274
- });
275
- });
276
- });