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