@vellumai/assistant 0.8.11 → 0.8.12-staging.2
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/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +633 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/bundler/app-compiler.ts +8 -0
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +225 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/security/redact-json.ts +61 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* When loadConfig()/loadRawConfig() quarantines a corrupt config.json, it
|
|
3
|
-
* appends a bulletin to <workspace>/UPDATES.md so the background update-
|
|
4
|
-
* bulletin job picks up the event inside a background-only conversation.
|
|
5
|
-
* The agent decides whether to surface the quarantine to the user — it's
|
|
6
|
-
* agent-visible context, not a push notification.
|
|
7
|
-
*
|
|
8
|
-
* The bulletin is keyed on the quarantine filename via an HTML marker so
|
|
9
|
-
* repeated appends for the same quarantine are idempotent — per the
|
|
10
|
-
* Release-Update-Hygiene rule in the root AGENTS.md, idempotency at both the
|
|
11
|
-
* runner level AND an in-file marker is required to close the crash-mid-append
|
|
12
|
-
* window.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
existsSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
readdirSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
rmSync,
|
|
21
|
-
writeFileSync,
|
|
22
|
-
} from "node:fs";
|
|
23
|
-
import { basename, join } from "node:path";
|
|
24
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
_appendQuarantineBulletin,
|
|
28
|
-
invalidateConfigCache,
|
|
29
|
-
loadConfig,
|
|
30
|
-
} from "../config/loader.js";
|
|
31
|
-
import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Helpers
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
38
|
-
const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
|
|
39
|
-
const UPDATES_PATH = join(WORKSPACE_DIR, "UPDATES.md");
|
|
40
|
-
|
|
41
|
-
function ensureTestDir(): void {
|
|
42
|
-
const dirs = [
|
|
43
|
-
WORKSPACE_DIR,
|
|
44
|
-
join(WORKSPACE_DIR, "data"),
|
|
45
|
-
join(WORKSPACE_DIR, "data", "memory"),
|
|
46
|
-
join(WORKSPACE_DIR, "data", "memory", "knowledge"),
|
|
47
|
-
join(WORKSPACE_DIR, "data", "logs"),
|
|
48
|
-
];
|
|
49
|
-
for (const dir of dirs) {
|
|
50
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function resetWorkspace(): void {
|
|
55
|
-
for (const name of readdirSync(WORKSPACE_DIR)) {
|
|
56
|
-
rmSync(join(WORKSPACE_DIR, name), { recursive: true, force: true });
|
|
57
|
-
}
|
|
58
|
-
ensureTestDir();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function listQuarantinedFiles(): string[] {
|
|
62
|
-
return readdirSync(WORKSPACE_DIR).filter((name) =>
|
|
63
|
-
/^config\.json\.corrupt-.+\.json$/.test(name),
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Tests
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
describe("config-quarantine UPDATES.md bulletin", () => {
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
resetWorkspace();
|
|
74
|
-
setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc"));
|
|
75
|
-
invalidateConfigCache();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
afterEach(() => {
|
|
79
|
-
setStorePathForTesting(null);
|
|
80
|
-
invalidateConfigCache();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("writes a bulletin with the quarantine marker when config.json is corrupt", () => {
|
|
84
|
-
writeFileSync(CONFIG_PATH, '{"provider": "anthropic", "mo');
|
|
85
|
-
|
|
86
|
-
loadConfig();
|
|
87
|
-
|
|
88
|
-
const [quarantinedName] = listQuarantinedFiles();
|
|
89
|
-
expect(quarantinedName).toBeDefined();
|
|
90
|
-
const quarantinedPath = join(WORKSPACE_DIR, quarantinedName);
|
|
91
|
-
|
|
92
|
-
expect(existsSync(UPDATES_PATH)).toBe(true);
|
|
93
|
-
const body = readFileSync(UPDATES_PATH, "utf-8");
|
|
94
|
-
|
|
95
|
-
// Heading must match the wording the background update-bulletin job
|
|
96
|
-
// relays to the user.
|
|
97
|
-
expect(body).toContain("## Config was reset to defaults");
|
|
98
|
-
// Quarantine file path is what the user cats to recover.
|
|
99
|
-
expect(body).toContain(quarantinedPath);
|
|
100
|
-
// Idempotency marker is shape-exact, basename-keyed (not full path).
|
|
101
|
-
expect(body).toContain(`<!-- config-quarantine:${quarantinedName} -->`);
|
|
102
|
-
expect(body).toMatch(
|
|
103
|
-
/<!-- config-quarantine:config\.json\.corrupt-.+\.json -->/,
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("two successive quarantines append two distinct bulletins (not overwrite)", () => {
|
|
108
|
-
// First corruption round.
|
|
109
|
-
writeFileSync(CONFIG_PATH, '{"partial": ');
|
|
110
|
-
loadConfig();
|
|
111
|
-
invalidateConfigCache();
|
|
112
|
-
|
|
113
|
-
const firstQuarantined = listQuarantinedFiles();
|
|
114
|
-
expect(firstQuarantined).toHaveLength(1);
|
|
115
|
-
const firstBody = readFileSync(UPDATES_PATH, "utf-8");
|
|
116
|
-
const firstMarker = `<!-- config-quarantine:${firstQuarantined[0]} -->`;
|
|
117
|
-
expect(firstBody).toContain(firstMarker);
|
|
118
|
-
|
|
119
|
-
// Loader wrote a fresh default config.json after quarantine; corrupt it
|
|
120
|
-
// again. Sleep briefly to guarantee a different ISO-timestamp millisecond
|
|
121
|
-
// component and therefore a distinct quarantine filename.
|
|
122
|
-
const untilDifferentMs = Date.now() + 5;
|
|
123
|
-
while (Date.now() < untilDifferentMs) {
|
|
124
|
-
/* spin */
|
|
125
|
-
}
|
|
126
|
-
writeFileSync(CONFIG_PATH, "still not json");
|
|
127
|
-
loadConfig();
|
|
128
|
-
|
|
129
|
-
const quarantined = listQuarantinedFiles().sort();
|
|
130
|
-
expect(quarantined).toHaveLength(2);
|
|
131
|
-
|
|
132
|
-
const body = readFileSync(UPDATES_PATH, "utf-8");
|
|
133
|
-
for (const name of quarantined) {
|
|
134
|
-
expect(body).toContain(`<!-- config-quarantine:${name} -->`);
|
|
135
|
-
}
|
|
136
|
-
// Two "Config was reset to defaults" sections — appended, not overwritten.
|
|
137
|
-
const headingMatches = body.match(/## Config was reset to defaults/g) ?? [];
|
|
138
|
-
expect(headingMatches).toHaveLength(2);
|
|
139
|
-
// The earlier marker is still present (append semantics).
|
|
140
|
-
expect(body).toContain(firstMarker);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("idempotent: pre-existing marker for the same quarantine filename skips the append", () => {
|
|
144
|
-
// Simulate a crash-mid-append: UPDATES.md already contains a marker for
|
|
145
|
-
// a specific quarantine filename. A follow-up call referencing the same
|
|
146
|
-
// filename must leave the file untouched.
|
|
147
|
-
const quarantineName = "config.json.corrupt-2026-04-20T12-00-00.000Z.json";
|
|
148
|
-
const quarantinePath = join(WORKSPACE_DIR, quarantineName);
|
|
149
|
-
const preexisting =
|
|
150
|
-
`## Config was reset to defaults\n\n` +
|
|
151
|
-
`Pre-existing bulletin for ${quarantinePath}.\n\n` +
|
|
152
|
-
`<!-- config-quarantine:${quarantineName} -->\n`;
|
|
153
|
-
writeFileSync(UPDATES_PATH, preexisting, "utf-8");
|
|
154
|
-
// Also create the quarantine file on disk so the helper sees an
|
|
155
|
-
// environment consistent with a prior successful rename.
|
|
156
|
-
writeFileSync(quarantinePath, "{ not json", "utf-8");
|
|
157
|
-
|
|
158
|
-
_appendQuarantineBulletin(CONFIG_PATH, quarantinePath);
|
|
159
|
-
|
|
160
|
-
const after = readFileSync(UPDATES_PATH, "utf-8");
|
|
161
|
-
expect(after).toBe(preexisting);
|
|
162
|
-
// Exactly one marker present — no duplicate was appended.
|
|
163
|
-
expect(
|
|
164
|
-
after.match(
|
|
165
|
-
new RegExp(`<!-- config-quarantine:${quarantineName} -->`, "g"),
|
|
166
|
-
)?.length,
|
|
167
|
-
).toBe(1);
|
|
168
|
-
expect(basename(quarantinePath)).toBe(quarantineName);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("valid config.json does not create UPDATES.md (regression guard)", () => {
|
|
172
|
-
writeFileSync(
|
|
173
|
-
CONFIG_PATH,
|
|
174
|
-
JSON.stringify({ provider: "anthropic", model: "claude-opus-4-7" }),
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
loadConfig();
|
|
178
|
-
|
|
179
|
-
expect(listQuarantinedFiles()).toHaveLength(0);
|
|
180
|
-
expect(existsSync(UPDATES_PATH)).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("appends (does not overwrite) when UPDATES.md already has unrelated content", () => {
|
|
184
|
-
const priorContent =
|
|
185
|
-
`## Some earlier bulletin\n\n` +
|
|
186
|
-
`Unrelated prior content from a previous migration.\n\n` +
|
|
187
|
-
`<!-- release-note-id:unrelated-note -->\n`;
|
|
188
|
-
writeFileSync(UPDATES_PATH, priorContent, "utf-8");
|
|
189
|
-
|
|
190
|
-
writeFileSync(CONFIG_PATH, "{oops");
|
|
191
|
-
loadConfig();
|
|
192
|
-
|
|
193
|
-
const body = readFileSync(UPDATES_PATH, "utf-8");
|
|
194
|
-
// Prior content preserved verbatim at the start.
|
|
195
|
-
expect(body.startsWith(priorContent)).toBe(true);
|
|
196
|
-
// New bulletin appended.
|
|
197
|
-
expect(body).toContain("## Config was reset to defaults");
|
|
198
|
-
expect(body).toMatch(
|
|
199
|
-
/<!-- config-quarantine:config\.json\.corrupt-.+\.json -->/,
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { v4 as uuid } from "uuid";
|
|
4
|
-
|
|
5
|
-
mock.module("../util/logger.js", () => ({
|
|
6
|
-
getLogger: () =>
|
|
7
|
-
new Proxy({} as Record<string, unknown>, {
|
|
8
|
-
get: () => () => {},
|
|
9
|
-
}),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
import { maybeEnqueueConversationStartersJob } from "../memory/conversation-starters-cadence.js";
|
|
13
|
-
import { getSqlite } from "../memory/db-connection.js";
|
|
14
|
-
import { initializeDb } from "../memory/db-init.js";
|
|
15
|
-
initializeDb();
|
|
16
|
-
|
|
17
|
-
function clearTables() {
|
|
18
|
-
getSqlite().run("DELETE FROM memory_graph_nodes");
|
|
19
|
-
getSqlite().run("DELETE FROM memory_jobs");
|
|
20
|
-
getSqlite().run("DELETE FROM memory_checkpoints");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function insertMemoryNode(scopeId = "default") {
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
getSqlite().run(
|
|
26
|
-
`INSERT INTO memory_graph_nodes (
|
|
27
|
-
id, content, type, created, last_accessed, last_consolidated,
|
|
28
|
-
emotional_charge, fidelity, confidence, significance,
|
|
29
|
-
stability, reinforcement_count, last_reinforced,
|
|
30
|
-
source_conversations, source_type, scope_id
|
|
31
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'vivid', 0.8, 0.5, 14, 0, ?, '[]', 'inferred', ?)`,
|
|
32
|
-
[
|
|
33
|
-
uuid(),
|
|
34
|
-
"test statement",
|
|
35
|
-
"semantic",
|
|
36
|
-
now,
|
|
37
|
-
now,
|
|
38
|
-
now,
|
|
39
|
-
'{"valence":0,"intensity":0.1,"decayCurve":"linear","decayRate":0.05,"originalIntensity":0.1}',
|
|
40
|
-
now,
|
|
41
|
-
scopeId,
|
|
42
|
-
],
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function setCheckpoint(key: string, value: string) {
|
|
47
|
-
getSqlite().run(
|
|
48
|
-
`INSERT OR REPLACE INTO memory_checkpoints (key, value, updated_at) VALUES (?, ?, ?)`,
|
|
49
|
-
[key, value, Date.now()],
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getPendingJobs(): Array<{ type: string }> {
|
|
54
|
-
return getSqlite()
|
|
55
|
-
.prepare(
|
|
56
|
-
`SELECT type FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
|
|
57
|
-
)
|
|
58
|
-
.all() as Array<{ type: string }>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
beforeEach(() => {
|
|
62
|
-
clearTables();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("maybeEnqueueConversationStartersJob", () => {
|
|
66
|
-
test("no-op when zero memory nodes", () => {
|
|
67
|
-
maybeEnqueueConversationStartersJob("default");
|
|
68
|
-
expect(getPendingJobs()).toHaveLength(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("enqueues when threshold exceeded (<=10 nodes, threshold=1)", () => {
|
|
72
|
-
insertMemoryNode();
|
|
73
|
-
insertMemoryNode();
|
|
74
|
-
|
|
75
|
-
maybeEnqueueConversationStartersJob("default");
|
|
76
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("no-op when delta below threshold", () => {
|
|
80
|
-
for (let i = 0; i < 5; i++) insertMemoryNode();
|
|
81
|
-
|
|
82
|
-
// Set checkpoint to current count — no new items since last gen
|
|
83
|
-
setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
|
|
84
|
-
|
|
85
|
-
maybeEnqueueConversationStartersJob("default");
|
|
86
|
-
expect(getPendingJobs()).toHaveLength(0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("uses higher threshold for larger memory counts (>50 nodes, threshold=10)", () => {
|
|
90
|
-
for (let i = 0; i < 55; i++) insertMemoryNode();
|
|
91
|
-
|
|
92
|
-
// Set checkpoint so delta is only 4 (below threshold of 10)
|
|
93
|
-
setCheckpoint(
|
|
94
|
-
"conversation_starters:item_count_at_last_gen:default",
|
|
95
|
-
"51",
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
maybeEnqueueConversationStartersJob("default");
|
|
99
|
-
expect(getPendingJobs()).toHaveLength(0);
|
|
100
|
-
|
|
101
|
-
// Add more to exceed threshold
|
|
102
|
-
for (let i = 0; i < 6; i++) insertMemoryNode();
|
|
103
|
-
|
|
104
|
-
maybeEnqueueConversationStartersJob("default");
|
|
105
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("dedup prevents double-enqueue", () => {
|
|
109
|
-
insertMemoryNode();
|
|
110
|
-
insertMemoryNode();
|
|
111
|
-
|
|
112
|
-
maybeEnqueueConversationStartersJob("default");
|
|
113
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
114
|
-
|
|
115
|
-
// Call again — should not create a second job
|
|
116
|
-
maybeEnqueueConversationStartersJob("default");
|
|
117
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("enqueues when active memory count drops below the last-generation checkpoint", () => {
|
|
121
|
-
// Start with 5 nodes and set checkpoint to 5
|
|
122
|
-
for (let i = 0; i < 5; i++) insertMemoryNode();
|
|
123
|
-
setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
|
|
124
|
-
|
|
125
|
-
// Simulate pruning: mark 3 nodes as gone (reducing totalActive to 2)
|
|
126
|
-
const ids = (
|
|
127
|
-
getSqlite()
|
|
128
|
-
.prepare(`SELECT id FROM memory_graph_nodes LIMIT 3`)
|
|
129
|
-
.all() as Array<{ id: string }>
|
|
130
|
-
).map((r) => r.id);
|
|
131
|
-
for (const id of ids) {
|
|
132
|
-
getSqlite().run(
|
|
133
|
-
`UPDATE memory_graph_nodes SET fidelity = 'gone' WHERE id = ?`,
|
|
134
|
-
[id],
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// The checkpoint is now ahead of the active memory count. This should
|
|
139
|
-
// enqueue a refresh immediately so stale starters can recover.
|
|
140
|
-
maybeEnqueueConversationStartersJob("default");
|
|
141
|
-
expect(getPendingJobs()).toHaveLength(1);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("scopes are independent", () => {
|
|
145
|
-
insertMemoryNode("scope-a");
|
|
146
|
-
insertMemoryNode("scope-b");
|
|
147
|
-
|
|
148
|
-
maybeEnqueueConversationStartersJob("scope-a");
|
|
149
|
-
maybeEnqueueConversationStartersJob("scope-b");
|
|
150
|
-
|
|
151
|
-
const jobs = getSqlite()
|
|
152
|
-
.prepare(
|
|
153
|
-
`SELECT payload FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
|
|
154
|
-
)
|
|
155
|
-
.all() as Array<{ payload: string }>;
|
|
156
|
-
|
|
157
|
-
expect(jobs).toHaveLength(2);
|
|
158
|
-
const payloads = jobs.map((j) => JSON.parse(j.payload).scopeId).sort();
|
|
159
|
-
expect(payloads).toEqual(["scope-a", "scope-b"]);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
mock.module("../util/logger.js", () => ({
|
|
4
|
-
getLogger: () =>
|
|
5
|
-
new Proxy({} as Record<string, unknown>, {
|
|
6
|
-
get: () => () => {},
|
|
7
|
-
}),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
// Track call starts for assertions
|
|
11
|
-
const startedCalls: Array<{
|
|
12
|
-
phoneNumber: string;
|
|
13
|
-
task: string;
|
|
14
|
-
conversationId: string;
|
|
15
|
-
}> = [];
|
|
16
|
-
let mockStartCallResult:
|
|
17
|
-
| {
|
|
18
|
-
ok: true;
|
|
19
|
-
session: { id: string };
|
|
20
|
-
callSid: string;
|
|
21
|
-
callerIdentityMode: string;
|
|
22
|
-
}
|
|
23
|
-
| { ok: false; error: string } = {
|
|
24
|
-
ok: true,
|
|
25
|
-
session: { id: "mock-call-session" },
|
|
26
|
-
callSid: "CA-mock",
|
|
27
|
-
callerIdentityMode: "assistant_number",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
mock.module("../calls/call-domain.js", () => ({
|
|
31
|
-
startCall: async (input: {
|
|
32
|
-
phoneNumber: string;
|
|
33
|
-
task: string;
|
|
34
|
-
conversationId: string;
|
|
35
|
-
}) => {
|
|
36
|
-
startedCalls.push(input);
|
|
37
|
-
return mockStartCallResult;
|
|
38
|
-
},
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
mock.module("../config/env.js", () => ({
|
|
42
|
-
isHttpAuthDisabled: () => true,
|
|
43
|
-
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
// Mock conversation-key-store for call_back conversation creation
|
|
47
|
-
let conversationCounter = 0;
|
|
48
|
-
mock.module("../memory/conversation-key-store.js", () => ({
|
|
49
|
-
getOrCreateConversation: () => ({
|
|
50
|
-
conversationId: `mock-conv-${++conversationCounter}`,
|
|
51
|
-
created: true,
|
|
52
|
-
}),
|
|
53
|
-
}));
|
|
54
|
-
|
|
55
|
-
import {
|
|
56
|
-
createCallSession,
|
|
57
|
-
createPendingQuestion,
|
|
58
|
-
} from "../calls/call-store.js";
|
|
59
|
-
import { getDb } from "../memory/db-connection.js";
|
|
60
|
-
import { initializeDb } from "../memory/db-init.js";
|
|
61
|
-
import {
|
|
62
|
-
createGuardianActionDelivery,
|
|
63
|
-
createGuardianActionRequest,
|
|
64
|
-
getGuardianActionRequest,
|
|
65
|
-
markTimedOutWithReason,
|
|
66
|
-
progressFollowupState,
|
|
67
|
-
startFollowupFromExpiredRequest,
|
|
68
|
-
updateDeliveryStatus,
|
|
69
|
-
} from "../memory/guardian-action-store.js";
|
|
70
|
-
import { conversations } from "../memory/schema.js";
|
|
71
|
-
import { executeFollowupAction } from "../runtime/guardian-action-followup-executor.js";
|
|
72
|
-
import { resolveCounterparty } from "../runtime/guardian-action-followup-executor.js";
|
|
73
|
-
|
|
74
|
-
initializeDb();
|
|
75
|
-
|
|
76
|
-
function ensureConversation(id: string): void {
|
|
77
|
-
const db = getDb();
|
|
78
|
-
const now = Date.now();
|
|
79
|
-
db.insert(conversations)
|
|
80
|
-
.values({
|
|
81
|
-
id,
|
|
82
|
-
title: `Conversation ${id}`,
|
|
83
|
-
createdAt: now,
|
|
84
|
-
updatedAt: now,
|
|
85
|
-
})
|
|
86
|
-
.run();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function resetTables(): void {
|
|
90
|
-
const db = getDb();
|
|
91
|
-
db.run("DELETE FROM guardian_action_deliveries");
|
|
92
|
-
db.run("DELETE FROM guardian_action_requests");
|
|
93
|
-
db.run("DELETE FROM call_pending_questions");
|
|
94
|
-
db.run("DELETE FROM call_events");
|
|
95
|
-
db.run("DELETE FROM call_sessions");
|
|
96
|
-
db.run("DELETE FROM messages");
|
|
97
|
-
db.run("DELETE FROM conversations");
|
|
98
|
-
startedCalls.length = 0;
|
|
99
|
-
conversationCounter = 0;
|
|
100
|
-
mockStartCallResult = {
|
|
101
|
-
ok: true,
|
|
102
|
-
session: { id: "mock-call-session" },
|
|
103
|
-
callSid: "CA-mock",
|
|
104
|
-
callerIdentityMode: "assistant_number",
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Create a request in `dispatching` state ready for the executor.
|
|
110
|
-
* The call session has fromNumber='+15550001111' (the counterparty).
|
|
111
|
-
*/
|
|
112
|
-
function createDispatchingRequest(convId: string, action: "call_back") {
|
|
113
|
-
ensureConversation(convId);
|
|
114
|
-
const session = createCallSession({
|
|
115
|
-
conversationId: convId,
|
|
116
|
-
provider: "twilio",
|
|
117
|
-
fromNumber: "+15550001111",
|
|
118
|
-
toNumber: "+15550002222",
|
|
119
|
-
});
|
|
120
|
-
const pq = createPendingQuestion(session.id, "What is the gate code?");
|
|
121
|
-
const request = createGuardianActionRequest({
|
|
122
|
-
kind: "ask_guardian",
|
|
123
|
-
sourceChannel: "phone",
|
|
124
|
-
sourceConversationId: convId,
|
|
125
|
-
callSessionId: session.id,
|
|
126
|
-
pendingQuestionId: pq.id,
|
|
127
|
-
questionText: pq.questionText,
|
|
128
|
-
expiresAt: Date.now() - 10_000,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const deliveryConvId = `delivery-conv-${request.id}`;
|
|
132
|
-
ensureConversation(deliveryConvId);
|
|
133
|
-
const delivery = createGuardianActionDelivery({
|
|
134
|
-
requestId: request.id,
|
|
135
|
-
destinationChannel: "telegram",
|
|
136
|
-
destinationChatId: "chat-123",
|
|
137
|
-
destinationExternalUserId: "user-456",
|
|
138
|
-
destinationConversationId: deliveryConvId,
|
|
139
|
-
});
|
|
140
|
-
updateDeliveryStatus(delivery.id, "sent");
|
|
141
|
-
|
|
142
|
-
// Expire the request
|
|
143
|
-
markTimedOutWithReason(request.id, "call_timeout");
|
|
144
|
-
|
|
145
|
-
// Start follow-up
|
|
146
|
-
startFollowupFromExpiredRequest(request.id, "The gate code is 1234");
|
|
147
|
-
|
|
148
|
-
// Progress to dispatching with the given action
|
|
149
|
-
progressFollowupState(request.id, "dispatching", action);
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
request: getGuardianActionRequest(request.id)!,
|
|
153
|
-
delivery,
|
|
154
|
-
callSession: session,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
describe("guardian-action-followup-executor", () => {
|
|
159
|
-
beforeEach(() => {
|
|
160
|
-
resetTables();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// ── Counterparty resolution ─────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
describe("resolveCounterparty", () => {
|
|
166
|
-
test("resolves fromNumber as counterparty for inbound call", () => {
|
|
167
|
-
ensureConversation("cp-test-1");
|
|
168
|
-
const session = createCallSession({
|
|
169
|
-
conversationId: "cp-test-1",
|
|
170
|
-
provider: "twilio",
|
|
171
|
-
fromNumber: "+15550001111",
|
|
172
|
-
toNumber: "+15550002222",
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const result = resolveCounterparty(session.id);
|
|
176
|
-
expect(result).not.toBeNull();
|
|
177
|
-
expect(result!.phoneNumber).toBe("+15550001111");
|
|
178
|
-
expect(result!.displayIdentifier).toBe("+15550001111");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("resolves toNumber as counterparty for outbound call", () => {
|
|
182
|
-
ensureConversation("cp-test-outbound");
|
|
183
|
-
const session = createCallSession({
|
|
184
|
-
conversationId: "cp-test-outbound",
|
|
185
|
-
provider: "twilio",
|
|
186
|
-
fromNumber: "+15550002222", // assistant's number
|
|
187
|
-
toNumber: "+15550001111", // callee (the counterparty)
|
|
188
|
-
initiatedFromConversationId: "cp-test-outbound", // signals outbound
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const result = resolveCounterparty(session.id);
|
|
192
|
-
expect(result).not.toBeNull();
|
|
193
|
-
expect(result!.phoneNumber).toBe("+15550001111");
|
|
194
|
-
expect(result!.displayIdentifier).toBe("+15550001111");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("returns null for nonexistent call session", () => {
|
|
198
|
-
const result = resolveCounterparty("nonexistent-session-id");
|
|
199
|
-
expect(result).toBeNull();
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// ── call_back execution ─────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
describe("call_back", () => {
|
|
206
|
-
test("starts outbound call to counterparty and finalizes as completed", async () => {
|
|
207
|
-
const { request } = createDispatchingRequest("exec-call-1", "call_back");
|
|
208
|
-
|
|
209
|
-
const result = await executeFollowupAction(request.id, "call_back");
|
|
210
|
-
|
|
211
|
-
expect(result.ok).toBe(true);
|
|
212
|
-
expect(result.action).toBe("call_back");
|
|
213
|
-
expect(result.guardianReplyText.length).toBeGreaterThan(0);
|
|
214
|
-
|
|
215
|
-
// Verify call was started to the counterparty
|
|
216
|
-
expect(startedCalls.length).toBe(1);
|
|
217
|
-
expect(startedCalls[0].phoneNumber).toBe("+15550001111");
|
|
218
|
-
expect(startedCalls[0].task).toContain("gate code");
|
|
219
|
-
|
|
220
|
-
// Verify follow-up state is completed
|
|
221
|
-
const updated = getGuardianActionRequest(request.id);
|
|
222
|
-
expect(updated!.followupState).toBe("completed");
|
|
223
|
-
expect(updated!.followupCompletedAt).toBeGreaterThan(0);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("confirmation text mentions calling back", async () => {
|
|
227
|
-
const { request } = createDispatchingRequest("exec-call-2", "call_back");
|
|
228
|
-
|
|
229
|
-
const result = await executeFollowupAction(request.id, "call_back");
|
|
230
|
-
|
|
231
|
-
expect(result.ok).toBe(true);
|
|
232
|
-
expect(result.guardianReplyText).toContain("calling");
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("failed call start finalizes as failed with error message", async () => {
|
|
236
|
-
mockStartCallResult = { ok: false, error: "Twilio account suspended" };
|
|
237
|
-
const { request } = createDispatchingRequest(
|
|
238
|
-
"exec-call-fail-1",
|
|
239
|
-
"call_back",
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const result = await executeFollowupAction(request.id, "call_back");
|
|
243
|
-
|
|
244
|
-
expect(result.ok).toBe(false);
|
|
245
|
-
if (!result.ok) {
|
|
246
|
-
expect(result.error).toContain("Twilio account suspended");
|
|
247
|
-
}
|
|
248
|
-
expect(result.guardianReplyText.length).toBeGreaterThan(0);
|
|
249
|
-
|
|
250
|
-
// Verify follow-up state is failed
|
|
251
|
-
const updated = getGuardianActionRequest(request.id);
|
|
252
|
-
expect(updated!.followupState).toBe("failed");
|
|
253
|
-
expect(updated!.followupCompletedAt).toBeGreaterThan(0);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ── Error handling ──────────────────────────────────────────────────
|
|
258
|
-
|
|
259
|
-
describe("error handling", () => {
|
|
260
|
-
test("nonexistent request returns failure with error message", async () => {
|
|
261
|
-
const result = await executeFollowupAction("nonexistent-id", "call_back");
|
|
262
|
-
|
|
263
|
-
expect(result.ok).toBe(false);
|
|
264
|
-
if (!result.ok) {
|
|
265
|
-
expect(result.error).toContain("not found");
|
|
266
|
-
}
|
|
267
|
-
expect(result.guardianReplyText.length).toBeGreaterThan(0);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("request not in dispatching state returns failure", async () => {
|
|
271
|
-
// Create a request in awaiting_guardian_choice (not dispatching)
|
|
272
|
-
ensureConversation("exec-wrong-state");
|
|
273
|
-
const session = createCallSession({
|
|
274
|
-
conversationId: "exec-wrong-state",
|
|
275
|
-
provider: "twilio",
|
|
276
|
-
fromNumber: "+15550001111",
|
|
277
|
-
toNumber: "+15550002222",
|
|
278
|
-
});
|
|
279
|
-
const pq = createPendingQuestion(session.id, "Question?");
|
|
280
|
-
const request = createGuardianActionRequest({
|
|
281
|
-
kind: "ask_guardian",
|
|
282
|
-
sourceChannel: "phone",
|
|
283
|
-
sourceConversationId: "exec-wrong-state",
|
|
284
|
-
callSessionId: session.id,
|
|
285
|
-
pendingQuestionId: pq.id,
|
|
286
|
-
questionText: pq.questionText,
|
|
287
|
-
expiresAt: Date.now() - 10_000,
|
|
288
|
-
});
|
|
289
|
-
markTimedOutWithReason(request.id, "call_timeout");
|
|
290
|
-
startFollowupFromExpiredRequest(request.id, "Answer");
|
|
291
|
-
// Still in awaiting_guardian_choice — do NOT progress to dispatching
|
|
292
|
-
|
|
293
|
-
const result = await executeFollowupAction(request.id, "call_back");
|
|
294
|
-
|
|
295
|
-
expect(result.ok).toBe(false);
|
|
296
|
-
if (!result.ok) {
|
|
297
|
-
expect(result.error).toContain("Invalid followup state");
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test("follow-up states terminate correctly on success", async () => {
|
|
302
|
-
const { request } = createDispatchingRequest("exec-state-1", "call_back");
|
|
303
|
-
|
|
304
|
-
await executeFollowupAction(request.id, "call_back");
|
|
305
|
-
|
|
306
|
-
const updated = getGuardianActionRequest(request.id);
|
|
307
|
-
expect(updated!.followupState).toBe("completed");
|
|
308
|
-
expect(updated!.followupCompletedAt).not.toBeNull();
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("follow-up states terminate correctly on failure", async () => {
|
|
312
|
-
mockStartCallResult = { ok: false, error: "Provider error" };
|
|
313
|
-
const { request } = createDispatchingRequest("exec-state-2", "call_back");
|
|
314
|
-
|
|
315
|
-
await executeFollowupAction(request.id, "call_back");
|
|
316
|
-
|
|
317
|
-
const updated = getGuardianActionRequest(request.id);
|
|
318
|
-
expect(updated!.followupState).toBe("failed");
|
|
319
|
-
expect(updated!.followupCompletedAt).not.toBeNull();
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
});
|