@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,82 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { installFileLogger } from "../utils/file-logger.js";
|
|
6
|
-
describe("installFileLogger", () => {
|
|
7
|
-
let tmpDir;
|
|
8
|
-
let logPath;
|
|
9
|
-
let uninstall = null;
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
tmpDir = join(os.tmpdir(), `unerr-file-logger-${Date.now()}-${Math.random()}`);
|
|
12
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
13
|
-
logPath = join(tmpDir, "mirror.log");
|
|
14
|
-
});
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
if (uninstall) {
|
|
17
|
-
uninstall();
|
|
18
|
-
uninstall = null;
|
|
19
|
-
}
|
|
20
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
-
});
|
|
22
|
-
it("mirrors stderr.write to the target file", () => {
|
|
23
|
-
uninstall = installFileLogger({ filePath: logPath });
|
|
24
|
-
process.stderr.write("hello from stderr\n");
|
|
25
|
-
expect(existsSync(logPath)).toBe(true);
|
|
26
|
-
expect(readFileSync(logPath, "utf-8")).toContain("hello from stderr");
|
|
27
|
-
});
|
|
28
|
-
it("strips ANSI escape codes from the file copy", () => {
|
|
29
|
-
uninstall = installFileLogger({ filePath: logPath });
|
|
30
|
-
const colored = "\x1b[31mred text\x1b[0m\n";
|
|
31
|
-
process.stderr.write(colored);
|
|
32
|
-
const fileContent = readFileSync(logPath, "utf-8");
|
|
33
|
-
expect(fileContent).toBe("red text\n");
|
|
34
|
-
expect(fileContent).not.toContain("\x1b[");
|
|
35
|
-
});
|
|
36
|
-
it("rotates when the file exceeds maxBytes", () => {
|
|
37
|
-
uninstall = installFileLogger({
|
|
38
|
-
filePath: logPath,
|
|
39
|
-
maxBytes: 512,
|
|
40
|
-
keep: 3,
|
|
41
|
-
});
|
|
42
|
-
// Write enough bytes to trigger rotation
|
|
43
|
-
for (let i = 0; i < 20; i++) {
|
|
44
|
-
process.stderr.write(`${"x".repeat(100)}\n`);
|
|
45
|
-
}
|
|
46
|
-
// After rotation, at least one .log.1 should exist
|
|
47
|
-
expect(existsSync(`${logPath}.1`)).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
it("honors the `keep` parameter — older rotations get dropped", () => {
|
|
50
|
-
uninstall = installFileLogger({
|
|
51
|
-
filePath: logPath,
|
|
52
|
-
maxBytes: 256,
|
|
53
|
-
keep: 2,
|
|
54
|
-
});
|
|
55
|
-
// Trigger many rotations
|
|
56
|
-
for (let cycle = 0; cycle < 6; cycle++) {
|
|
57
|
-
for (let i = 0; i < 10; i++) {
|
|
58
|
-
process.stderr.write(`${"x".repeat(100)}\n`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// .log, .log.1, .log.2 may exist; .log.3 must not
|
|
62
|
-
expect(existsSync(`${logPath}.3`)).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
it("uninstaller restores original stderr.write", () => {
|
|
65
|
-
const original = process.stderr.write;
|
|
66
|
-
const off = installFileLogger({ filePath: logPath });
|
|
67
|
-
expect(process.stderr.write).not.toBe(original);
|
|
68
|
-
off();
|
|
69
|
-
expect(process.stderr.write).toBe(original);
|
|
70
|
-
// No further writes should land in the file
|
|
71
|
-
const before = existsSync(logPath) ? readFileSync(logPath, "utf-8") : "";
|
|
72
|
-
process.stderr.write("post-uninstall noise\n");
|
|
73
|
-
const after = existsSync(logPath) ? readFileSync(logPath, "utf-8") : "";
|
|
74
|
-
expect(after).toBe(before);
|
|
75
|
-
});
|
|
76
|
-
it("creates the parent directory if missing", () => {
|
|
77
|
-
const nested = join(tmpDir, "a", "b", "c", "mirror.log");
|
|
78
|
-
uninstall = installFileLogger({ filePath: nested });
|
|
79
|
-
process.stderr.write("nested\n");
|
|
80
|
-
expect(existsSync(nested)).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { buildFileOutline } from "../tools/coding/file-outline.js";
|
|
6
|
-
function makeTmpDir(label) {
|
|
7
|
-
const dir = join(tmpdir(), `fo-${label}-${Date.now()}`);
|
|
8
|
-
mkdirSync(dir, { recursive: true });
|
|
9
|
-
return dir;
|
|
10
|
-
}
|
|
11
|
-
describe("buildFileOutline", () => {
|
|
12
|
-
it("extracts entities from TypeScript without graph", async () => {
|
|
13
|
-
const dir = makeTmpDir("ts");
|
|
14
|
-
writeFileSync(join(dir, "sample.ts"), "export function alpha(x: number): number {\n return x + 1;\n}\n", "utf-8");
|
|
15
|
-
const o = await buildFileOutline({
|
|
16
|
-
cwd: dir,
|
|
17
|
-
filePathArg: "sample.ts",
|
|
18
|
-
graph: null,
|
|
19
|
-
});
|
|
20
|
-
expect(o.total_lines).toBeGreaterThanOrEqual(3);
|
|
21
|
-
expect(o.language).toBe("typescript");
|
|
22
|
-
expect(o.entities.some((e) => e.name === "alpha")).toBe(true);
|
|
23
|
-
expect(o.imports.length + o.exports.length).toBeGreaterThanOrEqual(1);
|
|
24
|
-
});
|
|
25
|
-
it("parses JSON config keys for small .json files", async () => {
|
|
26
|
-
const dir = makeTmpDir("json");
|
|
27
|
-
writeFileSync(join(dir, "cfg.json"), JSON.stringify({ foo: 1, bar: { nested: true } }), "utf-8");
|
|
28
|
-
const o = await buildFileOutline({
|
|
29
|
-
cwd: dir,
|
|
30
|
-
filePathArg: "cfg.json",
|
|
31
|
-
graph: null,
|
|
32
|
-
});
|
|
33
|
-
expect(o.config_keys?.includes("foo")).toBe(true);
|
|
34
|
-
expect(o.config_keys?.includes("bar")).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
it("extracts a Go function without graph", async () => {
|
|
37
|
-
const dir = makeTmpDir("go");
|
|
38
|
-
writeFileSync(join(dir, "main.go"), "package main\n\nfunc goAlpha() int { return 1 }\n", "utf-8");
|
|
39
|
-
const o = await buildFileOutline({
|
|
40
|
-
cwd: dir,
|
|
41
|
-
filePathArg: "main.go",
|
|
42
|
-
graph: null,
|
|
43
|
-
});
|
|
44
|
-
expect(o.language).toBe("go");
|
|
45
|
-
expect(o.entities.some((e) => e.name === "goAlpha")).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
it("extracts a Python function without graph", async () => {
|
|
48
|
-
const dir = makeTmpDir("py");
|
|
49
|
-
writeFileSync(join(dir, "mod.py"), "def py_alpha(n):\n return n + 1\n", "utf-8");
|
|
50
|
-
const o = await buildFileOutline({
|
|
51
|
-
cwd: dir,
|
|
52
|
-
filePathArg: "mod.py",
|
|
53
|
-
graph: null,
|
|
54
|
-
});
|
|
55
|
-
expect(o.language).toBe("python");
|
|
56
|
-
expect(o.entities.some((e) => e.name === "py_alpha")).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
it("lists markdown headings", async () => {
|
|
59
|
-
const dir = makeTmpDir("md");
|
|
60
|
-
writeFileSync(join(dir, "doc.md"), "# Title\n\n## Section\nbody\n", "utf-8");
|
|
61
|
-
const o = await buildFileOutline({
|
|
62
|
-
cwd: dir,
|
|
63
|
-
filePathArg: "doc.md",
|
|
64
|
-
graph: null,
|
|
65
|
-
});
|
|
66
|
-
expect(o.headings?.some((h) => h.includes("Title"))).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
// ─── New FRP-3 tests ──────────────────────────────────────────────────────
|
|
69
|
-
it("marks exported entities with exported: true", async () => {
|
|
70
|
-
const dir = makeTmpDir("exported");
|
|
71
|
-
writeFileSync(join(dir, "lib.ts"), [
|
|
72
|
-
"export function publicFn(): void {}",
|
|
73
|
-
"function privateFn(): void {}",
|
|
74
|
-
"export const PUBLIC_CONST = 1;",
|
|
75
|
-
"const PRIVATE_CONST = 2;",
|
|
76
|
-
"export class MyClass {}",
|
|
77
|
-
].join("\n"), "utf-8");
|
|
78
|
-
const o = await buildFileOutline({
|
|
79
|
-
cwd: dir,
|
|
80
|
-
filePathArg: "lib.ts",
|
|
81
|
-
graph: null,
|
|
82
|
-
});
|
|
83
|
-
const publicFn = o.entities.find((e) => e.name === "publicFn");
|
|
84
|
-
const privateFn = o.entities.find((e) => e.name === "privateFn");
|
|
85
|
-
const myClass = o.entities.find((e) => e.name === "MyClass");
|
|
86
|
-
expect(publicFn?.exported).toBe(true);
|
|
87
|
-
expect(privateFn?.exported).toBe(false);
|
|
88
|
-
expect(myClass?.exported).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
it("provides token_estimate roughly proportional to file size", async () => {
|
|
91
|
-
const dir = makeTmpDir("token-est");
|
|
92
|
-
const content = "x".repeat(400); // 400 chars → ~100 tokens at 4 chars/token
|
|
93
|
-
writeFileSync(join(dir, "small.ts"), content, "utf-8");
|
|
94
|
-
const o = await buildFileOutline({
|
|
95
|
-
cwd: dir,
|
|
96
|
-
filePathArg: "small.ts",
|
|
97
|
-
graph: null,
|
|
98
|
-
});
|
|
99
|
-
// 400 chars / 4 chars_per_token = 100 tokens
|
|
100
|
-
expect(o.token_estimate).toBe(100);
|
|
101
|
-
});
|
|
102
|
-
it("stable sort: entities with same start line sorted by name", async () => {
|
|
103
|
-
const dir = makeTmpDir("sort");
|
|
104
|
-
// Two entities at same line is unusual but can happen with type + const on same line
|
|
105
|
-
// We'll simulate with a file where AST extracts multiple entities
|
|
106
|
-
writeFileSync(join(dir, "multi.ts"), [
|
|
107
|
-
"export function zebra(): void {}",
|
|
108
|
-
"export function alpha(): void {}",
|
|
109
|
-
"export function beta(): void {}",
|
|
110
|
-
].join("\n"), "utf-8");
|
|
111
|
-
const o = await buildFileOutline({
|
|
112
|
-
cwd: dir,
|
|
113
|
-
filePathArg: "multi.ts",
|
|
114
|
-
graph: null,
|
|
115
|
-
});
|
|
116
|
-
// Entities should be sorted by start line first
|
|
117
|
-
for (let i = 1; i < o.entities.length; i++) {
|
|
118
|
-
const prev = o.entities[i - 1];
|
|
119
|
-
const curr = o.entities[i];
|
|
120
|
-
if (prev.lines[0] === curr.lines[0]) {
|
|
121
|
-
// Same start line → alphabetical
|
|
122
|
-
expect(prev.name.localeCompare(curr.name)).toBeLessThanOrEqual(0);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
expect(prev.lines[0]).toBeLessThan(curr.lines[0]);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
it("all entities have the exported field defined", async () => {
|
|
130
|
-
const dir = makeTmpDir("exported-all");
|
|
131
|
-
writeFileSync(join(dir, "mod.ts"), "export function a() {}\nfunction b() {}\nexport class C {}\n", "utf-8");
|
|
132
|
-
const o = await buildFileOutline({
|
|
133
|
-
cwd: dir,
|
|
134
|
-
filePathArg: "mod.ts",
|
|
135
|
-
graph: null,
|
|
136
|
-
});
|
|
137
|
-
for (const entity of o.entities) {
|
|
138
|
-
expect(typeof entity.exported).toBe("boolean");
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
});
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { rankEntityMatches, runFileReadForRouter, } from "../tools/coding/file-read-protocol.js";
|
|
6
|
-
function makeTmpDir(label) {
|
|
7
|
-
const dir = join(tmpdir(), `frp-${label}-${Date.now()}`);
|
|
8
|
-
mkdirSync(dir, { recursive: true });
|
|
9
|
-
return dir;
|
|
10
|
-
}
|
|
11
|
-
describe("runFileReadForRouter", () => {
|
|
12
|
-
it("returns full content for small files", async () => {
|
|
13
|
-
const dir = makeTmpDir("small");
|
|
14
|
-
writeFileSync(join(dir, "a.txt"), "one\ntwo\nthree\n", "utf-8");
|
|
15
|
-
const r = await runFileReadForRouter({ file_path: "a.txt" }, { cwd: dir, graph: null });
|
|
16
|
-
expect(typeof r.content).toBe("string");
|
|
17
|
-
expect(r.content.includes("1\tone")).toBe(true);
|
|
18
|
-
expect(r._layer6_meta?.gated).toBeUndefined();
|
|
19
|
-
});
|
|
20
|
-
it("gates files with >200 lines when no offset/entity", async () => {
|
|
21
|
-
const dir = makeTmpDir("gate");
|
|
22
|
-
const lines = Array.from({ length: 205 }, () => "x").join("\n");
|
|
23
|
-
writeFileSync(join(dir, "big.txt"), lines, "utf-8");
|
|
24
|
-
const r = await runFileReadForRouter({ file_path: "big.txt" }, { cwd: dir, graph: null });
|
|
25
|
-
expect(r.content && typeof r.content === "object").toBe(true);
|
|
26
|
-
const c = r.content;
|
|
27
|
-
expect(c.gated).toBe(true);
|
|
28
|
-
expect(r._layer6_meta?.format).toBe("outline");
|
|
29
|
-
expect(r._layer6_meta?.gated).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
it("entity slice includes ±5 line context around the match", async () => {
|
|
32
|
-
const dir = makeTmpDir("ctx");
|
|
33
|
-
const prefix = Array.from({ length: 12 }, (_, i) => `// line ${i + 1}`).join("\n");
|
|
34
|
-
writeFileSync(join(dir, "deep.ts"), `${prefix}\nexport function sliceFn(): number {\n return 42;\n}\n`, "utf-8");
|
|
35
|
-
const r = await runFileReadForRouter({ file_path: "deep.ts", entity: "sliceFn" }, { cwd: dir, graph: null });
|
|
36
|
-
const body = r.content;
|
|
37
|
-
expect(body.includes("// line 8")).toBe(true);
|
|
38
|
-
expect(body.includes("sliceFn")).toBe(true);
|
|
39
|
-
expect(body.includes("(Showing lines")).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
it("targets entity by name using AST when graph is null", async () => {
|
|
42
|
-
const dir = makeTmpDir("ent");
|
|
43
|
-
writeFileSync(join(dir, "mod.ts"), "// head\nexport function targetFn(): void {\n return;\n}\n", "utf-8");
|
|
44
|
-
const r = await runFileReadForRouter({ file_path: "mod.ts", entity: "targetFn" }, { cwd: dir, graph: null });
|
|
45
|
-
expect(typeof r.content).toBe("string");
|
|
46
|
-
expect(r.content.includes("targetFn")).toBe(true);
|
|
47
|
-
expect(r.content.includes("Showing lines")).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
it("rejects binary files", async () => {
|
|
50
|
-
const dir = makeTmpDir("bin");
|
|
51
|
-
writeFileSync(join(dir, "bin.dat"), Buffer.from([0, 1, 0]));
|
|
52
|
-
const r = await runFileReadForRouter({ file_path: "bin.dat" }, { cwd: dir, graph: null });
|
|
53
|
-
expect(r.content && typeof r.content === "object").toBe(true);
|
|
54
|
-
expect(r.content.error).toMatch(/Binary/);
|
|
55
|
-
});
|
|
56
|
-
// ─── Token Budget Tests ─────────────────────────────────────────────────
|
|
57
|
-
it("adaptive gating: budget=5000 allows 500-line file through without gating", async () => {
|
|
58
|
-
const dir = makeTmpDir("budget-high");
|
|
59
|
-
const lines = Array.from({ length: 300 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
60
|
-
writeFileSync(join(dir, "medium.ts"), lines, "utf-8");
|
|
61
|
-
const r = await runFileReadForRouter({ file_path: "medium.ts", token_budget: 5000 }, { cwd: dir, graph: null });
|
|
62
|
-
// With budget=5000, budgetLines = (5000*4)/80 = 250. effectiveGate = max(200, 250) = 250.
|
|
63
|
-
// File has 300 lines > 250 → still gated
|
|
64
|
-
// But let's use a higher budget to prove the adaptive gating works
|
|
65
|
-
const r2 = await runFileReadForRouter({ file_path: "medium.ts", token_budget: 30000 }, { cwd: dir, graph: null });
|
|
66
|
-
// budget=30000 → budgetLines = (30000*4)/80 = 1500. effectiveGate = max(200, 1500) = 1500.
|
|
67
|
-
// File has 300 lines < 1500 → NOT gated
|
|
68
|
-
expect(typeof r2.content).toBe("string");
|
|
69
|
-
expect(r2._layer6_meta?.gated).toBeUndefined();
|
|
70
|
-
});
|
|
71
|
-
it("token budget constrains output line count", async () => {
|
|
72
|
-
const dir = makeTmpDir("budget-cap");
|
|
73
|
-
// 150 lines, each ~40 chars → fits in default budget but let's constrain
|
|
74
|
-
const lines = Array.from({ length: 150 }, (_, i) => `const x${i} = ${i}; // padding here for length`).join("\n");
|
|
75
|
-
writeFileSync(join(dir, "vars.ts"), lines, "utf-8");
|
|
76
|
-
const r = await runFileReadForRouter({ file_path: "vars.ts", token_budget: 300 }, { cwd: dir, graph: null });
|
|
77
|
-
// budget=300 → budgetLines = (300*4)/80 = 15
|
|
78
|
-
// File has 150 lines, effLimit = min(15, 150) = 15
|
|
79
|
-
const body = r.content;
|
|
80
|
-
expect(body.includes("(Showing lines")).toBe(true);
|
|
81
|
-
// Should only show ~15 lines
|
|
82
|
-
const outputLines = body.split("\n").filter((l) => /^\d+\t/.test(l));
|
|
83
|
-
expect(outputLines.length).toBeLessThanOrEqual(20); // some tolerance
|
|
84
|
-
});
|
|
85
|
-
it("default behavior unchanged: no token_budget → same as before", async () => {
|
|
86
|
-
const dir = makeTmpDir("default");
|
|
87
|
-
const lines = Array.from({ length: 205 }, () => "x").join("\n");
|
|
88
|
-
writeFileSync(join(dir, "big.txt"), lines, "utf-8");
|
|
89
|
-
// Default token_budget=2000 → budgetLines=(2000*4)/80=100 → effectiveGate=max(200,100)=200
|
|
90
|
-
// File has 205 > 200 → gated (same as before)
|
|
91
|
-
const r = await runFileReadForRouter({ file_path: "big.txt" }, { cwd: dir, graph: null });
|
|
92
|
-
expect(r.content.gated).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
// ─── Ranked Entity Matching Tests ───────────────────────────────────────
|
|
95
|
-
it("case-insensitive match: 'CompressOutput' finds 'compressOutput'", async () => {
|
|
96
|
-
const dir = makeTmpDir("case-ins");
|
|
97
|
-
writeFileSync(join(dir, "fn.ts"), "// top\nexport function compressOutput(): string {\n return '';\n}\n", "utf-8");
|
|
98
|
-
const r = await runFileReadForRouter({ file_path: "fn.ts", entity: "CompressOutput" }, { cwd: dir, graph: null });
|
|
99
|
-
expect(typeof r.content).toBe("string");
|
|
100
|
-
expect(r.content.includes("compressOutput")).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
it("prefix match: 'compress' matches 'compressShellOutput'", async () => {
|
|
103
|
-
const dir = makeTmpDir("prefix");
|
|
104
|
-
writeFileSync(join(dir, "fn.ts"), "// filler\nexport function compressShellOutput(): string {\n return '';\n}\n", "utf-8");
|
|
105
|
-
const r = await runFileReadForRouter({ file_path: "fn.ts", entity: "compress" }, { cwd: dir, graph: null });
|
|
106
|
-
expect(typeof r.content).toBe("string");
|
|
107
|
-
expect(r.content.includes("compressShellOutput")).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
it("entity not found on large file: returns outline with suggestions", async () => {
|
|
110
|
-
const dir = makeTmpDir("not-found");
|
|
111
|
-
const filler = Array.from({ length: 210 }, (_, i) => `// line ${i}`).join("\n");
|
|
112
|
-
writeFileSync(join(dir, "big.ts"), `${filler}\nexport function realFunction(): void {}\n`, "utf-8");
|
|
113
|
-
const r = await runFileReadForRouter({ file_path: "big.ts", entity: "nonExistentThing" }, { cwd: dir, graph: null });
|
|
114
|
-
const c = r.content;
|
|
115
|
-
expect(c.gated).toBe(true);
|
|
116
|
-
expect(c.entity_search).toBeDefined();
|
|
117
|
-
const search = c.entity_search;
|
|
118
|
-
expect(search.matched).toBe(false);
|
|
119
|
-
expect(search.query).toBe("nonExistentThing");
|
|
120
|
-
});
|
|
121
|
-
it("provides tokens_estimate in _layer6_meta", async () => {
|
|
122
|
-
const dir = makeTmpDir("tokens-est");
|
|
123
|
-
writeFileSync(join(dir, "a.ts"), "const x = 1;\nconst y = 2;\n", "utf-8");
|
|
124
|
-
const r = await runFileReadForRouter({ file_path: "a.ts" }, { cwd: dir, graph: null });
|
|
125
|
-
expect(r._layer6_meta?.tokens_estimate).toBeGreaterThan(0);
|
|
126
|
-
expect(r._layer6_meta?.tokens_estimate).toBeLessThan(100);
|
|
127
|
-
});
|
|
128
|
-
it("empty file returns empty content without error", async () => {
|
|
129
|
-
const dir = makeTmpDir("empty");
|
|
130
|
-
writeFileSync(join(dir, "empty.ts"), "", "utf-8");
|
|
131
|
-
const r = await runFileReadForRouter({ file_path: "empty.ts" }, { cwd: dir, graph: null });
|
|
132
|
-
// Empty file has totalLines=1 (one empty string from split), body may be empty
|
|
133
|
-
// Should not throw or return error
|
|
134
|
-
expect(r._layer6_meta?.format).toBe("json");
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
describe("rankEntityMatches", () => {
|
|
138
|
-
const entities = [
|
|
139
|
-
{ name: "compressShellOutput", start_line: 10, body: "function body" },
|
|
140
|
-
{ name: "compressOutput", start_line: 20, body: "function body" },
|
|
141
|
-
{ name: "decompressInput", start_line: 30, body: "function body" },
|
|
142
|
-
{ name: "CompressOutput", start_line: 40, body: "function body" },
|
|
143
|
-
];
|
|
144
|
-
it("exact match scores 100", () => {
|
|
145
|
-
const ranked = rankEntityMatches(entities, "compressOutput");
|
|
146
|
-
expect(ranked[0].score).toBe(100);
|
|
147
|
-
expect(ranked[0].entity.name).toBe("compressOutput");
|
|
148
|
-
expect(ranked[0].matchType).toBe("exact");
|
|
149
|
-
});
|
|
150
|
-
it("case-insensitive match scores 90", () => {
|
|
151
|
-
const ranked = rankEntityMatches(entities, "compressoutput");
|
|
152
|
-
expect(ranked[0].score).toBe(90);
|
|
153
|
-
expect(ranked[0].entity.name).toBe("compressOutput");
|
|
154
|
-
expect(ranked[0].matchType).toBe("case_insensitive");
|
|
155
|
-
});
|
|
156
|
-
it("prefix match scores 80", () => {
|
|
157
|
-
const ranked = rankEntityMatches(entities, "compress");
|
|
158
|
-
expect(ranked[0].score).toBe(80);
|
|
159
|
-
// Both compressShellOutput and compressOutput start with "compress"
|
|
160
|
-
// Both get score 80, sort is stable (order depends on input array)
|
|
161
|
-
expect(ranked[0].matchType).toBe("prefix");
|
|
162
|
-
});
|
|
163
|
-
it("camelCase segment match scores 70", () => {
|
|
164
|
-
const ents = [
|
|
165
|
-
{ name: "compressShellOutput", start_line: 10, body: "fn" },
|
|
166
|
-
{ name: "handleInput", start_line: 20, body: "fn" },
|
|
167
|
-
];
|
|
168
|
-
const ranked = rankEntityMatches(ents, "shell");
|
|
169
|
-
expect(ranked[0].score).toBe(70);
|
|
170
|
-
expect(ranked[0].entity.name).toBe("compressShellOutput");
|
|
171
|
-
expect(ranked[0].matchType).toBe("camelCase_segment");
|
|
172
|
-
});
|
|
173
|
-
it("substring match scores between 40-60 based on specificity", () => {
|
|
174
|
-
const ents = [{ name: "myHandlerForXyz", start_line: 10, body: "fn" }];
|
|
175
|
-
// "handler" is not a camelCase segment of "myHandlerForXyz" (segments: my, handler, for, xyz)
|
|
176
|
-
// Actually it IS a segment. Let's use a true substring that isn't a segment.
|
|
177
|
-
const ents2 = [{ name: "handleAllRequests", start_line: 10, body: "fn" }];
|
|
178
|
-
// "eAll" is a substring but not a segment
|
|
179
|
-
const ranked = rankEntityMatches(ents2, "eAll");
|
|
180
|
-
expect(ranked[0].score).toBeGreaterThanOrEqual(40);
|
|
181
|
-
expect(ranked[0].score).toBeLessThanOrEqual(60);
|
|
182
|
-
expect(ranked[0].matchType).toBe("substring");
|
|
183
|
-
});
|
|
184
|
-
it("returns empty array when nothing matches", () => {
|
|
185
|
-
const ranked = rankEntityMatches(entities, "zzzzNotHere");
|
|
186
|
-
expect(ranked).toHaveLength(0);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 6 Sprint FE-C — format encoder unit + router integration tests.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { QueryRouter } from "../intelligence/query-router.js";
|
|
6
|
-
import { COLUMNAR_LEGEND_TEXT, detectShape, encodeColumnar, escapeColumnarCell, formatToolOutput, isUniformObjectArray, } from "../proxy/format-encoder.js";
|
|
7
|
-
import { createSessionLegendTracker } from "../proxy/session-legend.js";
|
|
8
|
-
describe("detectShape", () => {
|
|
9
|
-
it("classifies uniform object arrays", () => {
|
|
10
|
-
expect(detectShape([
|
|
11
|
-
{ a: 1, b: 2 },
|
|
12
|
-
{ b: 3, a: 4 },
|
|
13
|
-
])).toBe("uniform-array");
|
|
14
|
-
});
|
|
15
|
-
it("classifies heterogeneous arrays", () => {
|
|
16
|
-
expect(detectShape([{ a: 1 }, { a: 1, b: 2 }])).toBe("heterogeneous");
|
|
17
|
-
expect(detectShape([1, 2])).toBe("heterogeneous");
|
|
18
|
-
});
|
|
19
|
-
it("classifies single objects", () => {
|
|
20
|
-
expect(detectShape({ x: 1 })).toBe("single-object");
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
describe("isUniformObjectArray", () => {
|
|
24
|
-
it("returns true when keys match across rows", () => {
|
|
25
|
-
expect(isUniformObjectArray([
|
|
26
|
-
{ k: "a", n: 1 },
|
|
27
|
-
{ n: 2, k: "b" },
|
|
28
|
-
])).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
it("returns false when keys differ", () => {
|
|
31
|
-
expect(isUniformObjectArray([{ k: "a" }, { k: "b", extra: 1 }])).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe("escapeColumnarCell", () => {
|
|
35
|
-
it("quotes pipes and newlines", () => {
|
|
36
|
-
expect(escapeColumnarCell("a|b")).toBe('"a|b"');
|
|
37
|
-
expect(escapeColumnarCell('say "hi"')).toBe('"say ""hi"""');
|
|
38
|
-
expect(escapeColumnarCell("x\ny")).toBe("x\\ny");
|
|
39
|
-
});
|
|
40
|
-
it("renders null as empty", () => {
|
|
41
|
-
expect(escapeColumnarCell(null)).toBe("");
|
|
42
|
-
expect(escapeColumnarCell(undefined)).toBe("");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
describe("encodeColumnar", () => {
|
|
46
|
-
it("emits _fmt header and aligned rows", () => {
|
|
47
|
-
const text = encodeColumnar([
|
|
48
|
-
{ name: "x", v: 1 },
|
|
49
|
-
{ name: "y", v: 2 },
|
|
50
|
-
], ["name", "v"]);
|
|
51
|
-
expect(text.startsWith("_fmt:columnar\nname|v\n")).toBe(true);
|
|
52
|
-
expect(text).toContain("x|1");
|
|
53
|
-
expect(text).toContain("y|2");
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe("formatToolOutput", () => {
|
|
57
|
-
it("columnar-encodes uniform arrays", () => {
|
|
58
|
-
const meta = { format: "json" };
|
|
59
|
-
const out = formatToolOutput("get_callers", [{ k: "a", z: 1 }], meta);
|
|
60
|
-
expect(typeof out).toBe("string");
|
|
61
|
-
expect(out).toContain("_fmt:columnar");
|
|
62
|
-
expect(meta.format).toBe("columnar");
|
|
63
|
-
expect(meta.columns).toEqual(["k", "z"]);
|
|
64
|
-
});
|
|
65
|
-
it("does not format outline payloads", () => {
|
|
66
|
-
const meta = { format: "outline", gated: true };
|
|
67
|
-
const payload = [{ a: 1 }];
|
|
68
|
-
expect(formatToolOutput("file_read", payload, meta)).toBe(payload);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
describe("formatToolOutput FE-E (legends, tiers)", () => {
|
|
72
|
-
it("attaches columnar legend once per tracker until invalidated", () => {
|
|
73
|
-
const legend = createSessionLegendTracker();
|
|
74
|
-
const metaC = {};
|
|
75
|
-
formatToolOutput("get_callers", [{ k: "a", z: 1 }], metaC, {
|
|
76
|
-
legend,
|
|
77
|
-
tier: "columnar",
|
|
78
|
-
});
|
|
79
|
-
expect(metaC.columnar_legend).toBe(COLUMNAR_LEGEND_TEXT);
|
|
80
|
-
const metaC2 = {};
|
|
81
|
-
formatToolOutput("get_callers", [{ k: "b", z: 2 }], metaC2, {
|
|
82
|
-
legend,
|
|
83
|
-
tier: "columnar",
|
|
84
|
-
});
|
|
85
|
-
expect(metaC2.columnar_legend).toBeUndefined();
|
|
86
|
-
});
|
|
87
|
-
it("minified tier skips columnar but keeps structured JSON", () => {
|
|
88
|
-
const meta = {};
|
|
89
|
-
const out = formatToolOutput("get_callers", [
|
|
90
|
-
{ k: "a", z: 1 },
|
|
91
|
-
{ k: "b", z: 2 },
|
|
92
|
-
], meta, { tier: "minified" });
|
|
93
|
-
expect(Array.isArray(out)).toBe(true);
|
|
94
|
-
expect(meta.format).toBe("json");
|
|
95
|
-
});
|
|
96
|
-
it("expanded tier returns untouched object", () => {
|
|
97
|
-
const obj = { key: "fn1", kind: "function", name: "doStuff" };
|
|
98
|
-
const meta = {};
|
|
99
|
-
const out = formatToolOutput("get_function", obj, meta, {
|
|
100
|
-
tier: "expanded",
|
|
101
|
-
});
|
|
102
|
-
expect(out).toBe(obj);
|
|
103
|
-
expect(meta.format).toBe("json");
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
describe("performance: columnar encode 500 rows", () => {
|
|
107
|
-
it("encodes in under 5ms", () => {
|
|
108
|
-
const row = {
|
|
109
|
-
key: "k",
|
|
110
|
-
kind: "function",
|
|
111
|
-
name: "n",
|
|
112
|
-
file_path: "f.ts",
|
|
113
|
-
start_line: 1,
|
|
114
|
-
signature: "()",
|
|
115
|
-
body: "x",
|
|
116
|
-
fan_in: 0,
|
|
117
|
-
fan_out: 0,
|
|
118
|
-
risk_level: "normal",
|
|
119
|
-
community: -1,
|
|
120
|
-
};
|
|
121
|
-
const rows = Array.from({ length: 500 }, (_, i) => ({
|
|
122
|
-
...row,
|
|
123
|
-
key: `k${i}`,
|
|
124
|
-
}));
|
|
125
|
-
const cols = Object.keys(row).sort();
|
|
126
|
-
const t0 = performance.now();
|
|
127
|
-
encodeColumnar(rows, cols);
|
|
128
|
-
const ms = performance.now() - t0;
|
|
129
|
-
expect(ms).toBeLessThan(5);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
function minimalGraphWithCallers(rows) {
|
|
133
|
-
return {
|
|
134
|
-
getEntity: vi.fn(),
|
|
135
|
-
getCallersOf: vi.fn().mockResolvedValue(rows),
|
|
136
|
-
getCalleesOf: vi.fn().mockResolvedValue([]),
|
|
137
|
-
searchEntities: vi.fn().mockResolvedValue([]),
|
|
138
|
-
getImports: vi.fn().mockResolvedValue([]),
|
|
139
|
-
getBlastRadius: vi.fn().mockReturnValue({
|
|
140
|
-
direct_callers: 0,
|
|
141
|
-
direct_callees: 0,
|
|
142
|
-
transitive_count: 0,
|
|
143
|
-
transitive_depth: 0,
|
|
144
|
-
is_chokepoint: false,
|
|
145
|
-
summary: "",
|
|
146
|
-
}),
|
|
147
|
-
getBlastRadiusEntities: vi.fn().mockReturnValue([]),
|
|
148
|
-
getConventionsForEntity: vi.fn().mockReturnValue([]),
|
|
149
|
-
getCorrections: vi.fn().mockReturnValue([]),
|
|
150
|
-
getCommunityForEntity: vi.fn().mockReturnValue(null),
|
|
151
|
-
getCrossCommunityEdges: vi.fn().mockReturnValue([]),
|
|
152
|
-
getEntitiesByFile: vi.fn().mockReturnValue([]),
|
|
153
|
-
queryEntities: vi.fn().mockReturnValue([]),
|
|
154
|
-
getDriftOverlayEntity: vi.fn().mockReturnValue(null),
|
|
155
|
-
getDriftSummary: vi
|
|
156
|
-
.fn()
|
|
157
|
-
.mockReturnValue({ added: 0, modified: 0, removed: 0, total: 0 }),
|
|
158
|
-
getCriticalNodes: vi.fn().mockReturnValue([]),
|
|
159
|
-
getCrossBoundaryLinks: vi.fn().mockReturnValue([]),
|
|
160
|
-
getRules: vi.fn().mockReturnValue([]),
|
|
161
|
-
getJustificationsForEntity: vi.fn().mockReturnValue([]),
|
|
162
|
-
getDriftEntitiesForFile: vi.fn().mockReturnValue([]),
|
|
163
|
-
db: { run: vi.fn().mockResolvedValue({ rows: [] }) },
|
|
164
|
-
close: vi.fn(),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
describe("QueryRouter + FE-C integration", () => {
|
|
168
|
-
it("get_callers returns columnar text for uniform caller rows", async () => {
|
|
169
|
-
const row = {
|
|
170
|
-
key: "caller1",
|
|
171
|
-
kind: "function",
|
|
172
|
-
name: "c",
|
|
173
|
-
file_path: "src/x.ts",
|
|
174
|
-
start_line: 1,
|
|
175
|
-
signature: "()",
|
|
176
|
-
body: "",
|
|
177
|
-
fan_in: 1,
|
|
178
|
-
fan_out: 0,
|
|
179
|
-
risk_level: "normal",
|
|
180
|
-
community: -1,
|
|
181
|
-
};
|
|
182
|
-
const graph = minimalGraphWithCallers([row]);
|
|
183
|
-
const router = new QueryRouter(graph);
|
|
184
|
-
const result = await router.execute("get_callers", { key: "fn1" });
|
|
185
|
-
expect(result._meta.format).toBe("columnar");
|
|
186
|
-
expect(typeof result.content).toBe("string");
|
|
187
|
-
expect(result.content).toContain("_fmt:columnar");
|
|
188
|
-
expect(result._meta.columns?.length).toBeGreaterThan(0);
|
|
189
|
-
});
|
|
190
|
-
it("search_code returns columnar for uniform entity rows", async () => {
|
|
191
|
-
const row = {
|
|
192
|
-
key: "e1",
|
|
193
|
-
kind: "function",
|
|
194
|
-
name: "n",
|
|
195
|
-
file_path: "a.ts",
|
|
196
|
-
start_line: 1,
|
|
197
|
-
signature: "()",
|
|
198
|
-
body: "",
|
|
199
|
-
fan_in: 0,
|
|
200
|
-
fan_out: 0,
|
|
201
|
-
risk_level: "low",
|
|
202
|
-
community: 0,
|
|
203
|
-
};
|
|
204
|
-
const graph = minimalGraphWithCallers([]);
|
|
205
|
-
graph.searchEntities.mockResolvedValue([row]);
|
|
206
|
-
const router = new QueryRouter(graph);
|
|
207
|
-
const result = await router.execute("search_code", { query: "foo" });
|
|
208
|
-
expect(result._meta.format).toBe("columnar");
|
|
209
|
-
expect(result.content.includes("_fmt:columnar")).toBe(true);
|
|
210
|
-
});
|
|
211
|
-
it("get_function strips community but preserves body (useful code)", async () => {
|
|
212
|
-
const graph = minimalGraphWithCallers([]);
|
|
213
|
-
graph.getEntity.mockResolvedValue({
|
|
214
|
-
key: "fn1",
|
|
215
|
-
kind: "function",
|
|
216
|
-
name: "doStuff",
|
|
217
|
-
file_path: "src/index.ts",
|
|
218
|
-
start_line: 10,
|
|
219
|
-
signature: "()",
|
|
220
|
-
body: "{}",
|
|
221
|
-
fan_in: 0,
|
|
222
|
-
fan_out: 0,
|
|
223
|
-
risk_level: "normal",
|
|
224
|
-
community: 0,
|
|
225
|
-
});
|
|
226
|
-
const router = new QueryRouter(graph);
|
|
227
|
-
const result = await router.execute("get_function", { key: "fn1" });
|
|
228
|
-
const content = result.content;
|
|
229
|
-
expect(content.key).toBe("fn1");
|
|
230
|
-
expect(content.body).toBe("{}");
|
|
231
|
-
expect(content.community).toBeUndefined();
|
|
232
|
-
});
|
|
233
|
-
});
|