@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,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 4 Sprint VS: Value Surfacing tests.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { formatGuardMoment, getDollarGate, shouldFireGuard, } from "../behaviors/guard-formatter.js";
|
|
6
|
-
import { getValueSurfacingConfig, resetValueSurfacingConfig, setValueSurfacingConfig, } from "../config/value-surfacing.js";
|
|
7
|
-
import { createIntelligenceCounter } from "../tracking/intelligence-counter.js";
|
|
8
|
-
import { frameGuardMoment, frameSessionSummary, frameTokenSavings, frameWeeklyTrend, } from "../utils/counterfactual.js";
|
|
9
|
-
describe("Counterfactual Framing (VS.8)", () => {
|
|
10
|
-
it("frames token savings with 'without unerr' language", () => {
|
|
11
|
-
const msg = frameTokenSavings(45000, 0.135);
|
|
12
|
-
expect(msg).toContain("Without unerr");
|
|
13
|
-
expect(msg).toContain("45.0K");
|
|
14
|
-
expect(msg).toContain("$");
|
|
15
|
-
});
|
|
16
|
-
it("frames guard moment with prevented cost", () => {
|
|
17
|
-
const msg = frameGuardMoment("hallucination loop detected", 2.13);
|
|
18
|
-
expect(msg).toContain("[unerr]");
|
|
19
|
-
expect(msg).toContain("Prevented");
|
|
20
|
-
expect(msg).toContain("$2.13");
|
|
21
|
-
});
|
|
22
|
-
it("frames session summary", () => {
|
|
23
|
-
const msg = frameSessionSummary(23000, 0.069, 2);
|
|
24
|
-
expect(msg).toContain("Without unerr");
|
|
25
|
-
expect(msg).toContain("23.0K");
|
|
26
|
-
expect(msg).toContain("2 issue(s) prevented");
|
|
27
|
-
});
|
|
28
|
-
it("frames session with zero events positively", () => {
|
|
29
|
-
const msg = frameSessionSummary(0, 0, 0);
|
|
30
|
-
expect(msg).toContain("monitoring");
|
|
31
|
-
expect(msg).not.toContain("Without unerr");
|
|
32
|
-
});
|
|
33
|
-
it("frames weekly trend", () => {
|
|
34
|
-
const msg = frameWeeklyTrend(45000, 30000, 0.135);
|
|
35
|
-
expect(msg).toContain("↑");
|
|
36
|
-
expect(msg).toContain("This week");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describe("Guard Formatter (VS.4)", () => {
|
|
40
|
-
it("fires guard when >$0.50 threshold", () => {
|
|
41
|
-
const tokens = 200_000;
|
|
42
|
-
expect(shouldFireGuard(tokens)).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
it("does not fire guard below $0.50 threshold", () => {
|
|
45
|
-
const tokens = 100;
|
|
46
|
-
expect(shouldFireGuard(tokens)).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
it("dollar gate is $0.50", () => {
|
|
49
|
-
expect(getDollarGate()).toBe(0.5);
|
|
50
|
-
});
|
|
51
|
-
it("formatGuardMoment returns null below threshold", () => {
|
|
52
|
-
const result = formatGuardMoment("test", 10);
|
|
53
|
-
expect(result).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
it("formatGuardMoment returns GuardMoment above threshold", () => {
|
|
56
|
-
const result = formatGuardMoment("loop detected", 500_000);
|
|
57
|
-
expect(result).not.toBeNull();
|
|
58
|
-
expect(result?.passed).toBe(true);
|
|
59
|
-
expect(result?.dollarsPrevented).toBeGreaterThanOrEqual(0.5);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe("Intelligence Counter (VS.7)", () => {
|
|
63
|
-
it("tracks metrics", () => {
|
|
64
|
-
const counter = createIntelligenceCounter();
|
|
65
|
-
counter.record("entityCount", 847);
|
|
66
|
-
counter.record("edgeCount", 2134);
|
|
67
|
-
counter.record("communitiesDetected", 8);
|
|
68
|
-
counter.increment("conventionsLearned");
|
|
69
|
-
counter.increment("conventionsLearned");
|
|
70
|
-
const metrics = counter.getMetrics();
|
|
71
|
-
expect(metrics.entityCount).toBe(847);
|
|
72
|
-
expect(metrics.edgeCount).toBe(2134);
|
|
73
|
-
expect(metrics.conventionsLearned).toBe(2);
|
|
74
|
-
});
|
|
75
|
-
it("formats summary string", () => {
|
|
76
|
-
const counter = createIntelligenceCounter();
|
|
77
|
-
counter.record("entityCount", 1247);
|
|
78
|
-
counter.record("edgeCount", 3456);
|
|
79
|
-
counter.record("communitiesDetected", 12);
|
|
80
|
-
const summary = counter.formatSummary();
|
|
81
|
-
expect(summary).toContain("1,247 entities");
|
|
82
|
-
expect(summary).toContain("3,456 edges");
|
|
83
|
-
expect(summary).toContain("12 communities");
|
|
84
|
-
});
|
|
85
|
-
it("resets all metrics", () => {
|
|
86
|
-
const counter = createIntelligenceCounter();
|
|
87
|
-
counter.record("entityCount", 100);
|
|
88
|
-
counter.reset();
|
|
89
|
-
expect(counter.getMetrics().entityCount).toBe(0);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe("Value Surfacing Config (VS.9)", () => {
|
|
93
|
-
it("has sensible defaults", () => {
|
|
94
|
-
resetValueSurfacingConfig();
|
|
95
|
-
const config = getValueSurfacingConfig();
|
|
96
|
-
expect(config.guardThresholdDollars).toBe(0.5);
|
|
97
|
-
expect(config.weeklyEnabled).toBe(true);
|
|
98
|
-
expect(config.scorecardEnabled).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
it("allows overrides", () => {
|
|
101
|
-
setValueSurfacingConfig({ guardThresholdDollars: 1.0 });
|
|
102
|
-
expect(getValueSurfacingConfig().guardThresholdDollars).toBe(1.0);
|
|
103
|
-
resetValueSurfacingConfig();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { extractChains, getEntityHistory, getRevertPatterns, getSessionTimeline, } from "../tracking/ledger-chains.js";
|
|
3
|
-
function makeEntry(overrides = {}) {
|
|
4
|
-
return {
|
|
5
|
-
id: `entry-${Math.random().toString(36).slice(2, 8)}`,
|
|
6
|
-
ts: new Date().toISOString(),
|
|
7
|
-
tool: "get_function",
|
|
8
|
-
args_summary: {},
|
|
9
|
-
result_summary: {},
|
|
10
|
-
branch: "main",
|
|
11
|
-
head_sha: "abc123",
|
|
12
|
-
session_id: "sess-001",
|
|
13
|
-
correlation_id: null,
|
|
14
|
-
...overrides,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
describe("ledger-chains", () => {
|
|
18
|
-
describe("extractChains", () => {
|
|
19
|
-
it("groups entries by correlation_id into chains", () => {
|
|
20
|
-
const root = makeEntry({ id: "root-1", correlation_id: null });
|
|
21
|
-
const child1 = makeEntry({ id: "c1", correlation_id: "root-1" });
|
|
22
|
-
const child2 = makeEntry({ id: "c2", correlation_id: "root-1" });
|
|
23
|
-
const standalone = makeEntry({ id: "root-2", correlation_id: null });
|
|
24
|
-
const chains = extractChains([root, child1, child2, standalone]);
|
|
25
|
-
expect(chains.length).toBe(2);
|
|
26
|
-
const mainChain = chains.find((c) => c.root_id === "root-1");
|
|
27
|
-
expect(mainChain).toBeDefined();
|
|
28
|
-
expect(mainChain.entries.length).toBe(3);
|
|
29
|
-
});
|
|
30
|
-
it("extracts entities from chain entries", () => {
|
|
31
|
-
const root = makeEntry({
|
|
32
|
-
id: "r",
|
|
33
|
-
correlation_id: null,
|
|
34
|
-
args_summary: { key: "src/auth.ts::login" },
|
|
35
|
-
});
|
|
36
|
-
const child = makeEntry({
|
|
37
|
-
id: "c",
|
|
38
|
-
correlation_id: "r",
|
|
39
|
-
args_summary: { file_path: "src/auth.ts" },
|
|
40
|
-
});
|
|
41
|
-
const chains = extractChains([root, child]);
|
|
42
|
-
expect(chains[0].entities_touched).toContain("src/auth.ts::login");
|
|
43
|
-
expect(chains[0].entities_touched).toContain("src/auth.ts");
|
|
44
|
-
});
|
|
45
|
-
it("classifies reverted chains", () => {
|
|
46
|
-
const root = makeEntry({
|
|
47
|
-
id: "r",
|
|
48
|
-
correlation_id: null,
|
|
49
|
-
tool: "get_function",
|
|
50
|
-
});
|
|
51
|
-
const revert = makeEntry({
|
|
52
|
-
id: "c",
|
|
53
|
-
correlation_id: "r",
|
|
54
|
-
tool: "unerr_revert_to_working_state",
|
|
55
|
-
});
|
|
56
|
-
const chains = extractChains([root, revert]);
|
|
57
|
-
expect(chains[0].outcome).toBe("reverted");
|
|
58
|
-
});
|
|
59
|
-
it("classifies modified chains (sync_local_diff present)", () => {
|
|
60
|
-
const root = makeEntry({
|
|
61
|
-
id: "r",
|
|
62
|
-
correlation_id: null,
|
|
63
|
-
tool: "get_function",
|
|
64
|
-
});
|
|
65
|
-
const sync = makeEntry({
|
|
66
|
-
id: "c",
|
|
67
|
-
correlation_id: "r",
|
|
68
|
-
tool: "sync_local_diff",
|
|
69
|
-
});
|
|
70
|
-
const chains = extractChains([root, sync]);
|
|
71
|
-
expect(chains[0].outcome).toBe("modified");
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe("getEntityHistory", () => {
|
|
75
|
-
it("returns chains that touched a specific entity", () => {
|
|
76
|
-
const entries = [
|
|
77
|
-
makeEntry({
|
|
78
|
-
id: "r1",
|
|
79
|
-
correlation_id: null,
|
|
80
|
-
args_summary: { key: "src/proxy.ts::startProxy" },
|
|
81
|
-
}),
|
|
82
|
-
makeEntry({
|
|
83
|
-
id: "r2",
|
|
84
|
-
correlation_id: null,
|
|
85
|
-
args_summary: { key: "src/auth.ts::login" },
|
|
86
|
-
}),
|
|
87
|
-
];
|
|
88
|
-
const history = getEntityHistory(entries, "src/proxy.ts");
|
|
89
|
-
expect(history.length).toBe(1);
|
|
90
|
-
expect(history[0].entities_touched).toContain("src/proxy.ts::startProxy");
|
|
91
|
-
});
|
|
92
|
-
it("limits results", () => {
|
|
93
|
-
const entries = Array.from({ length: 30 }, (_, i) => makeEntry({
|
|
94
|
-
id: `r${i}`,
|
|
95
|
-
correlation_id: null,
|
|
96
|
-
args_summary: { file_path: "src/hot.ts" },
|
|
97
|
-
}));
|
|
98
|
-
const history = getEntityHistory(entries, "src/hot.ts", 5);
|
|
99
|
-
expect(history.length).toBe(5);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe("getRevertPatterns", () => {
|
|
103
|
-
it("detects tool sequences that lead to reverts", () => {
|
|
104
|
-
const entries = [];
|
|
105
|
-
// Create 3 chains with same tool sequence, 2 reverted
|
|
106
|
-
for (let i = 0; i < 3; i++) {
|
|
107
|
-
const rootId = `root-${i}`;
|
|
108
|
-
entries.push(makeEntry({ id: rootId, correlation_id: null, tool: "get_function" }), makeEntry({
|
|
109
|
-
id: `c${i}-1`,
|
|
110
|
-
correlation_id: rootId,
|
|
111
|
-
tool: "file_read",
|
|
112
|
-
}), makeEntry({
|
|
113
|
-
id: `c${i}-2`,
|
|
114
|
-
correlation_id: rootId,
|
|
115
|
-
tool: i < 2 ? "unerr_revert_to_working_state" : "get_callers",
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
118
|
-
const patterns = getRevertPatterns(entries, 2);
|
|
119
|
-
expect(patterns.length).toBeGreaterThan(0);
|
|
120
|
-
const mainPattern = patterns.find((p) => p.tool_sequence.includes("get_function"));
|
|
121
|
-
expect(mainPattern).toBeDefined();
|
|
122
|
-
expect(mainPattern.revert_rate).toBeGreaterThan(0);
|
|
123
|
-
});
|
|
124
|
-
it("returns empty when no reverts", () => {
|
|
125
|
-
const entries = [
|
|
126
|
-
makeEntry({ id: "r1", correlation_id: null, tool: "get_function" }),
|
|
127
|
-
makeEntry({ id: "r2", correlation_id: null, tool: "get_callers" }),
|
|
128
|
-
];
|
|
129
|
-
const patterns = getRevertPatterns(entries);
|
|
130
|
-
expect(patterns.length).toBe(0);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe("getSessionTimeline", () => {
|
|
134
|
-
it("groups entries by session_id", () => {
|
|
135
|
-
const entries = [
|
|
136
|
-
makeEntry({ session_id: "s1", tool: "get_function" }),
|
|
137
|
-
makeEntry({ session_id: "s1", tool: "get_callers" }),
|
|
138
|
-
makeEntry({ session_id: "s2", tool: "search_code" }),
|
|
139
|
-
];
|
|
140
|
-
const timeline = getSessionTimeline(entries);
|
|
141
|
-
expect(timeline.length).toBe(2);
|
|
142
|
-
const s1 = timeline.find((s) => s.session_id === "s1");
|
|
143
|
-
expect(s1).toBeDefined();
|
|
144
|
-
expect(s1.tool_calls).toBe(2);
|
|
145
|
-
expect(s1.tools_used.get_function).toBe(1);
|
|
146
|
-
});
|
|
147
|
-
it("counts facts recorded", () => {
|
|
148
|
-
const entries = [
|
|
149
|
-
makeEntry({ session_id: "s1", tool: "record_fact" }),
|
|
150
|
-
makeEntry({ session_id: "s1", tool: "record_fact" }),
|
|
151
|
-
makeEntry({ session_id: "s1", tool: "get_function" }),
|
|
152
|
-
];
|
|
153
|
-
const timeline = getSessionTimeline(entries);
|
|
154
|
-
expect(timeline[0].facts_recorded).toBe(2);
|
|
155
|
-
});
|
|
156
|
-
it("respects count limit", () => {
|
|
157
|
-
const entries = Array.from({ length: 30 }, (_, i) => makeEntry({ session_id: `s${i}` }));
|
|
158
|
-
const timeline = getSessionTimeline(entries, 3);
|
|
159
|
-
expect(timeline.length).toBe(3);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { createActor } from "xstate";
|
|
3
|
-
import { createLifecycleActor } from "../proxy/lifecycle-actor.js";
|
|
4
|
-
import { proxyMachine, } from "../proxy/lifecycle-machine.js";
|
|
5
|
-
function makeActor() {
|
|
6
|
-
return createActor(proxyMachine, { input: { repoPath: "/test/repo" } });
|
|
7
|
-
}
|
|
8
|
-
function stateOf(actor) {
|
|
9
|
-
return actor.getSnapshot().value;
|
|
10
|
-
}
|
|
11
|
-
describe("Proxy Lifecycle Machine", () => {
|
|
12
|
-
it("starts in idle state", () => {
|
|
13
|
-
const actor = makeActor();
|
|
14
|
-
actor.start();
|
|
15
|
-
expect(stateOf(actor)).toBe("idle");
|
|
16
|
-
actor.stop();
|
|
17
|
-
});
|
|
18
|
-
it("transitions idle → detecting on START_DETECT", () => {
|
|
19
|
-
const actor = makeActor();
|
|
20
|
-
actor.start();
|
|
21
|
-
actor.send({ type: "START_DETECT" });
|
|
22
|
-
expect(stateOf(actor)).toBe("detecting");
|
|
23
|
-
actor.stop();
|
|
24
|
-
});
|
|
25
|
-
it("transitions detecting → setup when needsSetup is true", () => {
|
|
26
|
-
const actor = makeActor();
|
|
27
|
-
actor.start();
|
|
28
|
-
actor.send({ type: "START_DETECT" });
|
|
29
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: true });
|
|
30
|
-
expect(stateOf(actor)).toBe("setup");
|
|
31
|
-
actor.stop();
|
|
32
|
-
});
|
|
33
|
-
it("transitions detecting → indexing when needsSetup is false", () => {
|
|
34
|
-
const actor = makeActor();
|
|
35
|
-
actor.start();
|
|
36
|
-
actor.send({ type: "START_DETECT" });
|
|
37
|
-
actor.send({
|
|
38
|
-
type: "DETECT_COMPLETE",
|
|
39
|
-
needsSetup: false,
|
|
40
|
-
repoId: "repo-123",
|
|
41
|
-
});
|
|
42
|
-
expect(stateOf(actor)).toBe("indexing");
|
|
43
|
-
expect(actor.getSnapshot().context.repoId).toBe("repo-123");
|
|
44
|
-
actor.stop();
|
|
45
|
-
});
|
|
46
|
-
it("transitions setup → indexing on SETUP_COMPLETE", () => {
|
|
47
|
-
const actor = makeActor();
|
|
48
|
-
actor.start();
|
|
49
|
-
actor.send({ type: "START_DETECT" });
|
|
50
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: true });
|
|
51
|
-
actor.send({ type: "SETUP_COMPLETE", repoId: "new-repo" });
|
|
52
|
-
expect(stateOf(actor)).toBe("indexing");
|
|
53
|
-
expect(actor.getSnapshot().context.repoId).toBe("new-repo");
|
|
54
|
-
actor.stop();
|
|
55
|
-
});
|
|
56
|
-
it("transitions indexing → ready on INDEX_COMPLETE", () => {
|
|
57
|
-
const actor = makeActor();
|
|
58
|
-
actor.start();
|
|
59
|
-
actor.send({ type: "START_DETECT" });
|
|
60
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
61
|
-
actor.send({ type: "INDEX_COMPLETE" });
|
|
62
|
-
expect(stateOf(actor)).toBe("ready");
|
|
63
|
-
actor.stop();
|
|
64
|
-
});
|
|
65
|
-
it("transitions ready → running on MCP_READY", () => {
|
|
66
|
-
const actor = makeActor();
|
|
67
|
-
actor.start();
|
|
68
|
-
actor.send({ type: "START_DETECT" });
|
|
69
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
70
|
-
actor.send({ type: "INDEX_COMPLETE" });
|
|
71
|
-
actor.send({ type: "MCP_READY" });
|
|
72
|
-
expect(stateOf(actor)).toBe("running");
|
|
73
|
-
expect(actor.getSnapshot().context.mcpReady).toBe(true);
|
|
74
|
-
actor.stop();
|
|
75
|
-
});
|
|
76
|
-
it("handles GRAPH_LOADED during indexing without state change", () => {
|
|
77
|
-
const actor = makeActor();
|
|
78
|
-
actor.start();
|
|
79
|
-
actor.send({ type: "START_DETECT" });
|
|
80
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
81
|
-
actor.send({ type: "GRAPH_LOADED" });
|
|
82
|
-
expect(stateOf(actor)).toBe("indexing");
|
|
83
|
-
expect(actor.getSnapshot().context.graphLoaded).toBe(true);
|
|
84
|
-
actor.stop();
|
|
85
|
-
});
|
|
86
|
-
it("transitions running → shutdown on SHUTDOWN", () => {
|
|
87
|
-
const actor = makeActor();
|
|
88
|
-
actor.start();
|
|
89
|
-
actor.send({ type: "START_DETECT" });
|
|
90
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
91
|
-
actor.send({ type: "INDEX_COMPLETE" });
|
|
92
|
-
actor.send({ type: "MCP_READY" });
|
|
93
|
-
actor.send({ type: "SHUTDOWN" });
|
|
94
|
-
expect(stateOf(actor)).toBe("shutdown");
|
|
95
|
-
expect(actor.getSnapshot().status).toBe("done");
|
|
96
|
-
actor.stop();
|
|
97
|
-
});
|
|
98
|
-
it("transitions running → error on ERROR", () => {
|
|
99
|
-
const actor = makeActor();
|
|
100
|
-
actor.start();
|
|
101
|
-
actor.send({ type: "START_DETECT" });
|
|
102
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
103
|
-
actor.send({ type: "INDEX_COMPLETE" });
|
|
104
|
-
actor.send({ type: "MCP_READY" });
|
|
105
|
-
actor.send({ type: "ERROR", message: "something broke" });
|
|
106
|
-
expect(stateOf(actor)).toBe("error");
|
|
107
|
-
expect(actor.getSnapshot().context.error).toBe("something broke");
|
|
108
|
-
actor.stop();
|
|
109
|
-
});
|
|
110
|
-
it("transitions error → shutdown on SHUTDOWN", () => {
|
|
111
|
-
const actor = makeActor();
|
|
112
|
-
actor.start();
|
|
113
|
-
actor.send({ type: "START_DETECT" });
|
|
114
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
115
|
-
actor.send({ type: "INDEX_COMPLETE" });
|
|
116
|
-
actor.send({ type: "MCP_READY" });
|
|
117
|
-
actor.send({ type: "ERROR", message: "fail" });
|
|
118
|
-
actor.send({ type: "SHUTDOWN" });
|
|
119
|
-
expect(stateOf(actor)).toBe("shutdown");
|
|
120
|
-
actor.stop();
|
|
121
|
-
});
|
|
122
|
-
it("handles ERROR during indexing", () => {
|
|
123
|
-
const actor = makeActor();
|
|
124
|
-
actor.start();
|
|
125
|
-
actor.send({ type: "START_DETECT" });
|
|
126
|
-
actor.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
127
|
-
actor.send({ type: "ERROR", message: "index failure" });
|
|
128
|
-
expect(stateOf(actor)).toBe("error");
|
|
129
|
-
expect(actor.getSnapshot().context.error).toBe("index failure");
|
|
130
|
-
actor.stop();
|
|
131
|
-
});
|
|
132
|
-
it("handles ERROR during detecting", () => {
|
|
133
|
-
const actor = makeActor();
|
|
134
|
-
actor.start();
|
|
135
|
-
actor.send({ type: "START_DETECT" });
|
|
136
|
-
actor.send({ type: "ERROR", message: "git not found" });
|
|
137
|
-
expect(stateOf(actor)).toBe("error");
|
|
138
|
-
actor.stop();
|
|
139
|
-
});
|
|
140
|
-
it("records bootStartedAt in context", () => {
|
|
141
|
-
const before = Date.now();
|
|
142
|
-
const actor = makeActor();
|
|
143
|
-
actor.start();
|
|
144
|
-
const after = Date.now();
|
|
145
|
-
const ctx = actor.getSnapshot().context;
|
|
146
|
-
expect(ctx.bootStartedAt).toBeGreaterThanOrEqual(before);
|
|
147
|
-
expect(ctx.bootStartedAt).toBeLessThanOrEqual(after);
|
|
148
|
-
actor.stop();
|
|
149
|
-
});
|
|
150
|
-
it("ignores invalid events for current state", () => {
|
|
151
|
-
const actor = makeActor();
|
|
152
|
-
actor.start();
|
|
153
|
-
actor.send({ type: "MCP_READY" });
|
|
154
|
-
expect(stateOf(actor)).toBe("idle");
|
|
155
|
-
actor.stop();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe("Lifecycle Actor wrapper", () => {
|
|
159
|
-
it("provides imperative getState() API", () => {
|
|
160
|
-
const lifecycle = createLifecycleActor("/test");
|
|
161
|
-
expect(lifecycle.getState()).toBe("idle");
|
|
162
|
-
lifecycle.send({ type: "START_DETECT" });
|
|
163
|
-
expect(lifecycle.getState()).toBe("detecting");
|
|
164
|
-
lifecycle.stop();
|
|
165
|
-
});
|
|
166
|
-
it("provides getSnapshot() with value and context", () => {
|
|
167
|
-
const lifecycle = createLifecycleActor("/test");
|
|
168
|
-
const snap = lifecycle.getSnapshot();
|
|
169
|
-
expect(snap.value).toBe("idle");
|
|
170
|
-
expect(snap.context.repoPath).toBe("/test");
|
|
171
|
-
lifecycle.stop();
|
|
172
|
-
});
|
|
173
|
-
it("subscribe() receives state transition notifications", () => {
|
|
174
|
-
const lifecycle = createLifecycleActor("/test");
|
|
175
|
-
const states = [];
|
|
176
|
-
const sub = lifecycle.subscribe((state) => {
|
|
177
|
-
states.push(state);
|
|
178
|
-
});
|
|
179
|
-
lifecycle.send({ type: "START_DETECT" });
|
|
180
|
-
lifecycle.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
181
|
-
expect(states).toContain("detecting");
|
|
182
|
-
expect(states).toContain("indexing");
|
|
183
|
-
sub.unsubscribe();
|
|
184
|
-
lifecycle.stop();
|
|
185
|
-
});
|
|
186
|
-
it("waitForState() resolves when target state is reached", async () => {
|
|
187
|
-
const lifecycle = createLifecycleActor("/test");
|
|
188
|
-
lifecycle.send({ type: "START_DETECT" });
|
|
189
|
-
const promise = lifecycle.waitForState("indexing", 1000);
|
|
190
|
-
lifecycle.send({ type: "DETECT_COMPLETE", needsSetup: false });
|
|
191
|
-
await expect(promise).resolves.toBeUndefined();
|
|
192
|
-
lifecycle.stop();
|
|
193
|
-
});
|
|
194
|
-
it("waitForState() resolves immediately if already in target state", async () => {
|
|
195
|
-
const lifecycle = createLifecycleActor("/test");
|
|
196
|
-
await lifecycle.waitForState("idle", 100);
|
|
197
|
-
lifecycle.stop();
|
|
198
|
-
});
|
|
199
|
-
it("full happy path: idle → running", () => {
|
|
200
|
-
const lifecycle = createLifecycleActor("/test");
|
|
201
|
-
lifecycle.send({ type: "START_DETECT" });
|
|
202
|
-
lifecycle.send({
|
|
203
|
-
type: "DETECT_COMPLETE",
|
|
204
|
-
needsSetup: false,
|
|
205
|
-
repoId: "r1",
|
|
206
|
-
});
|
|
207
|
-
lifecycle.send({ type: "GRAPH_LOADED" });
|
|
208
|
-
lifecycle.send({ type: "INDEX_COMPLETE" });
|
|
209
|
-
lifecycle.send({ type: "MCP_READY" });
|
|
210
|
-
expect(lifecycle.getState()).toBe("running");
|
|
211
|
-
expect(lifecycle.getContext().graphLoaded).toBe(true);
|
|
212
|
-
expect(lifecycle.getContext().mcpReady).toBe(true);
|
|
213
|
-
lifecycle.stop();
|
|
214
|
-
});
|
|
215
|
-
it("full setup path: idle → setup → running", () => {
|
|
216
|
-
const lifecycle = createLifecycleActor("/test");
|
|
217
|
-
lifecycle.send({ type: "START_DETECT" });
|
|
218
|
-
lifecycle.send({ type: "DETECT_COMPLETE", needsSetup: true });
|
|
219
|
-
lifecycle.send({ type: "SETUP_COMPLETE", repoId: "new-repo" });
|
|
220
|
-
lifecycle.send({ type: "INDEX_COMPLETE" });
|
|
221
|
-
lifecycle.send({ type: "MCP_READY" });
|
|
222
|
-
expect(lifecycle.getState()).toBe("running");
|
|
223
|
-
expect(lifecycle.getContext().repoId).toBe("new-repo");
|
|
224
|
-
lifecycle.stop();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Sprint L8.1 — LocalChatProvider + ChatProvider interface.
|
|
3
|
-
*/
|
|
4
|
-
import http from "node:http";
|
|
5
|
-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
6
|
-
import { LocalChatProvider, toChatToolDefs, } from "../core/local-chat-provider.js";
|
|
7
|
-
// ── Mock Ollama Server ──────────────────────────────────────
|
|
8
|
-
function createMockOllamaServer() {
|
|
9
|
-
return http.createServer((req, res) => {
|
|
10
|
-
if (req.url === "/api/chat" && req.method === "POST") {
|
|
11
|
-
let body = "";
|
|
12
|
-
req.on("data", (chunk) => {
|
|
13
|
-
body += chunk;
|
|
14
|
-
});
|
|
15
|
-
req.on("end", () => {
|
|
16
|
-
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
17
|
-
// Stream two chunks then done
|
|
18
|
-
res.write(`${JSON.stringify({
|
|
19
|
-
message: { role: "assistant", content: "Hello " },
|
|
20
|
-
done: false,
|
|
21
|
-
})}\n`);
|
|
22
|
-
res.write(`${JSON.stringify({
|
|
23
|
-
message: { role: "assistant", content: "world!" },
|
|
24
|
-
done: true,
|
|
25
|
-
eval_count: 10,
|
|
26
|
-
prompt_eval_count: 5,
|
|
27
|
-
})}\n`);
|
|
28
|
-
res.end();
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
else if (req.url === "/api/tags") {
|
|
32
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
33
|
-
res.end(JSON.stringify({ models: [{ name: "llama3" }] }));
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
res.writeHead(404);
|
|
37
|
-
res.end();
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
// ── Mock OpenAI-Compatible Server ───────────────────────────
|
|
42
|
-
function createMockOpenAiServer() {
|
|
43
|
-
return http.createServer((req, res) => {
|
|
44
|
-
if (req.url === "/v1/chat/completions" && req.method === "POST") {
|
|
45
|
-
res.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
46
|
-
res.write('data: {"choices":[{"delta":{"content":"Hi "},"index":0}]}\n\n');
|
|
47
|
-
res.write('data: {"choices":[{"delta":{"content":"there!"},"index":0}]}\n\n');
|
|
48
|
-
res.write("data: [DONE]\n\n");
|
|
49
|
-
res.end();
|
|
50
|
-
}
|
|
51
|
-
else if (req.url === "/v1/models") {
|
|
52
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
53
|
-
res.end(JSON.stringify({ data: [{ id: "llama3" }] }));
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
res.writeHead(404);
|
|
57
|
-
res.end();
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
// ── Tests ───────────────────────────────────────────────────
|
|
62
|
-
describe("LocalChatProvider — Ollama (L8.1)", () => {
|
|
63
|
-
let server;
|
|
64
|
-
let port;
|
|
65
|
-
beforeAll(async () => {
|
|
66
|
-
server = createMockOllamaServer();
|
|
67
|
-
await new Promise((resolve) => {
|
|
68
|
-
server.listen(0, "127.0.0.1", () => resolve());
|
|
69
|
-
});
|
|
70
|
-
port = server.address().port;
|
|
71
|
-
});
|
|
72
|
-
afterAll(async () => {
|
|
73
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
74
|
-
});
|
|
75
|
-
it("streams chat response from Ollama /api/chat", async () => {
|
|
76
|
-
const config = {
|
|
77
|
-
provider: "ollama",
|
|
78
|
-
baseUrl: `http://127.0.0.1:${port}`,
|
|
79
|
-
chatModel: "llama3",
|
|
80
|
-
embeddingModel: "nomic-embed-text",
|
|
81
|
-
embeddingDimensions: 384,
|
|
82
|
-
maxConcurrency: 2,
|
|
83
|
-
};
|
|
84
|
-
const provider = new LocalChatProvider(config);
|
|
85
|
-
expect(provider.providerName).toBe("ollama");
|
|
86
|
-
expect(provider.modelId).toBe("llama3");
|
|
87
|
-
const chunks = [];
|
|
88
|
-
const response = await provider.streamChat([{ role: "user", content: "Hello" }], [], "You are a helpful assistant.", 1024, (chunk) => chunks.push(chunk));
|
|
89
|
-
expect(response.text).toBe("Hello world!");
|
|
90
|
-
expect(response.toolCalls).toHaveLength(0);
|
|
91
|
-
expect(response.outputTokens).toBe(10);
|
|
92
|
-
expect(response.inputTokens).toBe(5);
|
|
93
|
-
const textChunks = chunks.filter((c) => c.type === "text_delta");
|
|
94
|
-
expect(textChunks).toHaveLength(2);
|
|
95
|
-
expect(textChunks[0]?.text).toBe("Hello ");
|
|
96
|
-
expect(textChunks[1]?.text).toBe("world!");
|
|
97
|
-
expect(chunks[chunks.length - 1]?.type).toBe("done");
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
describe("LocalChatProvider — OpenAI-Compatible (L8.1)", () => {
|
|
101
|
-
let server;
|
|
102
|
-
let port;
|
|
103
|
-
beforeAll(async () => {
|
|
104
|
-
server = createMockOpenAiServer();
|
|
105
|
-
await new Promise((resolve) => {
|
|
106
|
-
server.listen(0, "127.0.0.1", () => resolve());
|
|
107
|
-
});
|
|
108
|
-
port = server.address().port;
|
|
109
|
-
});
|
|
110
|
-
afterAll(async () => {
|
|
111
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
112
|
-
});
|
|
113
|
-
it("streams chat response from OpenAI-compatible SSE endpoint", async () => {
|
|
114
|
-
const config = {
|
|
115
|
-
provider: "lm-studio",
|
|
116
|
-
baseUrl: `http://127.0.0.1:${port}`,
|
|
117
|
-
chatModel: "llama3",
|
|
118
|
-
embeddingModel: "nomic-embed-text",
|
|
119
|
-
embeddingDimensions: 384,
|
|
120
|
-
maxConcurrency: 2,
|
|
121
|
-
};
|
|
122
|
-
const provider = new LocalChatProvider(config);
|
|
123
|
-
expect(provider.providerName).toBe("lm-studio");
|
|
124
|
-
const chunks = [];
|
|
125
|
-
const response = await provider.streamChat([{ role: "user", content: "Hi" }], [], "You are helpful.", 1024, (chunk) => chunks.push(chunk));
|
|
126
|
-
expect(response.text).toBe("Hi there!");
|
|
127
|
-
expect(response.toolCalls).toHaveLength(0);
|
|
128
|
-
const textChunks = chunks.filter((c) => c.type === "text_delta");
|
|
129
|
-
expect(textChunks).toHaveLength(2);
|
|
130
|
-
expect(chunks[chunks.length - 1]?.type).toBe("done");
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe("toChatToolDefs (L8.1)", () => {
|
|
134
|
-
it("converts Tool[] to ChatToolDef[]", () => {
|
|
135
|
-
const tools = [
|
|
136
|
-
{
|
|
137
|
-
name: "get_function",
|
|
138
|
-
description: "Get function details",
|
|
139
|
-
inputSchema: {
|
|
140
|
-
type: "object",
|
|
141
|
-
properties: { key: { type: "string" } },
|
|
142
|
-
required: ["key"],
|
|
143
|
-
},
|
|
144
|
-
isReadOnly: true,
|
|
145
|
-
requiresPermission: false,
|
|
146
|
-
execute: async () => ({ content: "" }),
|
|
147
|
-
},
|
|
148
|
-
];
|
|
149
|
-
const defs = toChatToolDefs(tools);
|
|
150
|
-
expect(defs).toHaveLength(1);
|
|
151
|
-
expect(defs[0]?.type).toBe("function");
|
|
152
|
-
expect(defs[0]?.function.name).toBe("get_function");
|
|
153
|
-
expect(defs[0]?.function.description).toBe("Get function details");
|
|
154
|
-
expect(defs[0]?.function.parameters).toEqual(tools[0]?.inputSchema);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
describe("LocalChatProvider — anthropic-direct (L8.1)", () => {
|
|
158
|
-
it("throws if no API key configured", async () => {
|
|
159
|
-
const config = {
|
|
160
|
-
provider: "anthropic-direct",
|
|
161
|
-
chatModel: "claude-sonnet-4-20250514",
|
|
162
|
-
embeddingModel: "nomic-embed-text",
|
|
163
|
-
embeddingDimensions: 384,
|
|
164
|
-
maxConcurrency: 2,
|
|
165
|
-
};
|
|
166
|
-
const provider = new LocalChatProvider(config);
|
|
167
|
-
expect(provider.providerName).toBe("anthropic-direct");
|
|
168
|
-
await expect(provider.streamChat([{ role: "user", content: "Hello" }], [], "System prompt", 1024, () => { })).rejects.toThrow("anthropic-direct requires an API key");
|
|
169
|
-
});
|
|
170
|
-
});
|