@snoglobe/helios 0.3.6 → 0.4.1
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/cli/replay.d.ts.map +1 -1
- package/dist/cli/replay.js +2 -3
- package/dist/cli/replay.js.map +1 -1
- package/dist/cli/sessions.d.ts.map +1 -1
- package/dist/cli/sessions.js +2 -1
- package/dist/cli/sessions.js.map +1 -1
- 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 +1 -0
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +129 -37
- 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 +2 -1
- package/dist/core/task-poller.d.ts.map +1 -1
- package/dist/core/task-poller.js +25 -3
- 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 +42 -7
- package/dist/init.js.map +1 -1
- package/dist/memory/context-gate.d.ts.map +1 -1
- package/dist/memory/context-gate.js +2 -1
- package/dist/memory/context-gate.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/global-memory.d.ts +0 -1
- package/dist/memory/global-memory.d.ts.map +1 -1
- package/dist/memory/global-memory.js +0 -14
- package/dist/memory/global-memory.js.map +1 -1
- 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 +757 -0
- package/dist/providers/claude/history.test.js.map +1 -0
- package/dist/providers/claude/provider.d.ts +2 -1
- package/dist/providers/claude/provider.d.ts.map +1 -1
- package/dist/providers/claude/provider.js +87 -25
- 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 +1168 -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 +657 -0
- package/dist/providers/openai/history.test.js.map +1 -0
- package/dist/providers/openai/provider.d.ts +2 -1
- package/dist/providers/openai/provider.d.ts.map +1 -1
- package/dist/providers/openai/provider.js +75 -22
- 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 +1093 -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 +3 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.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 +9 -2
- 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.d.ts.map +1 -1
- package/dist/store/migrations.js +7 -0
- package/dist/store/migrations.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 +28 -1
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +62 -26
- 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 +24 -0
- package/dist/subagent/executor.d.ts.map +1 -0
- package/dist/subagent/executor.js +140 -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 +100 -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/sleep.d.ts.map +1 -1
- package/dist/tools/sleep.js +19 -7
- package/dist/tools/sleep.js.map +1 -1
- 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 +123 -16
- 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 +5 -6
- 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,674 @@
|
|
|
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
|
+
function mockProvider(name = "claude", sessionStore) {
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
displayName: name === "claude" ? "Claude" : "OpenAI",
|
|
38
|
+
currentModel: name === "claude" ? "claude-opus-4-6" : "gpt-5.4",
|
|
39
|
+
reasoningEffort: "medium",
|
|
40
|
+
isAuthenticated: vi.fn().mockResolvedValue(true),
|
|
41
|
+
authenticate: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
createSession: vi.fn().mockImplementation(async (_config) => {
|
|
43
|
+
if (sessionStore) {
|
|
44
|
+
return sessionStore.createSession(name, name === "claude" ? "claude-opus-4-6" : "gpt-5.4");
|
|
45
|
+
}
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
return {
|
|
48
|
+
id: `sess-${Math.random().toString(36).slice(2, 8)}`,
|
|
49
|
+
providerId: name,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
lastActiveAt: now,
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
resumeSession: vi.fn().mockImplementation(async (id) => {
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
providerId: name,
|
|
58
|
+
createdAt: Date.now(),
|
|
59
|
+
lastActiveAt: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
send: vi.fn().mockImplementation(async function* () {
|
|
63
|
+
yield { type: "text", text: "Hello", delta: "Hello" };
|
|
64
|
+
yield {
|
|
65
|
+
type: "done",
|
|
66
|
+
usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.01 },
|
|
67
|
+
};
|
|
68
|
+
}),
|
|
69
|
+
interrupt: vi.fn(),
|
|
70
|
+
resetHistory: vi.fn(),
|
|
71
|
+
closeSession: vi.fn().mockResolvedValue(undefined),
|
|
72
|
+
fetchModels: vi.fn().mockResolvedValue([{ id: "test-model", name: "Test Model" }]),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a "chatty" provider that returns different responses based on call count.
|
|
77
|
+
* Useful for multi-turn integration tests.
|
|
78
|
+
*/
|
|
79
|
+
function chattyProvider(name = "claude", sessionStore) {
|
|
80
|
+
let callCount = 0;
|
|
81
|
+
const base = mockProvider(name, sessionStore);
|
|
82
|
+
base.send = vi.fn().mockImplementation(async function* (_session, _message, _tools) {
|
|
83
|
+
callCount++;
|
|
84
|
+
if (callCount === 1) {
|
|
85
|
+
yield { type: "text", text: "Hello!", delta: "Hello!" };
|
|
86
|
+
yield { type: "done", usage: { inputTokens: 50, outputTokens: 20, costUsd: 0.005 } };
|
|
87
|
+
}
|
|
88
|
+
else if (callCount === 2) {
|
|
89
|
+
yield { type: "tool_call", id: "tc-1", name: "remote_exec", args: { command: "ls" } };
|
|
90
|
+
yield { type: "tool_result", callId: "tc-1", result: "file.txt" };
|
|
91
|
+
yield { type: "text", text: "Found file.txt", delta: "Found file.txt" };
|
|
92
|
+
yield { type: "done", usage: { inputTokens: 100, outputTokens: 40, costUsd: 0.015 } };
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
yield { type: "text", text: "Done", delta: "Done" };
|
|
96
|
+
yield { type: "done", usage: { inputTokens: 75, outputTokens: 30, costUsd: 0.008 } };
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return base;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Creates a provider that emits an error during send.
|
|
103
|
+
*/
|
|
104
|
+
function errorProvider(name = "claude", sessionStore) {
|
|
105
|
+
const base = mockProvider(name, sessionStore);
|
|
106
|
+
base.send = vi.fn().mockImplementation(async function* () {
|
|
107
|
+
yield { type: "text", text: "Starting...", delta: "Starting..." };
|
|
108
|
+
throw new Error("Provider exploded");
|
|
109
|
+
});
|
|
110
|
+
return base;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Provider whose send emits done events with no usage.
|
|
114
|
+
*/
|
|
115
|
+
function noUsageProvider(name = "claude", sessionStore) {
|
|
116
|
+
const base = mockProvider(name, sessionStore);
|
|
117
|
+
base.send = vi.fn().mockImplementation(async function* () {
|
|
118
|
+
yield { type: "text", text: "No stats", delta: "No stats" };
|
|
119
|
+
yield { type: "done" };
|
|
120
|
+
});
|
|
121
|
+
return base;
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Helpers
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
function makeOrchestrator(defaultProvider = "claude") {
|
|
127
|
+
return new Orchestrator({
|
|
128
|
+
defaultProvider,
|
|
129
|
+
systemPrompt: "You are a test agent.",
|
|
130
|
+
sessionStore: new SessionStore("integ-agent"),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function makeWiredOrchestrator(providerName = "claude") {
|
|
134
|
+
const orch = makeOrchestrator(providerName);
|
|
135
|
+
const provider = mockProvider(providerName, orch.sessionStore);
|
|
136
|
+
orch.registerProvider(provider);
|
|
137
|
+
await orch.switchProvider(providerName);
|
|
138
|
+
return { orch, provider };
|
|
139
|
+
}
|
|
140
|
+
function makeTool(name) {
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
description: `Tool ${name}`,
|
|
144
|
+
parameters: { type: "object", properties: {} },
|
|
145
|
+
execute: vi.fn().mockResolvedValue("ok"),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function collectEvents(gen) {
|
|
149
|
+
const events = [];
|
|
150
|
+
for await (const e of gen)
|
|
151
|
+
events.push(e);
|
|
152
|
+
return events;
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Tests
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
describe("Orchestrator — Integration Tests", () => {
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
mockDb.current = createTestDb();
|
|
160
|
+
});
|
|
161
|
+
// =======================================================================
|
|
162
|
+
// Multi-turn Conversations
|
|
163
|
+
// =======================================================================
|
|
164
|
+
describe("Multi-turn Conversations", () => {
|
|
165
|
+
it("3 sends accumulate correct cost", async () => {
|
|
166
|
+
const orch = makeOrchestrator();
|
|
167
|
+
const provider = chattyProvider("claude", orch.sessionStore);
|
|
168
|
+
orch.registerProvider(provider);
|
|
169
|
+
await orch.switchProvider("claude");
|
|
170
|
+
await collectEvents(orch.send("Turn 1"));
|
|
171
|
+
await collectEvents(orch.send("Turn 2"));
|
|
172
|
+
await collectEvents(orch.send("Turn 3"));
|
|
173
|
+
// 0.005 + 0.015 + 0.008 = 0.028
|
|
174
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.028, 3);
|
|
175
|
+
});
|
|
176
|
+
it("3 sends yield correct event types per turn", async () => {
|
|
177
|
+
const orch = makeOrchestrator();
|
|
178
|
+
const provider = chattyProvider("claude", orch.sessionStore);
|
|
179
|
+
orch.registerProvider(provider);
|
|
180
|
+
await orch.switchProvider("claude");
|
|
181
|
+
const events1 = await collectEvents(orch.send("Turn 1"));
|
|
182
|
+
expect(events1.some((e) => e.type === "text")).toBe(true);
|
|
183
|
+
expect(events1.some((e) => e.type === "done")).toBe(true);
|
|
184
|
+
const events2 = await collectEvents(orch.send("Turn 2"));
|
|
185
|
+
expect(events2.some((e) => e.type === "tool_call")).toBe(true);
|
|
186
|
+
expect(events2.some((e) => e.type === "tool_result")).toBe(true);
|
|
187
|
+
const events3 = await collectEvents(orch.send("Turn 3"));
|
|
188
|
+
const textEvents3 = events3.filter((e) => e.type === "text");
|
|
189
|
+
expect(textEvents3.some((e) => e.delta === "Done")).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it("session store records both user and assistant messages", async () => {
|
|
192
|
+
const { orch } = await makeWiredOrchestrator();
|
|
193
|
+
await collectEvents(orch.send("User question"));
|
|
194
|
+
const messages = orch.sessionStore.getMessages(orch.activeSession.id);
|
|
195
|
+
const roles = messages.map((m) => m.role);
|
|
196
|
+
expect(roles).toContain("user");
|
|
197
|
+
expect(roles).toContain("assistant");
|
|
198
|
+
});
|
|
199
|
+
it("empty assistant response: no assistant message stored", async () => {
|
|
200
|
+
const orch = makeOrchestrator();
|
|
201
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
202
|
+
// Override send to emit done with no text
|
|
203
|
+
provider.send = vi.fn().mockImplementation(async function* () {
|
|
204
|
+
yield { type: "done", usage: { inputTokens: 10, outputTokens: 0 } };
|
|
205
|
+
});
|
|
206
|
+
orch.registerProvider(provider);
|
|
207
|
+
await orch.switchProvider("claude");
|
|
208
|
+
await collectEvents(orch.send("Hello"));
|
|
209
|
+
const messages = orch.sessionStore.getMessages(orch.activeSession.id);
|
|
210
|
+
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
|
211
|
+
expect(assistantMessages).toHaveLength(0);
|
|
212
|
+
});
|
|
213
|
+
it("tool calls are forwarded correctly to caller", async () => {
|
|
214
|
+
const orch = makeOrchestrator();
|
|
215
|
+
const provider = chattyProvider("claude", orch.sessionStore);
|
|
216
|
+
orch.registerProvider(provider);
|
|
217
|
+
await orch.switchProvider("claude");
|
|
218
|
+
// First call is plain text, second has tool calls
|
|
219
|
+
await collectEvents(orch.send("Turn 1"));
|
|
220
|
+
const events = await collectEvents(orch.send("Turn 2"));
|
|
221
|
+
const toolCall = events.find((e) => e.type === "tool_call");
|
|
222
|
+
expect(toolCall).toBeDefined();
|
|
223
|
+
expect(toolCall.name).toBe("remote_exec");
|
|
224
|
+
expect(toolCall.id).toBe("tc-1");
|
|
225
|
+
const toolResult = events.find((e) => e.type === "tool_result");
|
|
226
|
+
expect(toolResult).toBeDefined();
|
|
227
|
+
expect(toolResult.callId).toBe("tc-1");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
// =======================================================================
|
|
231
|
+
// Provider Switching
|
|
232
|
+
// =======================================================================
|
|
233
|
+
describe("Provider Switching", () => {
|
|
234
|
+
it("switching mid-conversation: closes old session, carries context", async () => {
|
|
235
|
+
const orch = makeOrchestrator();
|
|
236
|
+
const claude = mockProvider("claude", orch.sessionStore);
|
|
237
|
+
const openai = mockProvider("openai", orch.sessionStore);
|
|
238
|
+
orch.registerProvider(claude);
|
|
239
|
+
orch.registerProvider(openai);
|
|
240
|
+
await orch.switchProvider("claude");
|
|
241
|
+
const claudeSession = await orch.startSession();
|
|
242
|
+
await collectEvents(orch.send("Message to Claude"));
|
|
243
|
+
await orch.switchProvider("openai");
|
|
244
|
+
expect(claude.closeSession).toHaveBeenCalledWith(claudeSession);
|
|
245
|
+
// Context is carried over — session is resumed on the new provider with the same ID
|
|
246
|
+
expect(orch.activeSession).not.toBeNull();
|
|
247
|
+
expect(orch.activeSession.id).toBe(claudeSession.id);
|
|
248
|
+
expect(openai.resumeSession).toHaveBeenCalledWith(claudeSession.id, expect.any(String));
|
|
249
|
+
await collectEvents(orch.send("Message to OpenAI"));
|
|
250
|
+
expect(orch.activeSession).not.toBeNull();
|
|
251
|
+
});
|
|
252
|
+
it("switching preserves total cost", async () => {
|
|
253
|
+
const orch = makeOrchestrator();
|
|
254
|
+
const claude = mockProvider("claude", orch.sessionStore);
|
|
255
|
+
const openai = mockProvider("openai", orch.sessionStore);
|
|
256
|
+
orch.registerProvider(claude);
|
|
257
|
+
orch.registerProvider(openai);
|
|
258
|
+
await orch.switchProvider("claude");
|
|
259
|
+
await collectEvents(orch.send("Claude msg")); // costs 0.01
|
|
260
|
+
const costAfterClaude = orch.totalCostUsd;
|
|
261
|
+
expect(costAfterClaude).toBeCloseTo(0.01, 3);
|
|
262
|
+
await orch.switchProvider("openai");
|
|
263
|
+
await collectEvents(orch.send("OpenAI msg")); // costs another 0.01
|
|
264
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.02, 3);
|
|
265
|
+
});
|
|
266
|
+
it("resume session on different provider switches correctly", async () => {
|
|
267
|
+
const orch = makeOrchestrator();
|
|
268
|
+
const claude = mockProvider("claude", orch.sessionStore);
|
|
269
|
+
const openai = mockProvider("openai", orch.sessionStore);
|
|
270
|
+
orch.registerProvider(claude);
|
|
271
|
+
orch.registerProvider(openai);
|
|
272
|
+
// Create a session with OpenAI
|
|
273
|
+
await orch.switchProvider("openai");
|
|
274
|
+
const openaiSession = orch.sessionStore.createSession("openai", "gpt-5.4");
|
|
275
|
+
orch.sessionStore.addMessage(openaiSession.id, "user", "Old msg");
|
|
276
|
+
// Currently on OpenAI, resume that session
|
|
277
|
+
await orch.switchProvider("claude");
|
|
278
|
+
await orch.resumeSession(openaiSession.id);
|
|
279
|
+
// Should have switched back to openai
|
|
280
|
+
expect(orch.currentProvider.name).toBe("openai");
|
|
281
|
+
expect(openai.resumeSession).toHaveBeenCalledWith(openaiSession.id, "You are a test agent.");
|
|
282
|
+
});
|
|
283
|
+
it("multiple providers registered: can switch between them", async () => {
|
|
284
|
+
const orch = makeOrchestrator();
|
|
285
|
+
const claude = mockProvider("claude", orch.sessionStore);
|
|
286
|
+
const openai = mockProvider("openai", orch.sessionStore);
|
|
287
|
+
orch.registerProvider(claude);
|
|
288
|
+
orch.registerProvider(openai);
|
|
289
|
+
await orch.switchProvider("claude");
|
|
290
|
+
expect(orch.currentProvider.name).toBe("claude");
|
|
291
|
+
await orch.switchProvider("openai");
|
|
292
|
+
expect(orch.currentProvider.name).toBe("openai");
|
|
293
|
+
await orch.switchProvider("claude");
|
|
294
|
+
expect(orch.currentProvider.name).toBe("claude");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
// =======================================================================
|
|
298
|
+
// Send Lock
|
|
299
|
+
// =======================================================================
|
|
300
|
+
describe("Send Lock", () => {
|
|
301
|
+
it("send lock prevents interleaved sends", async () => {
|
|
302
|
+
const { orch } = await makeWiredOrchestrator();
|
|
303
|
+
// Start first send (which we won't await yet)
|
|
304
|
+
const gen1 = orch.send("First");
|
|
305
|
+
// Start iterating first gen to acquire the lock
|
|
306
|
+
const iter1 = gen1[Symbol.asyncIterator]();
|
|
307
|
+
await iter1.next(); // should acquire lock
|
|
308
|
+
// Second send should throw
|
|
309
|
+
await expect(collectEvents(orch.send("Second"))).rejects.toThrow("Another message is already being processed");
|
|
310
|
+
// Finish first send
|
|
311
|
+
let done = false;
|
|
312
|
+
while (!done) {
|
|
313
|
+
const result = await iter1.next();
|
|
314
|
+
done = result.done ?? false;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
it("error in provider.send still releases send lock", async () => {
|
|
318
|
+
const orch = makeOrchestrator();
|
|
319
|
+
const provider = errorProvider("claude", orch.sessionStore);
|
|
320
|
+
orch.registerProvider(provider);
|
|
321
|
+
await orch.switchProvider("claude");
|
|
322
|
+
// First send will error
|
|
323
|
+
await expect(collectEvents(orch.send("Crash"))).rejects.toThrow("Provider exploded");
|
|
324
|
+
// Second send should work (lock released)
|
|
325
|
+
// Reset provider to one that works
|
|
326
|
+
provider.send = vi.fn().mockImplementation(async function* () {
|
|
327
|
+
yield { type: "text", text: "OK", delta: "OK" };
|
|
328
|
+
yield { type: "done", usage: { inputTokens: 10, outputTokens: 5 } };
|
|
329
|
+
});
|
|
330
|
+
const events = await collectEvents(orch.send("After crash"));
|
|
331
|
+
expect(events.some((e) => e.type === "done")).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
// =======================================================================
|
|
335
|
+
// Cost Tracking
|
|
336
|
+
// =======================================================================
|
|
337
|
+
describe("Cost Tracking", () => {
|
|
338
|
+
it("cost from multiple done events accumulates", async () => {
|
|
339
|
+
const orch = makeOrchestrator();
|
|
340
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
341
|
+
// Two done events in one send
|
|
342
|
+
provider.send = vi.fn().mockImplementation(async function* () {
|
|
343
|
+
yield { type: "text", text: "A", delta: "A" };
|
|
344
|
+
yield { type: "done", usage: { inputTokens: 50, outputTokens: 20, costUsd: 0.005 } };
|
|
345
|
+
});
|
|
346
|
+
orch.registerProvider(provider);
|
|
347
|
+
await orch.switchProvider("claude");
|
|
348
|
+
await collectEvents(orch.send("Msg 1"));
|
|
349
|
+
await collectEvents(orch.send("Msg 2"));
|
|
350
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.01, 3);
|
|
351
|
+
});
|
|
352
|
+
it("cost with no usage event does not change total", async () => {
|
|
353
|
+
const orch = makeOrchestrator();
|
|
354
|
+
const provider = noUsageProvider("claude", orch.sessionStore);
|
|
355
|
+
orch.registerProvider(provider);
|
|
356
|
+
await orch.switchProvider("claude");
|
|
357
|
+
await collectEvents(orch.send("Message"));
|
|
358
|
+
expect(orch.totalCostUsd).toBe(0);
|
|
359
|
+
});
|
|
360
|
+
it("ephemeral session: cost tracked in memory but not persisted to DB via addCost", async () => {
|
|
361
|
+
const orch = makeOrchestrator();
|
|
362
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
363
|
+
// Override createSession to return ephemeral — also create a real DB session
|
|
364
|
+
// so addMessage doesn't fail on FK constraint. The real test is that addCost skips.
|
|
365
|
+
provider.createSession = vi.fn().mockImplementation(async () => {
|
|
366
|
+
// Create a real session in DB so addMessage works
|
|
367
|
+
const real = orch.sessionStore.createSession("claude", "opus");
|
|
368
|
+
// But return an ephemeral-looking session ID wrapper
|
|
369
|
+
return {
|
|
370
|
+
id: real.id,
|
|
371
|
+
providerId: "claude",
|
|
372
|
+
createdAt: Date.now(),
|
|
373
|
+
lastActiveAt: Date.now(),
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
orch.registerProvider(provider);
|
|
377
|
+
await orch.switchProvider("claude");
|
|
378
|
+
await collectEvents(orch.send("Msg"));
|
|
379
|
+
// Cost is tracked in memory
|
|
380
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.01, 3);
|
|
381
|
+
});
|
|
382
|
+
it("addCost from external source accumulates", async () => {
|
|
383
|
+
const { orch } = await makeWiredOrchestrator();
|
|
384
|
+
await orch.startSession();
|
|
385
|
+
orch.addCost(0.05, 500, 200);
|
|
386
|
+
orch.addCost(0.03, 300, 100);
|
|
387
|
+
expect(orch.totalCostUsd).toBeCloseTo(0.08, 3);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
// =======================================================================
|
|
391
|
+
// Sticky Notes
|
|
392
|
+
// =======================================================================
|
|
393
|
+
describe("Sticky Notes", () => {
|
|
394
|
+
it("sticky notes prepended to every message", async () => {
|
|
395
|
+
const orch = makeOrchestrator();
|
|
396
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
397
|
+
orch.registerProvider(provider);
|
|
398
|
+
await orch.switchProvider("claude");
|
|
399
|
+
const stickies = new StickyManager();
|
|
400
|
+
stickies.add("Always use GPU 0");
|
|
401
|
+
orch.setStickyManager(stickies);
|
|
402
|
+
await collectEvents(orch.send("Train model"));
|
|
403
|
+
const sendCall = provider.send.mock.calls[0];
|
|
404
|
+
const message = sendCall[1];
|
|
405
|
+
expect(message).toContain("Always use GPU 0");
|
|
406
|
+
expect(message).toContain("Train model");
|
|
407
|
+
});
|
|
408
|
+
it("sticky notes change between sends", async () => {
|
|
409
|
+
const orch = makeOrchestrator();
|
|
410
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
411
|
+
orch.registerProvider(provider);
|
|
412
|
+
await orch.switchProvider("claude");
|
|
413
|
+
const stickies = new StickyManager();
|
|
414
|
+
stickies.add("Note 1");
|
|
415
|
+
orch.setStickyManager(stickies);
|
|
416
|
+
await collectEvents(orch.send("Msg 1"));
|
|
417
|
+
const msg1 = provider.send.mock.calls[0][1];
|
|
418
|
+
expect(msg1).toContain("Note 1");
|
|
419
|
+
stickies.add("Note 2");
|
|
420
|
+
await collectEvents(orch.send("Msg 2"));
|
|
421
|
+
const msg2 = provider.send.mock.calls[1][1];
|
|
422
|
+
expect(msg2).toContain("Note 1");
|
|
423
|
+
expect(msg2).toContain("Note 2");
|
|
424
|
+
});
|
|
425
|
+
it("no sticky notes: message passed through unchanged", async () => {
|
|
426
|
+
const orch = makeOrchestrator();
|
|
427
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
428
|
+
orch.registerProvider(provider);
|
|
429
|
+
await orch.switchProvider("claude");
|
|
430
|
+
await collectEvents(orch.send("Plain message"));
|
|
431
|
+
const sendCall = provider.send.mock.calls[0];
|
|
432
|
+
expect(sendCall[1]).toBe("Plain message");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
// =======================================================================
|
|
436
|
+
// Context Gate / Checkpoint
|
|
437
|
+
// =======================================================================
|
|
438
|
+
describe("Context Gate / Checkpoint", () => {
|
|
439
|
+
it("context gate not set: maybeCheckpoint is no-op", async () => {
|
|
440
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
441
|
+
// No context gate set
|
|
442
|
+
await collectEvents(orch.send("Message"));
|
|
443
|
+
expect(provider.resetHistory).not.toHaveBeenCalled();
|
|
444
|
+
});
|
|
445
|
+
it("context gate set: checkpoint when threshold exceeded", async () => {
|
|
446
|
+
const orch = makeOrchestrator();
|
|
447
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
448
|
+
// Provider reports high input tokens to trigger checkpoint
|
|
449
|
+
let sendCallCount = 0;
|
|
450
|
+
provider.send = vi.fn().mockImplementation(async function* () {
|
|
451
|
+
sendCallCount++;
|
|
452
|
+
if (sendCallCount === 1) {
|
|
453
|
+
yield { type: "text", text: "Reply", delta: "Reply" };
|
|
454
|
+
yield { type: "done", usage: { inputTokens: 999999, outputTokens: 50, costUsd: 0.01 } };
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
// Gist generation call
|
|
458
|
+
yield { type: "text", text: "My gist of the conversation", delta: "My gist of the conversation" };
|
|
459
|
+
yield { type: "done", usage: { inputTokens: 100, outputTokens: 50, costUsd: 0.005 } };
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
orch.registerProvider(provider);
|
|
463
|
+
await orch.switchProvider("claude");
|
|
464
|
+
const mockGate = {
|
|
465
|
+
onSessionStart: vi.fn(),
|
|
466
|
+
checkThreshold: vi.fn().mockReturnValue(true),
|
|
467
|
+
performCheckpointWithGist: vi.fn().mockReturnValue("=== CHECKPOINT BRIEFING ==="),
|
|
468
|
+
};
|
|
469
|
+
orch.setContextGate(mockGate);
|
|
470
|
+
const events = await collectEvents(orch.send("Trigger checkpoint"));
|
|
471
|
+
expect(mockGate.checkThreshold).toHaveBeenCalled();
|
|
472
|
+
expect(mockGate.performCheckpointWithGist).toHaveBeenCalledWith("My gist of the conversation");
|
|
473
|
+
expect(provider.resetHistory).toHaveBeenCalledWith(expect.anything(), "=== CHECKPOINT BRIEFING ===");
|
|
474
|
+
});
|
|
475
|
+
it("checkpoint resets provider history", async () => {
|
|
476
|
+
const orch = makeOrchestrator();
|
|
477
|
+
const provider = mockProvider("claude", orch.sessionStore);
|
|
478
|
+
let sendCallCount = 0;
|
|
479
|
+
provider.send = vi.fn().mockImplementation(async function* () {
|
|
480
|
+
sendCallCount++;
|
|
481
|
+
yield { type: "text", text: `Response ${sendCallCount}`, delta: `Response ${sendCallCount}` };
|
|
482
|
+
yield { type: "done", usage: { inputTokens: sendCallCount === 1 ? 999999 : 50, outputTokens: 20 } };
|
|
483
|
+
});
|
|
484
|
+
orch.registerProvider(provider);
|
|
485
|
+
await orch.switchProvider("claude");
|
|
486
|
+
const mockGate = {
|
|
487
|
+
onSessionStart: vi.fn(),
|
|
488
|
+
checkThreshold: vi.fn().mockImplementation((_model, tokens) => tokens > 100000),
|
|
489
|
+
performCheckpointWithGist: vi.fn().mockReturnValue("Briefing"),
|
|
490
|
+
};
|
|
491
|
+
orch.setContextGate(mockGate);
|
|
492
|
+
const events = await collectEvents(orch.send("Big message"));
|
|
493
|
+
// resetHistory should have been called once
|
|
494
|
+
expect(provider.resetHistory).toHaveBeenCalledTimes(1);
|
|
495
|
+
// The checkpoint text event should be in the output
|
|
496
|
+
const checkpointText = events.find((e) => e.type === "text" && e.text.includes("Context checkpoint"));
|
|
497
|
+
expect(checkpointText).toBeDefined();
|
|
498
|
+
});
|
|
499
|
+
it("context gate: low token count does not trigger checkpoint", async () => {
|
|
500
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
501
|
+
const mockGate = {
|
|
502
|
+
onSessionStart: vi.fn(),
|
|
503
|
+
checkThreshold: vi.fn().mockReturnValue(false),
|
|
504
|
+
performCheckpointWithGist: vi.fn(),
|
|
505
|
+
};
|
|
506
|
+
orch.setContextGate(mockGate);
|
|
507
|
+
await collectEvents(orch.send("Small message"));
|
|
508
|
+
expect(mockGate.checkThreshold).toHaveBeenCalled();
|
|
509
|
+
expect(mockGate.performCheckpointWithGist).not.toHaveBeenCalled();
|
|
510
|
+
expect(provider.resetHistory).not.toHaveBeenCalled();
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
// =======================================================================
|
|
514
|
+
// State Machine
|
|
515
|
+
// =======================================================================
|
|
516
|
+
describe("State Machine", () => {
|
|
517
|
+
it("transitions: idle -> active on startSession", async () => {
|
|
518
|
+
const orch = makeOrchestrator();
|
|
519
|
+
orch.registerProvider(mockProvider("claude"));
|
|
520
|
+
await orch.switchProvider("claude");
|
|
521
|
+
expect(orch.currentState).toBe("idle");
|
|
522
|
+
await orch.startSession();
|
|
523
|
+
expect(orch.currentState).toBe("active");
|
|
524
|
+
});
|
|
525
|
+
it("start session auto-authenticates provider", async () => {
|
|
526
|
+
const orch = makeOrchestrator();
|
|
527
|
+
const provider = mockProvider("claude");
|
|
528
|
+
orch.registerProvider(provider);
|
|
529
|
+
// Don't switch manually — let startSession do it
|
|
530
|
+
await orch.startSession();
|
|
531
|
+
expect(provider.authenticate).toHaveBeenCalledOnce();
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
// =======================================================================
|
|
535
|
+
// Interrupt
|
|
536
|
+
// =======================================================================
|
|
537
|
+
describe("Interrupt", () => {
|
|
538
|
+
it("interrupt during send aborts provider", async () => {
|
|
539
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
540
|
+
// Start a send
|
|
541
|
+
const gen = orch.send("Long running");
|
|
542
|
+
const iter = gen[Symbol.asyncIterator]();
|
|
543
|
+
// Get first event
|
|
544
|
+
await iter.next();
|
|
545
|
+
// Interrupt
|
|
546
|
+
orch.interrupt();
|
|
547
|
+
expect(provider.interrupt).toHaveBeenCalled();
|
|
548
|
+
});
|
|
549
|
+
it("interrupt with no active session is safe", () => {
|
|
550
|
+
const orch = makeOrchestrator();
|
|
551
|
+
// Should not throw
|
|
552
|
+
expect(() => orch.interrupt()).not.toThrow();
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
// =======================================================================
|
|
556
|
+
// setModel / fetchModels
|
|
557
|
+
// =======================================================================
|
|
558
|
+
describe("setModel / fetchModels", () => {
|
|
559
|
+
it("setModel resets session", async () => {
|
|
560
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
561
|
+
await orch.startSession();
|
|
562
|
+
expect(orch.activeSession).not.toBeNull();
|
|
563
|
+
await orch.setModel("claude-sonnet-4-6");
|
|
564
|
+
expect(provider.currentModel).toBe("claude-sonnet-4-6");
|
|
565
|
+
expect(provider.closeSession).toHaveBeenCalled();
|
|
566
|
+
expect(orch.activeSession).toBeNull();
|
|
567
|
+
});
|
|
568
|
+
it("fetchModels delegates to active provider", async () => {
|
|
569
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
570
|
+
const models = await orch.fetchModels();
|
|
571
|
+
expect(provider.fetchModels).toHaveBeenCalled();
|
|
572
|
+
expect(models).toEqual([{ id: "test-model", name: "Test Model" }]);
|
|
573
|
+
});
|
|
574
|
+
it("fetchModels auto-switches provider if none active", async () => {
|
|
575
|
+
const orch = makeOrchestrator();
|
|
576
|
+
const provider = mockProvider("claude");
|
|
577
|
+
orch.registerProvider(provider);
|
|
578
|
+
const models = await orch.fetchModels();
|
|
579
|
+
expect(provider.authenticate).toHaveBeenCalled();
|
|
580
|
+
expect(models).toEqual([{ id: "test-model", name: "Test Model" }]);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
// =======================================================================
|
|
584
|
+
// Reasoning Effort
|
|
585
|
+
// =======================================================================
|
|
586
|
+
describe("Reasoning Effort", () => {
|
|
587
|
+
it("reasoningEffort reflects provider setting", async () => {
|
|
588
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
589
|
+
provider.reasoningEffort = "high";
|
|
590
|
+
expect(orch.reasoningEffort).toBe("high");
|
|
591
|
+
});
|
|
592
|
+
it("setReasoningEffort updates provider", async () => {
|
|
593
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
594
|
+
await orch.setReasoningEffort("max");
|
|
595
|
+
expect(provider.reasoningEffort).toBe("max");
|
|
596
|
+
});
|
|
597
|
+
it("reasoningEffort is null when no provider active", () => {
|
|
598
|
+
const orch = makeOrchestrator();
|
|
599
|
+
expect(orch.reasoningEffort).toBeNull();
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
// =======================================================================
|
|
603
|
+
// Tools
|
|
604
|
+
// =======================================================================
|
|
605
|
+
describe("Tools", () => {
|
|
606
|
+
it("registerTool adds tool and passes it in send", async () => {
|
|
607
|
+
const { orch, provider } = await makeWiredOrchestrator();
|
|
608
|
+
const tool = makeTool("remote_exec");
|
|
609
|
+
orch.registerTool(tool);
|
|
610
|
+
await collectEvents(orch.send("Use the tool"));
|
|
611
|
+
const sendCall = provider.send.mock.calls[0];
|
|
612
|
+
const tools = sendCall[2];
|
|
613
|
+
expect(tools.some((t) => t.name === "remote_exec")).toBe(true);
|
|
614
|
+
});
|
|
615
|
+
it("registerTools adds multiple tools", async () => {
|
|
616
|
+
const { orch } = await makeWiredOrchestrator();
|
|
617
|
+
orch.registerTools([makeTool("tool_a"), makeTool("tool_b")]);
|
|
618
|
+
expect(orch.getTools()).toHaveLength(2);
|
|
619
|
+
});
|
|
620
|
+
it("duplicate tool registration is prevented", async () => {
|
|
621
|
+
const { orch } = await makeWiredOrchestrator();
|
|
622
|
+
orch.registerTool(makeTool("tool_a"));
|
|
623
|
+
orch.registerTool(makeTool("tool_a"));
|
|
624
|
+
expect(orch.getTools()).toHaveLength(1);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
// =======================================================================
|
|
628
|
+
// Error Propagation
|
|
629
|
+
// =======================================================================
|
|
630
|
+
describe("Error Propagation", () => {
|
|
631
|
+
it("error in provider.send propagates to caller", async () => {
|
|
632
|
+
const orch = makeOrchestrator();
|
|
633
|
+
const provider = errorProvider("claude", orch.sessionStore);
|
|
634
|
+
orch.registerProvider(provider);
|
|
635
|
+
await orch.switchProvider("claude");
|
|
636
|
+
await expect(collectEvents(orch.send("Boom"))).rejects.toThrow("Provider exploded");
|
|
637
|
+
});
|
|
638
|
+
it("error does not leave cost in inconsistent state", async () => {
|
|
639
|
+
const orch = makeOrchestrator();
|
|
640
|
+
const provider = errorProvider("claude", orch.sessionStore);
|
|
641
|
+
orch.registerProvider(provider);
|
|
642
|
+
await orch.switchProvider("claude");
|
|
643
|
+
try {
|
|
644
|
+
await collectEvents(orch.send("Boom"));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// expected
|
|
648
|
+
}
|
|
649
|
+
// Cost should still be 0 since no done event was emitted
|
|
650
|
+
expect(orch.totalCostUsd).toBe(0);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
// =======================================================================
|
|
654
|
+
// lastInputTokens
|
|
655
|
+
// =======================================================================
|
|
656
|
+
describe("lastInputTokens", () => {
|
|
657
|
+
it("tracks last input token count from done event", async () => {
|
|
658
|
+
const { orch } = await makeWiredOrchestrator();
|
|
659
|
+
await collectEvents(orch.send("Check tokens"));
|
|
660
|
+
expect(orch.lastInputTokens).toBe(100);
|
|
661
|
+
});
|
|
662
|
+
it("updates with each send", async () => {
|
|
663
|
+
const orch = makeOrchestrator();
|
|
664
|
+
const provider = chattyProvider("claude", orch.sessionStore);
|
|
665
|
+
orch.registerProvider(provider);
|
|
666
|
+
await orch.switchProvider("claude");
|
|
667
|
+
await collectEvents(orch.send("Turn 1")); // 50 input tokens
|
|
668
|
+
expect(orch.lastInputTokens).toBe(50);
|
|
669
|
+
await collectEvents(orch.send("Turn 2")); // 100 input tokens
|
|
670
|
+
expect(orch.lastInputTokens).toBe(100);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
//# sourceMappingURL=orchestrator-integration.test.js.map
|