@vellumai/assistant 0.10.3 → 0.10.4-staging.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/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold-cache guardian voice/phone regression.
|
|
3
|
+
*
|
|
4
|
+
* The sync trust resolver (`resolveActorTrust`) reads the IO-free guardian-
|
|
5
|
+
* delivery cache snapshot (`peekCachedGuardianDelivery`) keyed per channel
|
|
6
|
+
* filter. On a cold process only `vellum` is warmed at daemon startup, so for
|
|
7
|
+
* `phone` the snapshot is empty until some read warms that exact channel key.
|
|
8
|
+
* The voice setup path therefore awaits
|
|
9
|
+
* `getGuardianDeliveryFresh({ channelTypes: ["phone"] })` BEFORE the sync
|
|
10
|
+
* resolve so an inbound guardian call classifies as `guardian` rather than
|
|
11
|
+
* misclassifying during a gateway verdict blip. It reads FRESH because gateway-
|
|
12
|
+
* side binding writes don't invalidate the daemon cache: a stale empty snapshot
|
|
13
|
+
* from an earlier setup would otherwise survive the TTL.
|
|
14
|
+
*
|
|
15
|
+
* This test drives the REAL guardian-delivery reader cache (mocking only the
|
|
16
|
+
* gateway `ipcCall`) so the cold→warm transition is exercised end to end.
|
|
17
|
+
*/
|
|
18
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
const GUARDIAN_PHONE = "+15550100";
|
|
21
|
+
|
|
22
|
+
// Gateway IPC stub: returns the phone guardian delivery. The real reader caches
|
|
23
|
+
// the result under the `phone` key, so a subsequent sync `peek` finds it. When
|
|
24
|
+
// `guardianBound` is false the gateway reports no guardian — simulating the
|
|
25
|
+
// pre-binding state whose empty result the cache would otherwise pin.
|
|
26
|
+
let guardianBound = true;
|
|
27
|
+
let ipcCalls: Array<{ route: string; input: unknown }> = [];
|
|
28
|
+
mock.module("../ipc/gateway-client.js", () => ({
|
|
29
|
+
ipcCall: async (route: string, input: unknown) => {
|
|
30
|
+
ipcCalls.push({ route, input });
|
|
31
|
+
return {
|
|
32
|
+
guardians: guardianBound
|
|
33
|
+
? [
|
|
34
|
+
{
|
|
35
|
+
channelType: "phone",
|
|
36
|
+
contactId: "guardian-contact",
|
|
37
|
+
principalId: "P_GUARDIAN_COLD",
|
|
38
|
+
address: GUARDIAN_PHONE,
|
|
39
|
+
externalChatId: null,
|
|
40
|
+
status: "active",
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
: [],
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Member lookup is irrelevant to guardian classification (address match on the
|
|
49
|
+
// cached delivery decides it); return null so the member path is a no-op.
|
|
50
|
+
mock.module("../contacts/contact-store.js", () => ({
|
|
51
|
+
findContactByAddress: () => null,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
import {
|
|
55
|
+
__resetGuardianDeliveryCacheForTest,
|
|
56
|
+
getGuardianDelivery,
|
|
57
|
+
getGuardianDeliveryFresh,
|
|
58
|
+
peekCachedGuardianDelivery,
|
|
59
|
+
} from "../contacts/guardian-delivery-reader.js";
|
|
60
|
+
import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
|
|
61
|
+
|
|
62
|
+
describe("voice path warms the phone guardian cache before sync trust", () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
__resetGuardianDeliveryCacheForTest();
|
|
65
|
+
ipcCalls = [];
|
|
66
|
+
guardianBound = true;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("cold phone cache: guardian call misclassifies until upstream warm", async () => {
|
|
70
|
+
// Precondition: cold cache for phone — the sync peek would miss.
|
|
71
|
+
expect(
|
|
72
|
+
peekCachedGuardianDelivery({ channelTypes: ["phone"] }),
|
|
73
|
+
).toBeUndefined();
|
|
74
|
+
|
|
75
|
+
// Sync resolve on a cold cache: no guardian snapshot → classified unknown.
|
|
76
|
+
const cold = resolveActorTrust({
|
|
77
|
+
assistantId: "asst-1",
|
|
78
|
+
sourceChannel: "phone",
|
|
79
|
+
conversationExternalId: GUARDIAN_PHONE,
|
|
80
|
+
actorExternalId: GUARDIAN_PHONE,
|
|
81
|
+
});
|
|
82
|
+
expect(cold.trustClass).toBe("unknown");
|
|
83
|
+
|
|
84
|
+
// The voice setup path warms the phone-specific key via the fresh reader.
|
|
85
|
+
await getGuardianDeliveryFresh({ channelTypes: ["phone"] });
|
|
86
|
+
expect(
|
|
87
|
+
ipcCalls.some(
|
|
88
|
+
(c) =>
|
|
89
|
+
c.route === "resolve_guardian_delivery" &&
|
|
90
|
+
JSON.stringify(c.input) ===
|
|
91
|
+
JSON.stringify({ channelTypes: ["phone"] }),
|
|
92
|
+
),
|
|
93
|
+
).toBe(true);
|
|
94
|
+
|
|
95
|
+
// The sync resolve, reading the now-warm snapshot, classifies the caller as
|
|
96
|
+
// the guardian — not misclassified as `unknown`.
|
|
97
|
+
const warm = resolveActorTrust({
|
|
98
|
+
assistantId: "asst-1",
|
|
99
|
+
sourceChannel: "phone",
|
|
100
|
+
conversationExternalId: GUARDIAN_PHONE,
|
|
101
|
+
actorExternalId: GUARDIAN_PHONE,
|
|
102
|
+
});
|
|
103
|
+
expect(warm.trustClass).toBe("guardian");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("fresh warm bypasses a stale empty phone snapshot after a gateway-side binding", async () => {
|
|
107
|
+
// An earlier setup cached an empty phone snapshot (no guardian yet). Gateway-
|
|
108
|
+
// side binding writes don't invalidate the daemon cache, so the entry stays
|
|
109
|
+
// warm-but-stale until the TTL.
|
|
110
|
+
guardianBound = false;
|
|
111
|
+
await getGuardianDelivery({ channelTypes: ["phone"] });
|
|
112
|
+
expect(peekCachedGuardianDelivery({ channelTypes: ["phone"] })).toEqual([]);
|
|
113
|
+
|
|
114
|
+
// Guardian binding now exists gateway-side. A non-force read would still
|
|
115
|
+
// return the pinned empty snapshot; the fresh read bypasses it.
|
|
116
|
+
guardianBound = true;
|
|
117
|
+
expect(await getGuardianDelivery({ channelTypes: ["phone"] })).toEqual([]);
|
|
118
|
+
await getGuardianDeliveryFresh({ channelTypes: ["phone"] });
|
|
119
|
+
|
|
120
|
+
// The sync resolve now reads the refreshed snapshot and classifies guardian.
|
|
121
|
+
const warm = resolveActorTrust({
|
|
122
|
+
assistantId: "asst-1",
|
|
123
|
+
sourceChannel: "phone",
|
|
124
|
+
conversationExternalId: GUARDIAN_PHONE,
|
|
125
|
+
actorExternalId: GUARDIAN_PHONE,
|
|
126
|
+
});
|
|
127
|
+
expect(warm.trustClass).toBe("guardian");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("startup vellum-only warm leaves the phone key cold", async () => {
|
|
131
|
+
// Daemon startup warms only `vellum`; that must not populate the `phone` key.
|
|
132
|
+
await getGuardianDelivery({ channelTypes: ["vellum"] });
|
|
133
|
+
expect(
|
|
134
|
+
peekCachedGuardianDelivery({ channelTypes: ["phone"] }),
|
|
135
|
+
).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -14,6 +14,12 @@ const gatewayIpc = {
|
|
|
14
14
|
mirrored: boolean;
|
|
15
15
|
},
|
|
16
16
|
claimThrows: false,
|
|
17
|
+
// When set, contacts_get_rich throws (gateway read unreachable) so the
|
|
18
|
+
// gate-status fallback must fail open.
|
|
19
|
+
richThrows: false,
|
|
20
|
+
// When set, overrides the contacts_get_rich response (e.g. a gateway row
|
|
21
|
+
// under a divergent UUID for the same (type,address)).
|
|
22
|
+
richOverride: null as ((contactId: string | undefined) => unknown) | null,
|
|
17
23
|
// Drives the upsert_verified_channel relay verdict; false refuses the actor.
|
|
18
24
|
activationVerified: true,
|
|
19
25
|
calls: [] as { method: string; params?: Record<string, unknown> }[],
|
|
@@ -25,6 +31,13 @@ mock.module("../ipc/gateway-client.js", () => ({
|
|
|
25
31
|
params?: Record<string, unknown>,
|
|
26
32
|
) => {
|
|
27
33
|
gatewayIpc.calls.push({ method, params });
|
|
34
|
+
if (method === "contacts_get_rich") {
|
|
35
|
+
if (gatewayIpc.richThrows) throw new Error("gateway read unreachable");
|
|
36
|
+
if (gatewayIpc.richOverride) {
|
|
37
|
+
return gatewayIpc.richOverride(params?.contactId as string);
|
|
38
|
+
}
|
|
39
|
+
return richContactForId(params?.contactId as string);
|
|
40
|
+
}
|
|
28
41
|
if (method === "record_invite_redemption") {
|
|
29
42
|
if (gatewayIpc.claimThrows) throw new Error("gateway unreachable");
|
|
30
43
|
return gatewayIpc.claim;
|
|
@@ -39,9 +52,90 @@ mock.module("../ipc/gateway-client.js", () => ({
|
|
|
39
52
|
},
|
|
40
53
|
}));
|
|
41
54
|
|
|
55
|
+
// Serves contacts_get_rich (the gateway ACL read backing the gate-status
|
|
56
|
+
// fallback) from the seeded local contact, so gate resolution sources status
|
|
57
|
+
// from the gateway path rather than the local channel column.
|
|
58
|
+
function richContactForId(contactId: string | undefined) {
|
|
59
|
+
if (!contactId) return undefined;
|
|
60
|
+
const contact = getContact(contactId);
|
|
61
|
+
if (!contact) return undefined;
|
|
62
|
+
// ACL columns live on the still-present DB rows, not the slimmed interfaces;
|
|
63
|
+
// read them raw to build the gateway-rich response the production read parses.
|
|
64
|
+
const contactRole = (
|
|
65
|
+
getSqlite()
|
|
66
|
+
.query("SELECT role FROM contacts WHERE id = ?")
|
|
67
|
+
.get(contact.id) as { role: string } | undefined
|
|
68
|
+
)?.role;
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
contact: {
|
|
72
|
+
id: contact.id,
|
|
73
|
+
displayName: contact.displayName,
|
|
74
|
+
role: contactRole ?? "contact",
|
|
75
|
+
interactionCount: contact.interactionCount,
|
|
76
|
+
createdAt: contact.createdAt,
|
|
77
|
+
updatedAt: contact.updatedAt,
|
|
78
|
+
channels: contact.channels.map((c) => {
|
|
79
|
+
const acl = rawChannelAcl(c.id);
|
|
80
|
+
return {
|
|
81
|
+
id: c.id,
|
|
82
|
+
contactId: c.contactId,
|
|
83
|
+
type: c.type,
|
|
84
|
+
address: c.address,
|
|
85
|
+
isPrimary: c.isPrimary,
|
|
86
|
+
externalUserId: c.externalChatId,
|
|
87
|
+
status: acl.status,
|
|
88
|
+
policy: acl.policy,
|
|
89
|
+
verifiedAt: acl.verifiedAt,
|
|
90
|
+
verifiedVia: acl.verifiedVia,
|
|
91
|
+
lastSeenAt: acl.lastSeenAt,
|
|
92
|
+
interactionCount: acl.interactionCount,
|
|
93
|
+
lastInteraction: acl.lastInteraction,
|
|
94
|
+
revokedReason: acl.revokedReason,
|
|
95
|
+
blockedReason: acl.blockedReason,
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Read a channel's ACL columns straight off the still-present DB row. */
|
|
103
|
+
function rawChannelAcl(channelId: string) {
|
|
104
|
+
return getSqlite()
|
|
105
|
+
.query(
|
|
106
|
+
`SELECT status, policy, verified_at AS verifiedAt, verified_via AS verifiedVia,
|
|
107
|
+
last_seen_at AS lastSeenAt, interaction_count AS interactionCount,
|
|
108
|
+
last_interaction AS lastInteraction, revoked_reason AS revokedReason,
|
|
109
|
+
blocked_reason AS blockedReason
|
|
110
|
+
FROM contact_channels WHERE id = ?`,
|
|
111
|
+
)
|
|
112
|
+
.get(channelId) as {
|
|
113
|
+
status: string;
|
|
114
|
+
policy: string;
|
|
115
|
+
verifiedAt: number | null;
|
|
116
|
+
verifiedVia: string | null;
|
|
117
|
+
lastSeenAt: number | null;
|
|
118
|
+
interactionCount: number;
|
|
119
|
+
lastInteraction: number | null;
|
|
120
|
+
revokedReason: string | null;
|
|
121
|
+
blockedReason: string | null;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Read a contact's local role column (dropped from the Contact interface). */
|
|
126
|
+
function localContactRole(contactId: string): string | undefined {
|
|
127
|
+
return (
|
|
128
|
+
getSqlite()
|
|
129
|
+
.query("SELECT role FROM contacts WHERE id = ?")
|
|
130
|
+
.get(contactId) as { role: string } | undefined
|
|
131
|
+
)?.role;
|
|
132
|
+
}
|
|
133
|
+
|
|
42
134
|
function resetGatewayIpc() {
|
|
43
135
|
gatewayIpc.claim = { ok: true, updated: true, mirrored: true };
|
|
44
136
|
gatewayIpc.claimThrows = false;
|
|
137
|
+
gatewayIpc.richThrows = false;
|
|
138
|
+
gatewayIpc.richOverride = null;
|
|
45
139
|
gatewayIpc.activationVerified = true;
|
|
46
140
|
gatewayIpc.calls = [];
|
|
47
141
|
}
|
|
@@ -51,12 +145,12 @@ import {
|
|
|
51
145
|
getContact,
|
|
52
146
|
upsertContact,
|
|
53
147
|
} from "../contacts/contact-store.js";
|
|
54
|
-
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
55
148
|
import { getSqlite } from "../memory/db-connection.js";
|
|
56
149
|
import { initializeDb } from "../memory/db-init.js";
|
|
57
150
|
import { createInvite, revokeInvite } from "../memory/invite-store.js";
|
|
58
151
|
import { redeemVoiceInviteCode } from "../runtime/invite-redemption-service.js";
|
|
59
152
|
import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
|
|
153
|
+
import { seedContactChannel } from "./helpers/seed-contact-channel.js";
|
|
60
154
|
|
|
61
155
|
await initializeDb();
|
|
62
156
|
|
|
@@ -292,6 +386,83 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
292
386
|
).not.toBeNull();
|
|
293
387
|
});
|
|
294
388
|
|
|
389
|
+
test("matches an active member by (type,address) when the gateway row has a divergent uuid", async () => {
|
|
390
|
+
const phone = "+15551234567";
|
|
391
|
+
const member = seedContactChannel({
|
|
392
|
+
sourceChannel: "phone",
|
|
393
|
+
externalUserId: phone,
|
|
394
|
+
status: "active",
|
|
395
|
+
});
|
|
396
|
+
const { code } = createVoiceInvite({
|
|
397
|
+
callerPhone: phone,
|
|
398
|
+
contactId: member.contactId,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Gateway row for the same (type,address) under a DIFFERENT id.
|
|
402
|
+
gatewayIpc.richOverride = () => ({
|
|
403
|
+
ok: true,
|
|
404
|
+
contact: {
|
|
405
|
+
id: member.contactId,
|
|
406
|
+
displayName: phone,
|
|
407
|
+
role: "contact",
|
|
408
|
+
interactionCount: 0,
|
|
409
|
+
createdAt: 1,
|
|
410
|
+
updatedAt: 1,
|
|
411
|
+
channels: [
|
|
412
|
+
{
|
|
413
|
+
id: "gateway-divergent-uuid",
|
|
414
|
+
contactId: member.contactId,
|
|
415
|
+
type: "phone",
|
|
416
|
+
address: phone,
|
|
417
|
+
isPrimary: false,
|
|
418
|
+
externalUserId: null,
|
|
419
|
+
status: "active",
|
|
420
|
+
policy: "allow",
|
|
421
|
+
verifiedAt: 1,
|
|
422
|
+
verifiedVia: "invite",
|
|
423
|
+
lastSeenAt: null,
|
|
424
|
+
interactionCount: 0,
|
|
425
|
+
lastInteraction: null,
|
|
426
|
+
revokedReason: null,
|
|
427
|
+
blockedReason: null,
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const result = await redeemVoiceInviteCode({
|
|
434
|
+
callerExternalUserId: phone,
|
|
435
|
+
sourceChannel: "phone",
|
|
436
|
+
code,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(result.ok).toBe(true);
|
|
440
|
+
expect((result as { type: string }).type).toBe("already_member");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("fails open (no throw) when the gateway gate-status read is unreachable", async () => {
|
|
444
|
+
const phone = "+15551234567";
|
|
445
|
+
const member = seedContactChannel({
|
|
446
|
+
sourceChannel: "phone",
|
|
447
|
+
externalUserId: phone,
|
|
448
|
+
status: "revoked",
|
|
449
|
+
});
|
|
450
|
+
const { code } = createVoiceInvite({
|
|
451
|
+
callerPhone: phone,
|
|
452
|
+
contactId: member.contactId,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
gatewayIpc.richThrows = true;
|
|
456
|
+
|
|
457
|
+
const result = await redeemVoiceInviteCode({
|
|
458
|
+
callerExternalUserId: phone,
|
|
459
|
+
sourceChannel: "phone",
|
|
460
|
+
code,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(result.ok).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
295
466
|
test("marks channel as verified via invite on voice redemption", async () => {
|
|
296
467
|
const phone = "+15551234567";
|
|
297
468
|
const { code } = createVoiceInvite({ callerPhone: phone });
|
|
@@ -304,15 +475,12 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
304
475
|
|
|
305
476
|
expect(result.ok).toBe(true);
|
|
306
477
|
|
|
478
|
+
// The gateway owns the verified ACL verdict; the local mirror is identity-only.
|
|
307
479
|
const channelResult = findContactChannel({
|
|
308
480
|
channelType: "phone",
|
|
309
481
|
address: phone,
|
|
310
482
|
});
|
|
311
|
-
|
|
312
483
|
expect(channelResult).not.toBeNull();
|
|
313
|
-
expect(channelResult!.channel.verifiedAt).toBeGreaterThan(0);
|
|
314
|
-
expect(channelResult!.channel.verifiedVia).toBe("invite");
|
|
315
|
-
expect(channelResult!.channel.status).toBe("active");
|
|
316
484
|
});
|
|
317
485
|
|
|
318
486
|
test("wrong caller identity fails with generic error", async () => {
|
|
@@ -420,7 +588,7 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
420
588
|
const phone = "+15551234567";
|
|
421
589
|
|
|
422
590
|
// Pre-create an active member for this phone on voice channel
|
|
423
|
-
const member =
|
|
591
|
+
const member = seedContactChannel({
|
|
424
592
|
sourceChannel: "phone",
|
|
425
593
|
externalUserId: phone,
|
|
426
594
|
status: "active",
|
|
@@ -430,7 +598,7 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
430
598
|
// Create a voice invite targeting the same contact that owns the channel
|
|
431
599
|
const { code } = createVoiceInvite({
|
|
432
600
|
callerPhone: phone,
|
|
433
|
-
contactId: member
|
|
601
|
+
contactId: member.contactId,
|
|
434
602
|
});
|
|
435
603
|
|
|
436
604
|
const result = await redeemVoiceInviteCode({
|
|
@@ -456,7 +624,7 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
456
624
|
const phone = "+15551234567";
|
|
457
625
|
|
|
458
626
|
// Pre-create a blocked member and find their contact
|
|
459
|
-
const member =
|
|
627
|
+
const member = seedContactChannel({
|
|
460
628
|
sourceChannel: "phone",
|
|
461
629
|
externalUserId: phone,
|
|
462
630
|
status: "blocked",
|
|
@@ -466,7 +634,7 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
466
634
|
// Create a voice invite targeting the same contact that owns the channel
|
|
467
635
|
const { code } = createVoiceInvite({
|
|
468
636
|
callerPhone: phone,
|
|
469
|
-
contactId: member
|
|
637
|
+
contactId: member.contactId,
|
|
470
638
|
});
|
|
471
639
|
|
|
472
640
|
const result = await redeemVoiceInviteCode({
|
|
@@ -492,16 +660,12 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
492
660
|
const phone = "+15559998888";
|
|
493
661
|
|
|
494
662
|
// Pre-create a guardian contact with a revoked phone channel
|
|
495
|
-
const
|
|
663
|
+
const guardianSeed = seedContactChannel({
|
|
664
|
+
sourceChannel: "phone",
|
|
665
|
+
externalUserId: phone,
|
|
496
666
|
displayName: "Guardian",
|
|
497
667
|
role: "guardian",
|
|
498
|
-
|
|
499
|
-
{
|
|
500
|
-
type: "phone",
|
|
501
|
-
address: phone,
|
|
502
|
-
status: "revoked",
|
|
503
|
-
},
|
|
504
|
-
],
|
|
668
|
+
status: "revoked",
|
|
505
669
|
});
|
|
506
670
|
|
|
507
671
|
// Create a separate target contact "Mom"
|
|
@@ -533,11 +697,10 @@ describe("redeemVoiceInviteCode", () => {
|
|
|
533
697
|
});
|
|
534
698
|
expect(contactResult).not.toBeNull();
|
|
535
699
|
expect(contactResult!.contact.id).toBe(momContact.id);
|
|
536
|
-
expect(contactResult!.channel.status).toBe("active");
|
|
537
700
|
|
|
538
701
|
// Verify the original guardian contact was NOT modified
|
|
539
|
-
const guardian = getContact(
|
|
702
|
+
const guardian = getContact(guardianSeed.contactId);
|
|
540
703
|
expect(guardian).not.toBeNull();
|
|
541
|
-
expect(
|
|
704
|
+
expect(localContactRole(guardianSeed.contactId)).toBe("guardian");
|
|
542
705
|
});
|
|
543
706
|
});
|
|
@@ -55,11 +55,11 @@ describe("102-preserve-heartbeat-enabled-for-existing-workspaces migration", ()
|
|
|
55
55
|
).toContain("heartbeat.enabled");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("schema default is
|
|
59
|
-
expect(HeartbeatConfigSchema.parse({}).enabled).toBe(
|
|
58
|
+
test("schema default is enabled", () => {
|
|
59
|
+
expect(HeartbeatConfigSchema.parse({}).enabled).toBe(true);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
test("skips fresh workspaces so new users
|
|
62
|
+
test("skips fresh workspaces so new users inherit the schema default", () => {
|
|
63
63
|
preserveHeartbeatEnabledForExistingWorkspacesMigration.run(
|
|
64
64
|
workspaceDir,
|
|
65
65
|
NEW_WORKSPACE_CTX,
|
|
@@ -110,8 +110,8 @@ afterEach(() => {
|
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
describe("111-prune-seeded-callsite-defaults migration", () => {
|
|
113
|
-
test("is registered
|
|
114
|
-
expect(WORKSPACE_MIGRATIONS
|
|
113
|
+
test("is registered", () => {
|
|
114
|
+
expect(WORKSPACE_MIGRATIONS).toContain(
|
|
115
115
|
pruneSeededCallsiteDefaultsMigration,
|
|
116
116
|
);
|
|
117
117
|
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { removeAdvisorCallsiteOverrideMigration } from "../workspace/migrations/112-remove-advisor-callsite-override.js";
|
|
13
|
+
|
|
14
|
+
let workspaceDir: string;
|
|
15
|
+
|
|
16
|
+
function writeConfig(data: Record<string, unknown>): void {
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(workspaceDir, "config.json"),
|
|
19
|
+
JSON.stringify(data, null, 2) + "\n",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readConfig(): Record<string, unknown> {
|
|
24
|
+
return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
workspaceDir = join(
|
|
29
|
+
tmpdir(),
|
|
30
|
+
`vellum-migration-111-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
31
|
+
);
|
|
32
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
if (existsSync(workspaceDir)) {
|
|
37
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("112-remove-advisor-callsite-override migration", () => {
|
|
42
|
+
test("has correct migration id and description", () => {
|
|
43
|
+
expect(removeAdvisorCallsiteOverrideMigration.id).toBe(
|
|
44
|
+
"112-remove-advisor-callsite-override",
|
|
45
|
+
);
|
|
46
|
+
expect(removeAdvisorCallsiteOverrideMigration.description).toBe(
|
|
47
|
+
"Remove the stale advisor entry from llm.callSites (advisor call site removed)",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ─── No-op cases ────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
test("no-op when config.json does not exist", () => {
|
|
54
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
55
|
+
expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("gracefully handles invalid JSON", () => {
|
|
59
|
+
writeFileSync(join(workspaceDir, "config.json"), "not-valid-json");
|
|
60
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
61
|
+
expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
|
|
62
|
+
"not-valid-json",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("no-op when config has no llm.callSites", () => {
|
|
67
|
+
const original = { llm: { default: { provider: "anthropic" } } };
|
|
68
|
+
writeConfig(original);
|
|
69
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
70
|
+
expect(readConfig()).toEqual(original);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("no-op when llm.callSites has no advisor key", () => {
|
|
74
|
+
const original = {
|
|
75
|
+
llm: {
|
|
76
|
+
callSites: {
|
|
77
|
+
mainAgent: { profile: "quality-optimized" },
|
|
78
|
+
memoryRouter: { profile: "latency-optimized" },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
writeConfig(original);
|
|
83
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
84
|
+
expect(readConfig()).toEqual(original);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("gracefully handles non-object llm / callSites shapes", () => {
|
|
88
|
+
const original = { llm: { callSites: 42 } };
|
|
89
|
+
writeConfig(original);
|
|
90
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
91
|
+
expect(readConfig()).toEqual(original);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── Removal ────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
test("removes advisor and prunes the now-empty callSites map", () => {
|
|
97
|
+
writeConfig({
|
|
98
|
+
llm: {
|
|
99
|
+
callSites: {
|
|
100
|
+
advisor: { profile: "quality-optimized" },
|
|
101
|
+
},
|
|
102
|
+
advisorProfile: "frontier",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
107
|
+
|
|
108
|
+
const llm = readConfig().llm as Record<string, unknown>;
|
|
109
|
+
expect("callSites" in llm).toBe(false);
|
|
110
|
+
// The unrelated top-level advisor profile selection is untouched.
|
|
111
|
+
expect(llm.advisorProfile).toBe("frontier");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("removes advisor but preserves other callSites keys", () => {
|
|
115
|
+
writeConfig({
|
|
116
|
+
llm: {
|
|
117
|
+
callSites: {
|
|
118
|
+
advisor: { profile: "quality-optimized" },
|
|
119
|
+
mainAgent: { profile: "opus" },
|
|
120
|
+
memoryRouter: { profile: "latency-optimized" },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
126
|
+
|
|
127
|
+
const callSites = (readConfig().llm as Record<string, unknown>)
|
|
128
|
+
.callSites as Record<string, unknown>;
|
|
129
|
+
expect("advisor" in callSites).toBe(false);
|
|
130
|
+
expect(callSites.mainAgent).toEqual({ profile: "opus" });
|
|
131
|
+
expect(callSites.memoryRouter).toEqual({ profile: "latency-optimized" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── Idempotency ────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
test("idempotency: re-running yields no further mutation", () => {
|
|
137
|
+
writeConfig({
|
|
138
|
+
llm: {
|
|
139
|
+
callSites: {
|
|
140
|
+
advisor: { profile: "quality-optimized" },
|
|
141
|
+
mainAgent: { profile: "opus" },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
147
|
+
const afterFirst = readConfig();
|
|
148
|
+
|
|
149
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
150
|
+
const afterSecond = readConfig();
|
|
151
|
+
|
|
152
|
+
expect(afterSecond).toEqual(afterFirst);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("idempotency: writes nothing on a config without the advisor key", () => {
|
|
156
|
+
writeConfig({
|
|
157
|
+
llm: { callSites: { mainAgent: { profile: "opus" } } },
|
|
158
|
+
});
|
|
159
|
+
const beforeContent = readFileSync(
|
|
160
|
+
join(workspaceDir, "config.json"),
|
|
161
|
+
"utf-8",
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
removeAdvisorCallsiteOverrideMigration.run(workspaceDir);
|
|
165
|
+
|
|
166
|
+
expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
|
|
167
|
+
beforeContent,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|