@snoglobe/helios 0.3.5 → 0.4.0
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/dist/__tests__/db-helper.d.ts +8 -0
- package/dist/__tests__/db-helper.d.ts.map +1 -0
- package/dist/__tests__/db-helper.js +15 -0
- package/dist/__tests__/db-helper.js.map +1 -0
- package/dist/config/project.test.d.ts +2 -0
- package/dist/config/project.test.d.ts.map +1 -0
- package/dist/config/project.test.js +219 -0
- package/dist/config/project.test.js.map +1 -0
- package/dist/core/orchestrator-integration.test.d.ts +2 -0
- package/dist/core/orchestrator-integration.test.d.ts.map +1 -0
- package/dist/core/orchestrator-integration.test.js +674 -0
- package/dist/core/orchestrator-integration.test.js.map +1 -0
- package/dist/core/orchestrator.d.ts +3 -1
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +98 -53
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/orchestrator.test.d.ts +2 -0
- package/dist/core/orchestrator.test.d.ts.map +1 -0
- package/dist/core/orchestrator.test.js +982 -0
- package/dist/core/orchestrator.test.js.map +1 -0
- package/dist/core/state-machine.d.ts.map +1 -1
- package/dist/core/state-machine.js +3 -0
- package/dist/core/state-machine.js.map +1 -1
- package/dist/core/state-machine.test.d.ts +2 -0
- package/dist/core/state-machine.test.d.ts.map +1 -0
- package/dist/core/state-machine.test.js +95 -0
- package/dist/core/state-machine.test.js.map +1 -0
- package/dist/core/stickies.d.ts.map +1 -1
- package/dist/core/stickies.js +3 -0
- package/dist/core/stickies.js.map +1 -1
- package/dist/core/stickies.test.d.ts +2 -0
- package/dist/core/stickies.test.d.ts.map +1 -0
- package/dist/core/stickies.test.js +63 -0
- package/dist/core/stickies.test.js.map +1 -0
- package/dist/core/task-poller.d.ts.map +1 -1
- package/dist/core/task-poller.js +2 -1
- package/dist/core/task-poller.js.map +1 -1
- package/dist/init.d.ts +2 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +19 -0
- package/dist/init.js.map +1 -1
- package/dist/memory/context-gate.test.d.ts +2 -0
- package/dist/memory/context-gate.test.d.ts.map +1 -0
- package/dist/memory/context-gate.test.js +318 -0
- package/dist/memory/context-gate.test.js.map +1 -0
- package/dist/memory/experiment-tracker.test.d.ts +2 -0
- package/dist/memory/experiment-tracker.test.d.ts.map +1 -0
- package/dist/memory/experiment-tracker.test.js +325 -0
- package/dist/memory/experiment-tracker.test.js.map +1 -0
- package/dist/memory/memory-store.d.ts +2 -0
- package/dist/memory/memory-store.d.ts.map +1 -1
- package/dist/memory/memory-store.js +35 -28
- package/dist/memory/memory-store.js.map +1 -1
- package/dist/memory/memory-store.test.d.ts +2 -0
- package/dist/memory/memory-store.test.d.ts.map +1 -0
- package/dist/memory/memory-store.test.js +144 -0
- package/dist/memory/memory-store.test.js.map +1 -0
- package/dist/memory/token-estimator.test.d.ts +2 -0
- package/dist/memory/token-estimator.test.d.ts.map +1 -0
- package/dist/memory/token-estimator.test.js +42 -0
- package/dist/memory/token-estimator.test.js.map +1 -0
- package/dist/metrics/analyzer.test.d.ts +2 -0
- package/dist/metrics/analyzer.test.d.ts.map +1 -0
- package/dist/metrics/analyzer.test.js +65 -0
- package/dist/metrics/analyzer.test.js.map +1 -0
- package/dist/metrics/parser.test.d.ts +2 -0
- package/dist/metrics/parser.test.d.ts.map +1 -0
- package/dist/metrics/parser.test.js +106 -0
- package/dist/metrics/parser.test.js.map +1 -0
- package/dist/metrics/store.d.ts +2 -0
- package/dist/metrics/store.d.ts.map +1 -1
- package/dist/metrics/store.js +26 -50
- package/dist/metrics/store.js.map +1 -1
- package/dist/metrics/store.test.d.ts +2 -0
- package/dist/metrics/store.test.d.ts.map +1 -0
- package/dist/metrics/store.test.js +148 -0
- package/dist/metrics/store.test.js.map +1 -0
- package/dist/paths.js +5 -5
- package/dist/paths.js.map +1 -1
- package/dist/providers/auth/auth-manager.d.ts +1 -0
- package/dist/providers/auth/auth-manager.d.ts.map +1 -1
- package/dist/providers/auth/auth-manager.js +8 -1
- package/dist/providers/auth/auth-manager.js.map +1 -1
- package/dist/providers/auth/auth-manager.test.d.ts +2 -0
- package/dist/providers/auth/auth-manager.test.d.ts.map +1 -0
- package/dist/providers/auth/auth-manager.test.js +364 -0
- package/dist/providers/auth/auth-manager.test.js.map +1 -0
- package/dist/providers/auth/token-store.js +2 -2
- package/dist/providers/auth/token-store.js.map +1 -1
- package/dist/providers/claude/history.test.d.ts +2 -0
- package/dist/providers/claude/history.test.d.ts.map +1 -0
- package/dist/providers/claude/history.test.js +755 -0
- package/dist/providers/claude/history.test.js.map +1 -0
- package/dist/providers/claude/provider.d.ts +4 -2
- package/dist/providers/claude/provider.d.ts.map +1 -1
- package/dist/providers/claude/provider.js +44 -22
- package/dist/providers/claude/provider.js.map +1 -1
- package/dist/providers/claude/provider.test.d.ts +2 -0
- package/dist/providers/claude/provider.test.d.ts.map +1 -0
- package/dist/providers/claude/provider.test.js +1167 -0
- package/dist/providers/claude/provider.test.js.map +1 -0
- package/dist/providers/openai/history.test.d.ts +2 -0
- package/dist/providers/openai/history.test.d.ts.map +1 -0
- package/dist/providers/openai/history.test.js +655 -0
- package/dist/providers/openai/history.test.js.map +1 -0
- package/dist/providers/openai/provider.d.ts +3 -2
- package/dist/providers/openai/provider.d.ts.map +1 -1
- package/dist/providers/openai/provider.js +18 -11
- package/dist/providers/openai/provider.js.map +1 -1
- package/dist/providers/openai/provider.test.d.ts +2 -0
- package/dist/providers/openai/provider.test.d.ts.map +1 -0
- package/dist/providers/openai/provider.test.js +1089 -0
- package/dist/providers/openai/provider.test.js.map +1 -0
- package/dist/providers/retry.test.d.ts +2 -0
- package/dist/providers/retry.test.d.ts.map +1 -0
- package/dist/providers/retry.test.js +194 -0
- package/dist/providers/retry.test.js.map +1 -0
- package/dist/providers/sse.d.ts +1 -0
- package/dist/providers/sse.d.ts.map +1 -1
- package/dist/providers/sse.js +29 -20
- package/dist/providers/sse.js.map +1 -1
- package/dist/providers/sse.test.d.ts +2 -0
- package/dist/providers/sse.test.d.ts.map +1 -0
- package/dist/providers/sse.test.js +79 -0
- package/dist/providers/sse.test.js.map +1 -0
- package/dist/providers/types.d.ts +1 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.test.d.ts +2 -0
- package/dist/providers/types.test.d.ts.map +1 -0
- package/dist/providers/types.test.js +112 -0
- package/dist/providers/types.test.js.map +1 -0
- package/dist/remote/connection-pool.d.ts.map +1 -1
- package/dist/remote/connection-pool.js +24 -3
- package/dist/remote/connection-pool.js.map +1 -1
- package/dist/remote/file-sync.d.ts.map +1 -1
- package/dist/remote/file-sync.js +4 -3
- package/dist/remote/file-sync.js.map +1 -1
- package/dist/scheduler/sleep-manager.d.ts.map +1 -1
- package/dist/scheduler/sleep-manager.js +8 -1
- package/dist/scheduler/sleep-manager.js.map +1 -1
- package/dist/scheduler/sleep-manager.test.d.ts +2 -0
- package/dist/scheduler/sleep-manager.test.d.ts.map +1 -0
- package/dist/scheduler/sleep-manager.test.js +491 -0
- package/dist/scheduler/sleep-manager.test.js.map +1 -0
- package/dist/scheduler/ssh-batcher.d.ts.map +1 -1
- package/dist/scheduler/ssh-batcher.js +6 -4
- package/dist/scheduler/ssh-batcher.js.map +1 -1
- package/dist/scheduler/ssh-batcher.test.d.ts +2 -0
- package/dist/scheduler/ssh-batcher.test.d.ts.map +1 -0
- package/dist/scheduler/ssh-batcher.test.js +76 -0
- package/dist/scheduler/ssh-batcher.test.js.map +1 -0
- package/dist/scheduler/state-store.d.ts.map +1 -1
- package/dist/scheduler/state-store.js +1 -2
- package/dist/scheduler/state-store.js.map +1 -1
- package/dist/scheduler/trigger-scheduler.d.ts.map +1 -1
- package/dist/scheduler/trigger-scheduler.js +59 -36
- package/dist/scheduler/trigger-scheduler.js.map +1 -1
- package/dist/scheduler/trigger-scheduler.test.d.ts +2 -0
- package/dist/scheduler/trigger-scheduler.test.d.ts.map +1 -0
- package/dist/scheduler/trigger-scheduler.test.js +483 -0
- package/dist/scheduler/trigger-scheduler.test.js.map +1 -0
- package/dist/scheduler/triggers/file.d.ts.map +1 -1
- package/dist/scheduler/triggers/file.js +12 -3
- package/dist/scheduler/triggers/file.js.map +1 -1
- package/dist/scheduler/triggers/file.test.d.ts +2 -0
- package/dist/scheduler/triggers/file.test.d.ts.map +1 -0
- package/dist/scheduler/triggers/file.test.js +294 -0
- package/dist/scheduler/triggers/file.test.js.map +1 -0
- package/dist/scheduler/triggers/metric.d.ts +3 -1
- package/dist/scheduler/triggers/metric.d.ts.map +1 -1
- package/dist/scheduler/triggers/metric.js +12 -8
- package/dist/scheduler/triggers/metric.js.map +1 -1
- package/dist/scheduler/triggers/metric.test.d.ts +2 -0
- package/dist/scheduler/triggers/metric.test.d.ts.map +1 -0
- package/dist/scheduler/triggers/metric.test.js +533 -0
- package/dist/scheduler/triggers/metric.test.js.map +1 -0
- package/dist/scheduler/triggers/process-exit.d.ts.map +1 -1
- package/dist/scheduler/triggers/process-exit.js +2 -1
- package/dist/scheduler/triggers/process-exit.js.map +1 -1
- package/dist/scheduler/triggers/process-exit.test.d.ts +2 -0
- package/dist/scheduler/triggers/process-exit.test.d.ts.map +1 -0
- package/dist/scheduler/triggers/process-exit.test.js +118 -0
- package/dist/scheduler/triggers/process-exit.test.js.map +1 -0
- package/dist/scheduler/triggers/resource.d.ts.map +1 -1
- package/dist/scheduler/triggers/resource.js +2 -10
- package/dist/scheduler/triggers/resource.js.map +1 -1
- package/dist/scheduler/triggers/resource.test.d.ts +2 -0
- package/dist/scheduler/triggers/resource.test.d.ts.map +1 -0
- package/dist/scheduler/triggers/resource.test.js +225 -0
- package/dist/scheduler/triggers/resource.test.js.map +1 -0
- package/dist/scheduler/triggers/timer.test.d.ts +2 -0
- package/dist/scheduler/triggers/timer.test.d.ts.map +1 -0
- package/dist/scheduler/triggers/timer.test.js +56 -0
- package/dist/scheduler/triggers/timer.test.js.map +1 -0
- package/dist/scheduler/triggers/types.d.ts +4 -2
- package/dist/scheduler/triggers/types.d.ts.map +1 -1
- package/dist/scheduler/triggers/types.js +0 -1
- package/dist/scheduler/triggers/types.js.map +1 -1
- package/dist/skills/executor.d.ts.map +1 -1
- package/dist/skills/executor.js +13 -15
- package/dist/skills/executor.js.map +1 -1
- package/dist/store/database.d.ts +5 -0
- package/dist/store/database.d.ts.map +1 -1
- package/dist/store/database.js +17 -1
- package/dist/store/database.js.map +1 -1
- package/dist/store/migrations.test.d.ts +2 -0
- package/dist/store/migrations.test.d.ts.map +1 -0
- package/dist/store/migrations.test.js +278 -0
- package/dist/store/migrations.test.js.map +1 -0
- package/dist/store/session-store-edge.test.d.ts +2 -0
- package/dist/store/session-store-edge.test.d.ts.map +1 -0
- package/dist/store/session-store-edge.test.js +522 -0
- package/dist/store/session-store-edge.test.js.map +1 -0
- package/dist/store/session-store.d.ts +7 -0
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +27 -22
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/session-store.test.d.ts +2 -0
- package/dist/store/session-store.test.d.ts.map +1 -0
- package/dist/store/session-store.test.js +125 -0
- package/dist/store/session-store.test.js.map +1 -0
- package/dist/subagent/executor.d.ts +21 -0
- package/dist/subagent/executor.d.ts.map +1 -0
- package/dist/subagent/executor.js +136 -0
- package/dist/subagent/executor.js.map +1 -0
- package/dist/subagent/manager.d.ts +20 -0
- package/dist/subagent/manager.d.ts.map +1 -0
- package/dist/subagent/manager.js +98 -0
- package/dist/subagent/manager.js.map +1 -0
- package/dist/subagent/scoped-memory.d.ts +28 -0
- package/dist/subagent/scoped-memory.d.ts.map +1 -0
- package/dist/subagent/scoped-memory.js +122 -0
- package/dist/subagent/scoped-memory.js.map +1 -0
- package/dist/subagent/types.d.ts +27 -0
- package/dist/subagent/types.d.ts.map +1 -0
- package/dist/subagent/types.js +2 -0
- package/dist/subagent/types.js.map +1 -0
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +23 -13
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/file-ops.test.d.ts +2 -0
- package/dist/tools/file-ops.test.d.ts.map +1 -0
- package/dist/tools/file-ops.test.js +656 -0
- package/dist/tools/file-ops.test.js.map +1 -0
- package/dist/tools/memory-tools.test.d.ts +2 -0
- package/dist/tools/memory-tools.test.d.ts.map +1 -0
- package/dist/tools/memory-tools.test.js +273 -0
- package/dist/tools/memory-tools.test.js.map +1 -0
- package/dist/tools/subagent.d.ts +10 -0
- package/dist/tools/subagent.d.ts.map +1 -0
- package/dist/tools/subagent.js +164 -0
- package/dist/tools/subagent.js.map +1 -0
- package/dist/tools/task-output.d.ts.map +1 -1
- package/dist/tools/task-output.js +13 -2
- package/dist/tools/task-output.js.map +1 -1
- package/dist/ui/components/input-bar.d.ts +1 -1
- package/dist/ui/components/input-bar.d.ts.map +1 -1
- package/dist/ui/components/input-bar.js +3 -3
- package/dist/ui/components/input-bar.js.map +1 -1
- package/dist/ui/components/input-bar.test.d.ts +2 -0
- package/dist/ui/components/input-bar.test.d.ts.map +1 -0
- package/dist/ui/components/input-bar.test.js +380 -0
- package/dist/ui/components/input-bar.test.js.map +1 -0
- package/dist/ui/components/key-hint-rule.d.ts +2 -1
- package/dist/ui/components/key-hint-rule.d.ts.map +1 -1
- package/dist/ui/components/key-hint-rule.js +3 -3
- package/dist/ui/components/key-hint-rule.js.map +1 -1
- package/dist/ui/components/status-bar.d.ts +1 -1
- package/dist/ui/components/status-bar.d.ts.map +1 -1
- package/dist/ui/components/status-bar.js +11 -10
- package/dist/ui/components/status-bar.js.map +1 -1
- package/dist/ui/components/status-bar.test.d.ts +2 -0
- package/dist/ui/components/status-bar.test.d.ts.map +1 -0
- package/dist/ui/components/status-bar.test.js +206 -0
- package/dist/ui/components/status-bar.test.js.map +1 -0
- package/dist/ui/format.d.ts +9 -0
- package/dist/ui/format.d.ts.map +1 -1
- package/dist/ui/format.js +21 -0
- package/dist/ui/format.js.map +1 -1
- package/dist/ui/format.test.d.ts +2 -0
- package/dist/ui/format.test.d.ts.map +1 -0
- package/dist/ui/format.test.js +122 -0
- package/dist/ui/format.test.js.map +1 -0
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +74 -12
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/markdown.test.d.ts +2 -0
- package/dist/ui/markdown.test.d.ts.map +1 -0
- package/dist/ui/markdown.test.js +133 -0
- package/dist/ui/markdown.test.js.map +1 -0
- package/dist/ui/mouse-filter.test.d.ts +2 -0
- package/dist/ui/mouse-filter.test.d.ts.map +1 -0
- package/dist/ui/mouse-filter.test.js +231 -0
- package/dist/ui/mouse-filter.test.js.map +1 -0
- package/dist/ui/overlays/metrics-overlay.test.d.ts +2 -0
- package/dist/ui/overlays/metrics-overlay.test.d.ts.map +1 -0
- package/dist/ui/overlays/metrics-overlay.test.js +248 -0
- package/dist/ui/overlays/metrics-overlay.test.js.map +1 -0
- package/dist/ui/overlays/task-overlay.test.d.ts +2 -0
- package/dist/ui/overlays/task-overlay.test.d.ts.map +1 -0
- package/dist/ui/overlays/task-overlay.test.js +238 -0
- package/dist/ui/overlays/task-overlay.test.js.map +1 -0
- package/dist/ui/panels/conversation.d.ts.map +1 -1
- package/dist/ui/panels/conversation.js +60 -5
- package/dist/ui/panels/conversation.js.map +1 -1
- package/dist/ui/panels/conversation.test.d.ts +2 -0
- package/dist/ui/panels/conversation.test.d.ts.map +1 -0
- package/dist/ui/panels/conversation.test.js +381 -0
- package/dist/ui/panels/conversation.test.js.map +1 -0
- package/dist/ui/panels/metrics-dashboard.test.d.ts +2 -0
- package/dist/ui/panels/metrics-dashboard.test.d.ts.map +1 -0
- package/dist/ui/panels/metrics-dashboard.test.js +191 -0
- package/dist/ui/panels/metrics-dashboard.test.js.map +1 -0
- package/dist/ui/panels/sleep-panel.test.d.ts +2 -0
- package/dist/ui/panels/sleep-panel.test.d.ts.map +1 -0
- package/dist/ui/panels/sleep-panel.test.js +376 -0
- package/dist/ui/panels/sleep-panel.test.js.map +1 -0
- package/dist/ui/panels/task-list.d.ts.map +1 -1
- package/dist/ui/panels/task-list.js +3 -2
- package/dist/ui/panels/task-list.js.map +1 -1
- package/dist/ui/panels/task-list.test.d.ts +2 -0
- package/dist/ui/panels/task-list.test.d.ts.map +1 -0
- package/dist/ui/panels/task-list.test.js +210 -0
- package/dist/ui/panels/task-list.test.js.map +1 -0
- package/dist/ui/theme.test.d.ts +2 -0
- package/dist/ui/theme.test.d.ts.map +1 -0
- package/dist/ui/theme.test.js +159 -0
- package/dist/ui/theme.test.js.map +1 -0
- package/dist/ui/types.d.ts +3 -0
- package/dist/ui/types.d.ts.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDb } from "../__tests__/db-helper.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Module mocks — must come before dynamic import of Orchestrator
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
const mockDb = { current: createTestDb() };
|
|
7
|
+
vi.mock("../store/database.js", () => {
|
|
8
|
+
const getDb = () => mockDb.current;
|
|
9
|
+
class StmtCache {
|
|
10
|
+
cache = new Map();
|
|
11
|
+
stmt(sql) {
|
|
12
|
+
let s = this.cache.get(sql);
|
|
13
|
+
if (!s) {
|
|
14
|
+
s = getDb().prepare(sql);
|
|
15
|
+
this.cache.set(sql, s);
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { getDb, StmtCache, getHeliosDir: () => "/tmp/helios-test" };
|
|
21
|
+
});
|
|
22
|
+
vi.mock("../store/preferences.js", () => ({
|
|
23
|
+
savePreferences: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("../paths.js", () => ({
|
|
26
|
+
debugLog: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
const { Orchestrator } = await import("./orchestrator.js");
|
|
29
|
+
const { SessionStore } = await import("../store/session-store.js");
|
|
30
|
+
const { StickyManager } = await import("./stickies.js");
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Mock provider factory
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Creates a mock ModelProvider. If a SessionStore is provided, createSession
|
|
36
|
+
* will insert the session into the DB so that foreign-key dependent operations
|
|
37
|
+
* (addMessage, addCost, etc.) succeed.
|
|
38
|
+
*/
|
|
39
|
+
function mockProvider(name = "claude", sessionStore) {
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
displayName: name === "claude" ? "Claude" : "OpenAI",
|
|
43
|
+
currentModel: name === "claude" ? "claude-opus-4-6" : "gpt-5.4",
|
|
44
|
+
reasoningEffort: "medium",
|
|
45
|
+
isAuthenticated: vi.fn().mockResolvedValue(true),
|
|
46
|
+
authenticate: vi.fn().mockResolvedValue(undefined),
|
|
47
|
+
createSession: vi.fn().mockImplementation(async (_config) => {
|
|
48
|
+
if (sessionStore) {
|
|
49
|
+
return sessionStore.createSession(name, name === "claude" ? "claude-opus-4-6" : "gpt-5.4");
|
|
50
|
+
}
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
return {
|
|
53
|
+
id: `sess-${Math.random().toString(36).slice(2, 8)}`,
|
|
54
|
+
providerId: name,
|
|
55
|
+
createdAt: now,
|
|
56
|
+
lastActiveAt: now,
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
59
|
+
resumeSession: vi.fn().mockImplementation(async (id) => {
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
providerId: name,
|
|
63
|
+
createdAt: Date.now(),
|
|
64
|
+
lastActiveAt: Date.now(),
|
|
65
|
+
};
|
|
66
|
+
}),
|
|
67
|
+
send: vi.fn().mockImplementation(async function* () {
|
|
68
|
+
yield {
|
|
69
|
+
type: "text",
|
|
70
|
+
text: "Hello",
|
|
71
|
+
delta: "Hello",
|
|
72
|
+
};
|
|
73
|
+
yield {
|
|
74
|
+
type: "done",
|
|
75
|
+
usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.01 },
|
|
76
|
+
};
|
|
77
|
+
}),
|
|
78
|
+
interrupt: vi.fn(),
|
|
79
|
+
resetHistory: vi.fn(),
|
|
80
|
+
closeSession: vi.fn().mockResolvedValue(undefined),
|
|
81
|
+
fetchModels: vi
|
|
82
|
+
.fn()
|
|
83
|
+
.mockResolvedValue([{ id: "test-model", name: "Test Model" }]),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Helpers
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
function makeOrchestrator(defaultProvider = "claude") {
|
|
90
|
+
return new Orchestrator({
|
|
91
|
+
defaultProvider,
|
|
92
|
+
systemPrompt: "You are a test agent.",
|
|
93
|
+
sessionStore: new SessionStore("test-agent"),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create an orchestrator wired to a mock provider whose createSession inserts
|
|
98
|
+
* into the DB (so addMessage / addCost don't violate foreign key constraints).
|
|
99
|
+
*/
|
|
100
|
+
async function makeWiredOrchestrator(providerName = "claude") {
|
|
101
|
+
const orch = makeOrchestrator(providerName);
|
|
102
|
+
const provider = mockProvider(providerName, orch.sessionStore);
|
|
103
|
+
orch.registerProvider(provider);
|
|
104
|
+
await orch.switchProvider(providerName);
|
|
105
|
+
return { orch, provider };
|
|
106
|
+
}
|
|
107
|
+
function makeTool(name) {
|
|
108
|
+
return {
|
|
109
|
+
name,
|
|
110
|
+
description: `Tool ${name}`,
|
|
111
|
+
parameters: { type: "object", properties: {} },
|
|
112
|
+
execute: vi.fn().mockResolvedValue("ok"),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Collect all events from an async generator. */
|
|
116
|
+
async function collectEvents(gen) {
|
|
117
|
+
const events = [];
|
|
118
|
+
for await (const e of gen) {
|
|
119
|
+
events.push(e);
|
|
120
|
+
}
|
|
121
|
+
return events;
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Tests
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
describe("Orchestrator", () => {
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
mockDb.current = createTestDb();
|
|
129
|
+
});
|
|
130
|
+
// =======================================================================
|
|
131
|
+
// Provider Management
|
|
132
|
+
// =======================================================================
|
|
133
|
+
describe("Provider Management", () => {
|
|
134
|
+
it("registerProvider stores provider by name", () => {
|
|
135
|
+
const orch = makeOrchestrator();
|
|
136
|
+
const provider = mockProvider("claude");
|
|
137
|
+
orch.registerProvider(provider);
|
|
138
|
+
expect(orch.getProvider("claude")).toBe(provider);
|
|
139
|
+
});
|
|
140
|
+
it("registerProvider for both claude and openai", () => {
|
|
141
|
+
const orch = makeOrchestrator();
|
|
142
|
+
const claude = mockProvider("claude");
|
|
143
|
+
const openai = mockProvider("openai");
|
|
144
|
+
orch.registerProvider(claude);
|
|
145
|
+
orch.registerProvider(openai);
|
|
146
|
+
expect(orch.getProvider("claude")).toBe(claude);
|
|
147
|
+
expect(orch.getProvider("openai")).toBe(openai);
|
|
148
|
+
});
|
|
149
|
+
it("getProvider returns registered provider by name", () => {
|
|
150
|
+
const orch = makeOrchestrator();
|
|
151
|
+
const provider = mockProvider("openai");
|
|
152
|
+
orch.registerProvider(provider);
|
|
153
|
+
expect(orch.getProvider("openai")).toBe(provider);
|
|
154
|
+
});
|
|
155
|
+
it("getProvider with no args returns active provider", async () => {
|
|
156
|
+
const orch = makeOrchestrator();
|
|
157
|
+
const provider = mockProvider("claude");
|
|
158
|
+
orch.registerProvider(provider);
|
|
159
|
+
// Before switching, no active provider
|
|
160
|
+
expect(orch.getProvider()).toBeNull();
|
|
161
|
+
await orch.switchProvider("claude");
|
|
162
|
+
expect(orch.getProvider()).toBe(provider);
|
|
163
|
+
});
|
|
164
|
+
it("getProvider returns null if not registered", () => {
|
|
165
|
+
const orch = makeOrchestrator();
|
|
166
|
+
expect(orch.getProvider("claude")).toBeNull();
|
|
167
|
+
expect(orch.getProvider("openai")).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
it("switchProvider authenticates and sets active", async () => {
|
|
170
|
+
const orch = makeOrchestrator();
|
|
171
|
+
const provider = mockProvider("claude");
|
|
172
|
+
orch.registerProvider(provider);
|
|
173
|
+
await orch.switchProvider("claude");
|
|
174
|
+
expect(provider.authenticate).toHaveBeenCalledOnce();
|
|
175
|
+
expect(orch.getProvider()).toBe(provider);
|
|
176
|
+
});
|
|
177
|
+
it("switchProvider cleans up previous session", async () => {
|
|
178
|
+
const orch = makeOrchestrator();
|
|
179
|
+
const claude = mockProvider("claude");
|
|
180
|
+
const openai = mockProvider("openai");
|
|
181
|
+
orch.registerProvider(claude);
|
|
182
|
+
orch.registerProvider(openai);
|
|
183
|
+
await orch.switchProvider("claude");
|
|
184
|
+
const session = await orch.startSession();
|
|
185
|
+
await orch.switchProvider("openai");
|
|
186
|
+
expect(claude.closeSession).toHaveBeenCalledWith(session);
|
|
187
|
+
expect(orch.activeSession).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
it("switchProvider throws for unregistered provider", async () => {
|
|
190
|
+
const orch = makeOrchestrator();
|
|
191
|
+
await expect(orch.switchProvider("openai")).rejects.toThrow('Provider "openai" not registered');
|
|
192
|
+
});
|
|
193
|
+
it("switchProvider saves preference", async () => {
|
|
194
|
+
const { savePreferences } = await import("../store/preferences.js");
|
|
195
|
+
const orch = makeOrchestrator();
|
|
196
|
+
orch.registerProvider(mockProvider("claude"));
|
|
197
|
+
await orch.switchProvider("claude");
|
|
198
|
+
expect(savePreferences).toHaveBeenCalledWith({
|
|
199
|
+
lastProvider: "claude",
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it("switchProvider to same provider re-authenticates", async () => {
|
|
203
|
+
const orch = makeOrchestrator();
|
|
204
|
+
const provider = mockProvider("claude");
|
|
205
|
+
orch.registerProvider(provider);
|
|
206
|
+
await orch.switchProvider("claude");
|
|
207
|
+
await orch.switchProvider("claude");
|
|
208
|
+
expect(provider.authenticate).toHaveBeenCalledTimes(2);
|
|
209
|
+
});
|
|
210
|
+
it("currentProvider returns active provider", async () => {
|
|
211
|
+
const orch = makeOrchestrator();
|
|
212
|
+
const provider = mockProvider("claude");
|
|
213
|
+
orch.registerProvider(provider);
|
|
214
|
+
expect(orch.currentProvider).toBeNull();
|
|
215
|
+
await orch.switchProvider("claude");
|
|
216
|
+
expect(orch.currentProvider).toBe(provider);
|
|
217
|
+
});
|
|
218
|
+
it("registerProvider overwrites existing provider with same name", () => {
|
|
219
|
+
const orch = makeOrchestrator();
|
|
220
|
+
const p1 = mockProvider("claude");
|
|
221
|
+
const p2 = mockProvider("claude");
|
|
222
|
+
orch.registerProvider(p1);
|
|
223
|
+
orch.registerProvider(p2);
|
|
224
|
+
expect(orch.getProvider("claude")).toBe(p2);
|
|
225
|
+
});
|
|
226
|
+
it("switchProvider cleans up session even if closeSession throws", async () => {
|
|
227
|
+
const orch = makeOrchestrator();
|
|
228
|
+
const claude = mockProvider("claude");
|
|
229
|
+
const openai = mockProvider("openai");
|
|
230
|
+
claude.closeSession.mockRejectedValue(new Error("cleanup failed"));
|
|
231
|
+
orch.registerProvider(claude);
|
|
232
|
+
orch.registerProvider(openai);
|
|
233
|
+
await orch.switchProvider("claude");
|
|
234
|
+
await orch.startSession();
|
|
235
|
+
// Should not throw despite closeSession failure
|
|
236
|
+
await orch.switchProvider("openai");
|
|
237
|
+
expect(orch.getProvider()).toBe(openai);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// =======================================================================
|
|
241
|
+
// Session Lifecycle
|
|
242
|
+
// =======================================================================
|
|
243
|
+
describe("Session Lifecycle", () => {
|
|
244
|
+
it("startSession creates session via provider", async () => {
|
|
245
|
+
const orch = makeOrchestrator();
|
|
246
|
+
const provider = mockProvider("claude");
|
|
247
|
+
orch.registerProvider(provider);
|
|
248
|
+
await orch.switchProvider("claude");
|
|
249
|
+
const session = await orch.startSession();
|
|
250
|
+
expect(provider.createSession).toHaveBeenCalledOnce();
|
|
251
|
+
expect(session.id).toBeTruthy();
|
|
252
|
+
expect(session.providerId).toBe("claude");
|
|
253
|
+
});
|
|
254
|
+
it("startSession auto-switches to default provider if none active", async () => {
|
|
255
|
+
const orch = makeOrchestrator("claude");
|
|
256
|
+
const provider = mockProvider("claude");
|
|
257
|
+
orch.registerProvider(provider);
|
|
258
|
+
const session = await orch.startSession();
|
|
259
|
+
expect(provider.authenticate).toHaveBeenCalledOnce();
|
|
260
|
+
expect(session).toBeTruthy();
|
|
261
|
+
});
|
|
262
|
+
it("startSession binds context gate", async () => {
|
|
263
|
+
const orch = makeOrchestrator();
|
|
264
|
+
const provider = mockProvider("claude");
|
|
265
|
+
orch.registerProvider(provider);
|
|
266
|
+
await orch.switchProvider("claude");
|
|
267
|
+
const mockGate = {
|
|
268
|
+
onSessionStart: vi.fn(),
|
|
269
|
+
checkThreshold: vi.fn().mockReturnValue(false),
|
|
270
|
+
performCheckpointWithGist: vi.fn(),
|
|
271
|
+
};
|
|
272
|
+
orch.setContextGate(mockGate);
|
|
273
|
+
const session = await orch.startSession();
|
|
274
|
+
expect(mockGate.onSessionStart).toHaveBeenCalledWith(session.id);
|
|
275
|
+
});
|
|
276
|
+
it("startSession transitions state machine to active", async () => {
|
|
277
|
+
const orch = makeOrchestrator();
|
|
278
|
+
orch.registerProvider(mockProvider("claude"));
|
|
279
|
+
await orch.switchProvider("claude");
|
|
280
|
+
expect(orch.currentState).toBe("idle");
|
|
281
|
+
await orch.startSession();
|
|
282
|
+
expect(orch.currentState).toBe("active");
|
|
283
|
+
});
|
|
284
|
+
it("startSession passes config to provider", async () => {
|
|
285
|
+
const orch = makeOrchestrator();
|
|
286
|
+
const provider = mockProvider("claude");
|
|
287
|
+
orch.registerProvider(provider);
|
|
288
|
+
await orch.switchProvider("claude");
|
|
289
|
+
await orch.startSession({ model: "opus", temperature: 0.5 });
|
|
290
|
+
expect(provider.createSession).toHaveBeenCalledWith(expect.objectContaining({
|
|
291
|
+
systemPrompt: "You are a test agent.",
|
|
292
|
+
model: "opus",
|
|
293
|
+
temperature: 0.5,
|
|
294
|
+
}));
|
|
295
|
+
});
|
|
296
|
+
it("ensureSession returns existing session", async () => {
|
|
297
|
+
const orch = makeOrchestrator();
|
|
298
|
+
orch.registerProvider(mockProvider("claude"));
|
|
299
|
+
await orch.switchProvider("claude");
|
|
300
|
+
const s1 = await orch.startSession();
|
|
301
|
+
const s2 = await orch.ensureSession();
|
|
302
|
+
expect(s2).toBe(s1);
|
|
303
|
+
});
|
|
304
|
+
it("ensureSession creates new if none exists", async () => {
|
|
305
|
+
const orch = makeOrchestrator();
|
|
306
|
+
orch.registerProvider(mockProvider("claude"));
|
|
307
|
+
await orch.switchProvider("claude");
|
|
308
|
+
const session = await orch.ensureSession();
|
|
309
|
+
expect(session).toBeTruthy();
|
|
310
|
+
expect(session.id).toBeTruthy();
|
|
311
|
+
});
|
|
312
|
+
it("resumeSession loads from DB", async () => {
|
|
313
|
+
const orch = makeOrchestrator();
|
|
314
|
+
const provider = mockProvider("claude");
|
|
315
|
+
orch.registerProvider(provider);
|
|
316
|
+
await orch.switchProvider("claude");
|
|
317
|
+
// Create a session in the DB first
|
|
318
|
+
const dbSession = orch.sessionStore.createSession("claude", "opus");
|
|
319
|
+
const session = await orch.resumeSession(dbSession.id);
|
|
320
|
+
expect(provider.resumeSession).toHaveBeenCalledWith(dbSession.id, "You are a test agent.");
|
|
321
|
+
expect(session.id).toBe(dbSession.id);
|
|
322
|
+
});
|
|
323
|
+
it("resumeSession switches provider if needed", async () => {
|
|
324
|
+
const orch = makeOrchestrator();
|
|
325
|
+
const claude = mockProvider("claude");
|
|
326
|
+
const openai = mockProvider("openai");
|
|
327
|
+
orch.registerProvider(claude);
|
|
328
|
+
orch.registerProvider(openai);
|
|
329
|
+
await orch.switchProvider("claude");
|
|
330
|
+
// Create a session stored as openai
|
|
331
|
+
const dbSession = orch.sessionStore.createSession("openai", "gpt-5.4");
|
|
332
|
+
await orch.resumeSession(dbSession.id);
|
|
333
|
+
// Should have switched to openai
|
|
334
|
+
expect(orch.getProvider()).toBe(openai);
|
|
335
|
+
expect(openai.authenticate).toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
it("resumeSession binds context gate", async () => {
|
|
338
|
+
const orch = makeOrchestrator();
|
|
339
|
+
orch.registerProvider(mockProvider("claude"));
|
|
340
|
+
await orch.switchProvider("claude");
|
|
341
|
+
const mockGate = {
|
|
342
|
+
onSessionStart: vi.fn(),
|
|
343
|
+
checkThreshold: vi.fn().mockReturnValue(false),
|
|
344
|
+
performCheckpointWithGist: vi.fn(),
|
|
345
|
+
};
|
|
346
|
+
orch.setContextGate(mockGate);
|
|
347
|
+
const dbSession = orch.sessionStore.createSession("claude");
|
|
348
|
+
await orch.resumeSession(dbSession.id);
|
|
349
|
+
expect(mockGate.onSessionStart).toHaveBeenCalledWith(dbSession.id);
|
|
350
|
+
});
|
|
351
|
+
it("resumeSession throws for unknown session", async () => {
|
|
352
|
+
const orch = makeOrchestrator();
|
|
353
|
+
orch.registerProvider(mockProvider("claude"));
|
|
354
|
+
await orch.switchProvider("claude");
|
|
355
|
+
await expect(orch.resumeSession("nonexistent-id")).rejects.toThrow('Session "nonexistent-id" not found');
|
|
356
|
+
});
|
|
357
|
+
it("resumeSession transitions state machine to active", async () => {
|
|
358
|
+
const orch = makeOrchestrator();
|
|
359
|
+
orch.registerProvider(mockProvider("claude"));
|
|
360
|
+
await orch.switchProvider("claude");
|
|
361
|
+
const dbSession = orch.sessionStore.createSession("claude");
|
|
362
|
+
await orch.resumeSession(dbSession.id);
|
|
363
|
+
expect(orch.currentState).toBe("active");
|
|
364
|
+
});
|
|
365
|
+
it("activeSession reflects current session", async () => {
|
|
366
|
+
const orch = makeOrchestrator();
|
|
367
|
+
orch.registerProvider(mockProvider("claude"));
|
|
368
|
+
await orch.switchProvider("claude");
|
|
369
|
+
expect(orch.activeSession).toBeNull();
|
|
370
|
+
const session = await orch.startSession();
|
|
371
|
+
expect(orch.activeSession).toBe(session);
|
|
372
|
+
});
|
|
373
|
+
it("currentSession is an alias for activeSession", async () => {
|
|
374
|
+
const orch = makeOrchestrator();
|
|
375
|
+
orch.registerProvider(mockProvider("claude"));
|
|
376
|
+
await orch.switchProvider("claude");
|
|
377
|
+
const session = await orch.startSession();
|
|
378
|
+
expect(orch.currentSession).toBe(session);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
// =======================================================================
|
|
382
|
+
// Message Flow
|
|
383
|
+
// =======================================================================
|
|
384
|
+
describe("Message Flow", () => {
|
|
385
|
+
it("send yields events from provider", async () => {
|
|
386
|
+
const { orch } = await makeWiredOrchestrator();
|
|
387
|
+
await orch.startSession();
|
|
388
|
+
const events = await collectEvents(orch.send("Hello"));
|
|
389
|
+
const textEvents = events.filter((e) => e.type === "text");
|
|
390
|
+
const doneEvents = events.filter((e) => e.type === "done");
|
|
391
|
+
expect(textEvents.length).toBeGreaterThanOrEqual(1);
|
|
392
|
+
expect(doneEvents).toHaveLength(1);
|
|
393
|
+
});
|
|
394
|
+
it("send stores user message in session store", async () => {
|
|
395
|
+
const { orch } = await makeWiredOrchestrator();
|
|
396
|
+
await orch.startSession();
|
|
397
|
+
await collectEvents(orch.send("Test message"));
|
|
398
|
+
const messages = orch.sessionStore.getMessages(orch.activeSession.id);
|
|
399
|
+
expect(messages.some((m) => m.role === "user" && m.content === "Test message")).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
it("send stores assistant response in session store", async () => {
|
|
402
|
+
const { orch } = await makeWiredOrchestrator();
|
|
403
|
+
await orch.startSession();
|
|
404
|
+
await collectEvents(orch.send("Hello"));
|
|
405
|
+
const messages = orch.sessionStore.getMessages(orch.activeSession.id);
|
|
406
|
+
expect(messages.some((m) => m.role === "assistant" && m.content === "Hello")).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
it("send prepends sticky notes to message", async () => {
|
|
409
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
410
|
+
await orch.startSession();
|
|
411
|
+
const stickies = new StickyManager();
|
|
412
|
+
stickies.add("Always use GPU");
|
|
413
|
+
orch.setStickyManager(stickies);
|
|
414
|
+
await collectEvents(orch.send("Train a model"));
|
|
415
|
+
// The provider.send should have been called with augmented message
|
|
416
|
+
const sendCall = provider.send.mock.calls[0];
|
|
417
|
+
const sentMessage = sendCall[1];
|
|
418
|
+
expect(sentMessage).toContain("STICKY NOTES");
|
|
419
|
+
expect(sentMessage).toContain("Always use GPU");
|
|
420
|
+
expect(sentMessage).toContain("Train a model");
|
|
421
|
+
});
|
|
422
|
+
it("send without stickies passes message unchanged", async () => {
|
|
423
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
424
|
+
await orch.startSession();
|
|
425
|
+
await collectEvents(orch.send("Plain message"));
|
|
426
|
+
const sendCall = provider.send.mock.calls[0];
|
|
427
|
+
const sentMessage = sendCall[1];
|
|
428
|
+
expect(sentMessage).toBe("Plain message");
|
|
429
|
+
});
|
|
430
|
+
it("send tracks cost from done event", async () => {
|
|
431
|
+
const { orch } = await makeWiredOrchestrator();
|
|
432
|
+
await orch.startSession();
|
|
433
|
+
await collectEvents(orch.send("Hello"));
|
|
434
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.01);
|
|
435
|
+
});
|
|
436
|
+
it("send tracks input tokens from done event", async () => {
|
|
437
|
+
const { orch } = await makeWiredOrchestrator();
|
|
438
|
+
await orch.startSession();
|
|
439
|
+
await collectEvents(orch.send("Hello"));
|
|
440
|
+
expect(orch.lastInputTokens).toBe(100);
|
|
441
|
+
});
|
|
442
|
+
it("send lock prevents concurrent sends", async () => {
|
|
443
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
444
|
+
// Make send take a while
|
|
445
|
+
let resolveBarrier;
|
|
446
|
+
const barrier = new Promise((r) => (resolveBarrier = r));
|
|
447
|
+
provider.send.mockImplementation(async function* () {
|
|
448
|
+
await barrier;
|
|
449
|
+
yield { type: "text", text: "ok", delta: "ok" };
|
|
450
|
+
yield { type: "done", usage: { inputTokens: 10, outputTokens: 5, costUsd: 0 } };
|
|
451
|
+
});
|
|
452
|
+
await orch.startSession();
|
|
453
|
+
// Start first send (will block on barrier)
|
|
454
|
+
const gen1 = orch.send("First");
|
|
455
|
+
// Start iterating to acquire the lock
|
|
456
|
+
const p1 = gen1.next();
|
|
457
|
+
// Second send should throw
|
|
458
|
+
await expect(collectEvents(orch.send("Second"))).rejects.toThrow("Another message is already being processed");
|
|
459
|
+
// Clean up
|
|
460
|
+
resolveBarrier();
|
|
461
|
+
await p1;
|
|
462
|
+
await collectEvents(gen1);
|
|
463
|
+
});
|
|
464
|
+
it("send lock is released after completion", async () => {
|
|
465
|
+
const { orch } = await makeWiredOrchestrator();
|
|
466
|
+
await orch.startSession();
|
|
467
|
+
await collectEvents(orch.send("First"));
|
|
468
|
+
// Should not throw
|
|
469
|
+
await collectEvents(orch.send("Second"));
|
|
470
|
+
});
|
|
471
|
+
it("send lock is released after error", async () => {
|
|
472
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
473
|
+
provider.send.mockImplementation(async function* () {
|
|
474
|
+
throw new Error("provider error");
|
|
475
|
+
});
|
|
476
|
+
await orch.startSession();
|
|
477
|
+
await expect(collectEvents(orch.send("Fail"))).rejects.toThrow("provider error");
|
|
478
|
+
// Restore working provider
|
|
479
|
+
provider.send.mockImplementation(async function* () {
|
|
480
|
+
yield { type: "text", text: "ok", delta: "ok" };
|
|
481
|
+
yield { type: "done", usage: { inputTokens: 10, outputTokens: 5, costUsd: 0 } };
|
|
482
|
+
});
|
|
483
|
+
// Lock should be released — next send should work
|
|
484
|
+
await collectEvents(orch.send("Recover"));
|
|
485
|
+
});
|
|
486
|
+
it("send auto-starts session if none active", async () => {
|
|
487
|
+
const orch = makeOrchestrator();
|
|
488
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
489
|
+
orch.registerProvider(provider);
|
|
490
|
+
// No switchProvider, no startSession — send should handle it
|
|
491
|
+
const events = await collectEvents(orch.send("Auto start"));
|
|
492
|
+
expect(provider.authenticate).toHaveBeenCalled();
|
|
493
|
+
expect(provider.createSession).toHaveBeenCalled();
|
|
494
|
+
expect(events.some((e) => e.type === "done")).toBe(true);
|
|
495
|
+
});
|
|
496
|
+
it("send calls maybeCheckpoint after done", async () => {
|
|
497
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
498
|
+
const mockGate = {
|
|
499
|
+
onSessionStart: vi.fn(),
|
|
500
|
+
checkThreshold: vi.fn().mockReturnValue(true),
|
|
501
|
+
performCheckpointWithGist: vi.fn().mockReturnValue("checkpoint briefing"),
|
|
502
|
+
};
|
|
503
|
+
orch.setContextGate(mockGate);
|
|
504
|
+
// Provider will be called twice: once for the user message, once for checkpoint gist
|
|
505
|
+
let callCount = 0;
|
|
506
|
+
provider.send.mockImplementation(async function* () {
|
|
507
|
+
callCount++;
|
|
508
|
+
if (callCount === 1) {
|
|
509
|
+
yield { type: "text", text: "response", delta: "response" };
|
|
510
|
+
yield { type: "done", usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.01 } };
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
// checkpoint gist generation
|
|
514
|
+
yield { type: "text", text: "gist content", delta: "gist content" };
|
|
515
|
+
yield { type: "done", usage: { inputTokens: 50, outputTokens: 20, costUsd: 0.005 } };
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
await orch.startSession();
|
|
519
|
+
const events = await collectEvents(orch.send("Hello"));
|
|
520
|
+
// Should have a checkpoint text event
|
|
521
|
+
expect(events.some((e) => e.type === "text" && e.text.includes("Context checkpoint"))).toBe(true);
|
|
522
|
+
expect(mockGate.checkThreshold).toHaveBeenCalled();
|
|
523
|
+
});
|
|
524
|
+
it("send passes tools and attachments to provider", async () => {
|
|
525
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
526
|
+
await orch.startSession();
|
|
527
|
+
const tool = makeTool("my_tool");
|
|
528
|
+
orch.registerTool(tool);
|
|
529
|
+
const attachment = {
|
|
530
|
+
filename: "test.png",
|
|
531
|
+
mediaType: "image/png",
|
|
532
|
+
data: "base64data",
|
|
533
|
+
};
|
|
534
|
+
await collectEvents(orch.send("Use tool", [attachment]));
|
|
535
|
+
const sendCall = provider.send.mock.calls[0];
|
|
536
|
+
expect(sendCall[2]).toContain(tool); // tools
|
|
537
|
+
expect(sendCall[3]).toEqual([attachment]); // attachments
|
|
538
|
+
});
|
|
539
|
+
it("send accumulates cost from multiple messages", async () => {
|
|
540
|
+
const { orch } = await makeWiredOrchestrator();
|
|
541
|
+
await orch.startSession();
|
|
542
|
+
await collectEvents(orch.send("msg1")); // 0.01
|
|
543
|
+
await collectEvents(orch.send("msg2")); // 0.01
|
|
544
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.02);
|
|
545
|
+
});
|
|
546
|
+
it("send updates lastActive on session store", async () => {
|
|
547
|
+
const { orch } = await makeWiredOrchestrator();
|
|
548
|
+
await orch.startSession();
|
|
549
|
+
const sessionId = orch.activeSession.id;
|
|
550
|
+
// Spy on updateLastActive
|
|
551
|
+
const spy = vi.spyOn(orch.sessionStore, "updateLastActive");
|
|
552
|
+
await collectEvents(orch.send("Hello"));
|
|
553
|
+
expect(spy).toHaveBeenCalledWith(sessionId);
|
|
554
|
+
});
|
|
555
|
+
it("send with empty stickies passes message unchanged", async () => {
|
|
556
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
557
|
+
await orch.startSession();
|
|
558
|
+
// Set sticky manager with no notes
|
|
559
|
+
orch.setStickyManager(new StickyManager());
|
|
560
|
+
await collectEvents(orch.send("No stickies"));
|
|
561
|
+
const sendCall = provider.send.mock.calls[0];
|
|
562
|
+
expect(sendCall[1]).toBe("No stickies");
|
|
563
|
+
});
|
|
564
|
+
it("send builds full response from text deltas", async () => {
|
|
565
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
566
|
+
provider.send.mockImplementation(async function* () {
|
|
567
|
+
yield { type: "text", text: "He", delta: "He" };
|
|
568
|
+
yield { type: "text", text: "llo", delta: "llo" };
|
|
569
|
+
yield { type: "done", usage: { inputTokens: 10, outputTokens: 5, costUsd: 0 } };
|
|
570
|
+
});
|
|
571
|
+
await orch.startSession();
|
|
572
|
+
await collectEvents(orch.send("Hi"));
|
|
573
|
+
const messages = orch.sessionStore.getMessages(orch.activeSession.id);
|
|
574
|
+
const assistantMsg = messages.find((m) => m.role === "assistant");
|
|
575
|
+
expect(assistantMsg.content).toBe("Hello");
|
|
576
|
+
});
|
|
577
|
+
it("send does not store empty assistant response", async () => {
|
|
578
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
579
|
+
provider.send.mockImplementation(async function* () {
|
|
580
|
+
yield { type: "done", usage: { inputTokens: 10, outputTokens: 0, costUsd: 0 } };
|
|
581
|
+
});
|
|
582
|
+
await orch.startSession();
|
|
583
|
+
await collectEvents(orch.send("Hi"));
|
|
584
|
+
const messages = orch.sessionStore.getMessages(orch.activeSession.id);
|
|
585
|
+
expect(messages.filter((m) => m.role === "assistant")).toHaveLength(0);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
// =======================================================================
|
|
589
|
+
// Tool Management
|
|
590
|
+
// =======================================================================
|
|
591
|
+
describe("Tool Management", () => {
|
|
592
|
+
it("registerTool adds tool", () => {
|
|
593
|
+
const orch = makeOrchestrator();
|
|
594
|
+
const tool = makeTool("remote_exec");
|
|
595
|
+
orch.registerTool(tool);
|
|
596
|
+
expect(orch.getTools()).toContain(tool);
|
|
597
|
+
});
|
|
598
|
+
it("registerTool deduplicates by name", () => {
|
|
599
|
+
const orch = makeOrchestrator();
|
|
600
|
+
const tool1 = makeTool("remote_exec");
|
|
601
|
+
const tool2 = makeTool("remote_exec");
|
|
602
|
+
orch.registerTool(tool1);
|
|
603
|
+
orch.registerTool(tool2);
|
|
604
|
+
expect(orch.getTools()).toHaveLength(1);
|
|
605
|
+
// First registration wins
|
|
606
|
+
expect(orch.getTools()[0]).toBe(tool1);
|
|
607
|
+
});
|
|
608
|
+
it("registerTools adds multiple tools", () => {
|
|
609
|
+
const orch = makeOrchestrator();
|
|
610
|
+
const tools = [makeTool("tool_a"), makeTool("tool_b"), makeTool("tool_c")];
|
|
611
|
+
orch.registerTools(tools);
|
|
612
|
+
expect(orch.getTools()).toHaveLength(3);
|
|
613
|
+
});
|
|
614
|
+
it("registerTools deduplicates", () => {
|
|
615
|
+
const orch = makeOrchestrator();
|
|
616
|
+
const tools = [
|
|
617
|
+
makeTool("tool_a"),
|
|
618
|
+
makeTool("tool_b"),
|
|
619
|
+
makeTool("tool_a"),
|
|
620
|
+
];
|
|
621
|
+
orch.registerTools(tools);
|
|
622
|
+
expect(orch.getTools()).toHaveLength(2);
|
|
623
|
+
});
|
|
624
|
+
it("getTools returns all registered tools", () => {
|
|
625
|
+
const orch = makeOrchestrator();
|
|
626
|
+
orch.registerTool(makeTool("alpha"));
|
|
627
|
+
orch.registerTool(makeTool("beta"));
|
|
628
|
+
const tools = orch.getTools();
|
|
629
|
+
expect(tools).toHaveLength(2);
|
|
630
|
+
expect(tools.map((t) => t.name)).toEqual(["alpha", "beta"]);
|
|
631
|
+
});
|
|
632
|
+
it("getTools returns empty array when no tools registered", () => {
|
|
633
|
+
const orch = makeOrchestrator();
|
|
634
|
+
expect(orch.getTools()).toEqual([]);
|
|
635
|
+
});
|
|
636
|
+
it("registerTool after registerTools appends", () => {
|
|
637
|
+
const orch = makeOrchestrator();
|
|
638
|
+
orch.registerTools([makeTool("a"), makeTool("b")]);
|
|
639
|
+
orch.registerTool(makeTool("c"));
|
|
640
|
+
expect(orch.getTools()).toHaveLength(3);
|
|
641
|
+
});
|
|
642
|
+
it("registerTools preserves order", () => {
|
|
643
|
+
const orch = makeOrchestrator();
|
|
644
|
+
const tools = [makeTool("z"), makeTool("a"), makeTool("m")];
|
|
645
|
+
orch.registerTools(tools);
|
|
646
|
+
expect(orch.getTools().map((t) => t.name)).toEqual(["z", "a", "m"]);
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
// =======================================================================
|
|
650
|
+
// Cost Tracking
|
|
651
|
+
// =======================================================================
|
|
652
|
+
describe("Cost Tracking", () => {
|
|
653
|
+
it("addCost accumulates total", async () => {
|
|
654
|
+
const { orch } = await makeWiredOrchestrator();
|
|
655
|
+
await orch.startSession();
|
|
656
|
+
orch.addCost(0.05);
|
|
657
|
+
orch.addCost(0.03);
|
|
658
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.08);
|
|
659
|
+
});
|
|
660
|
+
it("addCost persists to session store", async () => {
|
|
661
|
+
const { orch } = await makeWiredOrchestrator();
|
|
662
|
+
await orch.startSession();
|
|
663
|
+
const spy = vi.spyOn(orch.sessionStore, "addCost");
|
|
664
|
+
orch.addCost(0.01, 100, 50);
|
|
665
|
+
expect(spy).toHaveBeenCalledWith(orch.activeSession.id, 0.01, 100, 50);
|
|
666
|
+
});
|
|
667
|
+
it("addCost skips DB write for ephemeral sessions (eph- prefix)", async () => {
|
|
668
|
+
const orch = makeOrchestrator();
|
|
669
|
+
const provider = mockProvider("claude");
|
|
670
|
+
// Make provider return ephemeral-like session
|
|
671
|
+
provider.createSession.mockResolvedValue({
|
|
672
|
+
id: "eph-abc123",
|
|
673
|
+
providerId: "claude",
|
|
674
|
+
createdAt: Date.now(),
|
|
675
|
+
lastActiveAt: Date.now(),
|
|
676
|
+
});
|
|
677
|
+
orch.registerProvider(provider);
|
|
678
|
+
await orch.switchProvider("claude");
|
|
679
|
+
await orch.startSession();
|
|
680
|
+
const spy = vi.spyOn(orch.sessionStore, "addCost");
|
|
681
|
+
orch.addCost(0.01, 100, 50);
|
|
682
|
+
expect(spy).not.toHaveBeenCalled();
|
|
683
|
+
// But total still accumulates
|
|
684
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.01);
|
|
685
|
+
});
|
|
686
|
+
it("totalCostUsd returns accumulated cost", () => {
|
|
687
|
+
const orch = makeOrchestrator();
|
|
688
|
+
expect(orch.totalCostUsd).toBe(0);
|
|
689
|
+
});
|
|
690
|
+
it("lastInputTokens returns last reported tokens", async () => {
|
|
691
|
+
const { orch } = await makeWiredOrchestrator();
|
|
692
|
+
await orch.startSession();
|
|
693
|
+
expect(orch.lastInputTokens).toBe(0);
|
|
694
|
+
await collectEvents(orch.send("Hello"));
|
|
695
|
+
expect(orch.lastInputTokens).toBe(100);
|
|
696
|
+
});
|
|
697
|
+
it("addCost with no tokens defaults to zero", async () => {
|
|
698
|
+
const { orch } = await makeWiredOrchestrator();
|
|
699
|
+
await orch.startSession();
|
|
700
|
+
const spy = vi.spyOn(orch.sessionStore, "addCost");
|
|
701
|
+
orch.addCost(0.01);
|
|
702
|
+
expect(spy).toHaveBeenCalledWith(orch.activeSession.id, 0.01, 0, 0);
|
|
703
|
+
});
|
|
704
|
+
it("addCost with no active session does not write to DB", () => {
|
|
705
|
+
const orch = makeOrchestrator();
|
|
706
|
+
const spy = vi.spyOn(orch.sessionStore, "addCost");
|
|
707
|
+
orch.addCost(0.01);
|
|
708
|
+
expect(spy).not.toHaveBeenCalled();
|
|
709
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.01);
|
|
710
|
+
});
|
|
711
|
+
it("cost persists across multiple send calls", async () => {
|
|
712
|
+
const { orch } = await makeWiredOrchestrator();
|
|
713
|
+
await orch.startSession();
|
|
714
|
+
await collectEvents(orch.send("a"));
|
|
715
|
+
await collectEvents(orch.send("b"));
|
|
716
|
+
await collectEvents(orch.send("c"));
|
|
717
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.03);
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
// =======================================================================
|
|
721
|
+
// Model & Reasoning
|
|
722
|
+
// =======================================================================
|
|
723
|
+
describe("Model & Reasoning", () => {
|
|
724
|
+
it("setModel changes provider model", async () => {
|
|
725
|
+
const orch = makeOrchestrator();
|
|
726
|
+
const provider = mockProvider("claude");
|
|
727
|
+
orch.registerProvider(provider);
|
|
728
|
+
await orch.switchProvider("claude");
|
|
729
|
+
await orch.setModel("claude-sonnet-4-20250514");
|
|
730
|
+
expect(provider.currentModel).toBe("claude-sonnet-4-20250514");
|
|
731
|
+
});
|
|
732
|
+
it("setModel closes current session (force new session)", async () => {
|
|
733
|
+
const orch = makeOrchestrator();
|
|
734
|
+
const provider = mockProvider("claude");
|
|
735
|
+
orch.registerProvider(provider);
|
|
736
|
+
await orch.switchProvider("claude");
|
|
737
|
+
const session = await orch.startSession();
|
|
738
|
+
await orch.setModel("claude-sonnet-4-20250514");
|
|
739
|
+
expect(provider.closeSession).toHaveBeenCalledWith(session);
|
|
740
|
+
expect(orch.activeSession).toBeNull();
|
|
741
|
+
});
|
|
742
|
+
it("setModel auto-switches provider if none active", async () => {
|
|
743
|
+
const orch = makeOrchestrator();
|
|
744
|
+
const provider = mockProvider("claude");
|
|
745
|
+
orch.registerProvider(provider);
|
|
746
|
+
await orch.setModel("new-model");
|
|
747
|
+
expect(provider.authenticate).toHaveBeenCalled();
|
|
748
|
+
expect(provider.currentModel).toBe("new-model");
|
|
749
|
+
});
|
|
750
|
+
it("setReasoningEffort updates provider", async () => {
|
|
751
|
+
const orch = makeOrchestrator();
|
|
752
|
+
const provider = mockProvider("claude");
|
|
753
|
+
orch.registerProvider(provider);
|
|
754
|
+
await orch.switchProvider("claude");
|
|
755
|
+
await orch.setReasoningEffort("high");
|
|
756
|
+
expect(provider.reasoningEffort).toBe("high");
|
|
757
|
+
});
|
|
758
|
+
it("setReasoningEffort auto-switches provider if none active", async () => {
|
|
759
|
+
const orch = makeOrchestrator();
|
|
760
|
+
const provider = mockProvider("claude");
|
|
761
|
+
orch.registerProvider(provider);
|
|
762
|
+
await orch.setReasoningEffort("max");
|
|
763
|
+
expect(provider.authenticate).toHaveBeenCalled();
|
|
764
|
+
expect(provider.reasoningEffort).toBe("max");
|
|
765
|
+
});
|
|
766
|
+
it("fetchModels delegates to provider", async () => {
|
|
767
|
+
const orch = makeOrchestrator();
|
|
768
|
+
const provider = mockProvider("claude");
|
|
769
|
+
orch.registerProvider(provider);
|
|
770
|
+
await orch.switchProvider("claude");
|
|
771
|
+
const models = await orch.fetchModels();
|
|
772
|
+
expect(provider.fetchModels).toHaveBeenCalledOnce();
|
|
773
|
+
expect(models).toEqual([{ id: "test-model", name: "Test Model" }]);
|
|
774
|
+
});
|
|
775
|
+
it("fetchModels returns current model if fetchModels not implemented", async () => {
|
|
776
|
+
const orch = makeOrchestrator();
|
|
777
|
+
const provider = mockProvider("claude");
|
|
778
|
+
delete provider.fetchModels;
|
|
779
|
+
orch.registerProvider(provider);
|
|
780
|
+
await orch.switchProvider("claude");
|
|
781
|
+
const models = await orch.fetchModels();
|
|
782
|
+
expect(models).toEqual([
|
|
783
|
+
{ id: "claude-opus-4-6", name: "claude-opus-4-6" },
|
|
784
|
+
]);
|
|
785
|
+
});
|
|
786
|
+
it("currentModel returns provider's model", async () => {
|
|
787
|
+
const orch = makeOrchestrator();
|
|
788
|
+
const provider = mockProvider("claude");
|
|
789
|
+
orch.registerProvider(provider);
|
|
790
|
+
await orch.switchProvider("claude");
|
|
791
|
+
expect(orch.currentModel).toBe("claude-opus-4-6");
|
|
792
|
+
});
|
|
793
|
+
it("currentModel returns null when no provider active", () => {
|
|
794
|
+
const orch = makeOrchestrator();
|
|
795
|
+
expect(orch.currentModel).toBeNull();
|
|
796
|
+
});
|
|
797
|
+
it("reasoningEffort returns provider's effort", async () => {
|
|
798
|
+
const orch = makeOrchestrator();
|
|
799
|
+
const provider = mockProvider("claude");
|
|
800
|
+
orch.registerProvider(provider);
|
|
801
|
+
await orch.switchProvider("claude");
|
|
802
|
+
expect(orch.reasoningEffort).toBe("medium");
|
|
803
|
+
});
|
|
804
|
+
it("reasoningEffort returns null when no provider active", () => {
|
|
805
|
+
const orch = makeOrchestrator();
|
|
806
|
+
expect(orch.reasoningEffort).toBeNull();
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
// =======================================================================
|
|
810
|
+
// Interrupt
|
|
811
|
+
// =======================================================================
|
|
812
|
+
describe("Interrupt", () => {
|
|
813
|
+
it("interrupt calls provider interrupt", async () => {
|
|
814
|
+
const orch = makeOrchestrator();
|
|
815
|
+
const provider = mockProvider("claude");
|
|
816
|
+
orch.registerProvider(provider);
|
|
817
|
+
await orch.switchProvider("claude");
|
|
818
|
+
const session = await orch.startSession();
|
|
819
|
+
orch.interrupt();
|
|
820
|
+
expect(provider.interrupt).toHaveBeenCalledWith(session);
|
|
821
|
+
});
|
|
822
|
+
it("interrupt does nothing without active session", () => {
|
|
823
|
+
const orch = makeOrchestrator();
|
|
824
|
+
const provider = mockProvider("claude");
|
|
825
|
+
orch.registerProvider(provider);
|
|
826
|
+
// Should not throw
|
|
827
|
+
orch.interrupt();
|
|
828
|
+
expect(provider.interrupt).not.toHaveBeenCalled();
|
|
829
|
+
});
|
|
830
|
+
it("interrupt aborts secondary controller", async () => {
|
|
831
|
+
const orch = makeOrchestrator();
|
|
832
|
+
const provider = mockProvider("claude");
|
|
833
|
+
orch.registerProvider(provider);
|
|
834
|
+
await orch.switchProvider("claude");
|
|
835
|
+
await orch.startSession();
|
|
836
|
+
const controller = new AbortController();
|
|
837
|
+
orch.setActiveAbort(controller);
|
|
838
|
+
orch.interrupt();
|
|
839
|
+
expect(controller.signal.aborted).toBe(true);
|
|
840
|
+
});
|
|
841
|
+
it("setActiveAbort registers controller", async () => {
|
|
842
|
+
const orch = makeOrchestrator();
|
|
843
|
+
const provider = mockProvider("claude");
|
|
844
|
+
orch.registerProvider(provider);
|
|
845
|
+
await orch.switchProvider("claude");
|
|
846
|
+
await orch.startSession();
|
|
847
|
+
const controller = new AbortController();
|
|
848
|
+
orch.setActiveAbort(controller);
|
|
849
|
+
orch.interrupt();
|
|
850
|
+
expect(controller.signal.aborted).toBe(true);
|
|
851
|
+
});
|
|
852
|
+
it("interrupt clears the active abort controller", async () => {
|
|
853
|
+
const orch = makeOrchestrator();
|
|
854
|
+
const provider = mockProvider("claude");
|
|
855
|
+
orch.registerProvider(provider);
|
|
856
|
+
await orch.switchProvider("claude");
|
|
857
|
+
await orch.startSession();
|
|
858
|
+
const controller1 = new AbortController();
|
|
859
|
+
orch.setActiveAbort(controller1);
|
|
860
|
+
orch.interrupt();
|
|
861
|
+
// Register a second controller — first interrupt should have cleared
|
|
862
|
+
const controller2 = new AbortController();
|
|
863
|
+
orch.setActiveAbort(controller2);
|
|
864
|
+
// First controller stays aborted, second is not yet aborted
|
|
865
|
+
expect(controller1.signal.aborted).toBe(true);
|
|
866
|
+
expect(controller2.signal.aborted).toBe(false);
|
|
867
|
+
});
|
|
868
|
+
it("setActiveAbort with null clears controller", async () => {
|
|
869
|
+
const orch = makeOrchestrator();
|
|
870
|
+
const provider = mockProvider("claude");
|
|
871
|
+
orch.registerProvider(provider);
|
|
872
|
+
await orch.switchProvider("claude");
|
|
873
|
+
await orch.startSession();
|
|
874
|
+
const controller = new AbortController();
|
|
875
|
+
orch.setActiveAbort(controller);
|
|
876
|
+
orch.setActiveAbort(null);
|
|
877
|
+
orch.interrupt();
|
|
878
|
+
// Controller should NOT be aborted since it was cleared
|
|
879
|
+
expect(controller.signal.aborted).toBe(false);
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
// =======================================================================
|
|
883
|
+
// Context Gate integration
|
|
884
|
+
// =======================================================================
|
|
885
|
+
describe("Context Gate", () => {
|
|
886
|
+
it("setContextGate stores the gate", () => {
|
|
887
|
+
const orch = makeOrchestrator();
|
|
888
|
+
const mockGate = { onSessionStart: vi.fn() };
|
|
889
|
+
orch.setContextGate(mockGate);
|
|
890
|
+
expect(orch.contextGate).toBe(mockGate);
|
|
891
|
+
});
|
|
892
|
+
it("contextGate returns null by default", () => {
|
|
893
|
+
const orch = makeOrchestrator();
|
|
894
|
+
expect(orch.contextGate).toBeNull();
|
|
895
|
+
});
|
|
896
|
+
it("maybeCheckpoint skips when no context gate", async () => {
|
|
897
|
+
const { orch } = await makeWiredOrchestrator();
|
|
898
|
+
await orch.startSession();
|
|
899
|
+
// Should not throw or emit checkpoint events
|
|
900
|
+
const events = await collectEvents(orch.send("Hello"));
|
|
901
|
+
expect(events.some((e) => e.type === "text" &&
|
|
902
|
+
e.text.includes("Context checkpoint"))).toBe(false);
|
|
903
|
+
});
|
|
904
|
+
it("maybeCheckpoint skips when threshold not reached", async () => {
|
|
905
|
+
const { orch } = await makeWiredOrchestrator();
|
|
906
|
+
const mockGate = {
|
|
907
|
+
onSessionStart: vi.fn(),
|
|
908
|
+
checkThreshold: vi.fn().mockReturnValue(false),
|
|
909
|
+
performCheckpointWithGist: vi.fn(),
|
|
910
|
+
};
|
|
911
|
+
orch.setContextGate(mockGate);
|
|
912
|
+
await orch.startSession();
|
|
913
|
+
const events = await collectEvents(orch.send("Hello"));
|
|
914
|
+
expect(mockGate.checkThreshold).toHaveBeenCalled();
|
|
915
|
+
expect(mockGate.performCheckpointWithGist).not.toHaveBeenCalled();
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
// =======================================================================
|
|
919
|
+
// Sticky Manager
|
|
920
|
+
// =======================================================================
|
|
921
|
+
describe("Sticky Manager", () => {
|
|
922
|
+
it("setStickyManager stores the manager", () => {
|
|
923
|
+
const orch = makeOrchestrator();
|
|
924
|
+
const stickies = new StickyManager();
|
|
925
|
+
orch.setStickyManager(stickies);
|
|
926
|
+
// Verify it's used in send — if setStickyManager didn't store it,
|
|
927
|
+
// the augmented message test above would fail
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
// =======================================================================
|
|
931
|
+
// State Machine
|
|
932
|
+
// =======================================================================
|
|
933
|
+
describe("State Machine", () => {
|
|
934
|
+
it("stateMachine is accessible", () => {
|
|
935
|
+
const orch = makeOrchestrator();
|
|
936
|
+
expect(orch.stateMachine).toBeTruthy();
|
|
937
|
+
expect(orch.stateMachine.state).toBe("idle");
|
|
938
|
+
});
|
|
939
|
+
it("currentState reflects state machine", async () => {
|
|
940
|
+
const orch = makeOrchestrator();
|
|
941
|
+
expect(orch.currentState).toBe("idle");
|
|
942
|
+
orch.registerProvider(mockProvider("claude"));
|
|
943
|
+
await orch.switchProvider("claude");
|
|
944
|
+
await orch.startSession();
|
|
945
|
+
expect(orch.currentState).toBe("active");
|
|
946
|
+
});
|
|
947
|
+
it("startSession does not transition if already active", async () => {
|
|
948
|
+
const orch = makeOrchestrator();
|
|
949
|
+
orch.registerProvider(mockProvider("claude"));
|
|
950
|
+
await orch.switchProvider("claude");
|
|
951
|
+
await orch.startSession();
|
|
952
|
+
expect(orch.currentState).toBe("active");
|
|
953
|
+
// Starting another session should not throw (already active)
|
|
954
|
+
await orch.startSession();
|
|
955
|
+
expect(orch.currentState).toBe("active");
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
// =======================================================================
|
|
959
|
+
// Config
|
|
960
|
+
// =======================================================================
|
|
961
|
+
describe("Config", () => {
|
|
962
|
+
it("config is accessible", () => {
|
|
963
|
+
const orch = makeOrchestrator();
|
|
964
|
+
expect(orch.config.defaultProvider).toBe("claude");
|
|
965
|
+
expect(orch.config.systemPrompt).toBe("You are a test agent.");
|
|
966
|
+
});
|
|
967
|
+
it("sessionStore is accessible", () => {
|
|
968
|
+
const orch = makeOrchestrator();
|
|
969
|
+
expect(orch.sessionStore).toBeInstanceOf(SessionStore);
|
|
970
|
+
});
|
|
971
|
+
it("uses provided sessionStore", () => {
|
|
972
|
+
const store = new SessionStore("custom");
|
|
973
|
+
const orch = new Orchestrator({
|
|
974
|
+
defaultProvider: "claude",
|
|
975
|
+
systemPrompt: "test",
|
|
976
|
+
sessionStore: store,
|
|
977
|
+
});
|
|
978
|
+
expect(orch.sessionStore).toBe(store);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
//# sourceMappingURL=orchestrator.test.js.map
|