@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
|
@@ -31,16 +31,33 @@ import {
|
|
|
31
31
|
|
|
32
32
|
// ── Mock state ────────────────────────────────────────────────────
|
|
33
33
|
|
|
34
|
+
interface GuardianDeliveryStub {
|
|
35
|
+
channelType: string;
|
|
36
|
+
address: string;
|
|
37
|
+
status: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
let mockWorkspaceDir: string = "";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let mockAnyGuardian: {
|
|
40
|
-
contact: { userFile: string | null };
|
|
41
|
-
channels: Record<string, unknown>[];
|
|
42
|
-
} | null = null;
|
|
41
|
+
// Gateway guardian delivery cache, keyed by the same source the production
|
|
42
|
+
// peek reads; the guardian's userFile (local INFO) is joined separately via
|
|
43
|
+
// findContactByAddress on the delivery's address.
|
|
44
|
+
let mockGuardianDeliveries: GuardianDeliveryStub[] = [];
|
|
43
45
|
let mockContactsByAddress: Record<string, { userFile: string | null }> = {};
|
|
46
|
+
// Simulates a cold sync cache: the sync `peek` returns nothing until the async
|
|
47
|
+
// `getGuardianDelivery` warm runs and populates `mockGuardianDeliveries`. The
|
|
48
|
+
// pending list is what the warm reveals.
|
|
49
|
+
let pendingWarmDeliveries: GuardianDeliveryStub[] | null = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Seed a vellum guardian: a gateway delivery for the vellum channel plus the
|
|
53
|
+
* local contact (by address) carrying its userFile.
|
|
54
|
+
*/
|
|
55
|
+
function seedVellumGuardian(userFile: string | null): void {
|
|
56
|
+
mockGuardianDeliveries = [
|
|
57
|
+
{ channelType: "vellum", address: "vellum:self", status: "active" },
|
|
58
|
+
];
|
|
59
|
+
mockContactsByAddress["vellum:vellum:self"] = { userFile };
|
|
60
|
+
}
|
|
44
61
|
|
|
45
62
|
// ── Mock modules (must precede imports from the module under test) ──
|
|
46
63
|
|
|
@@ -51,13 +68,32 @@ mock.module("../util/platform.js", () => ({
|
|
|
51
68
|
mock.module("../contacts/contact-store.js", () => ({
|
|
52
69
|
findContactByAddress: (channelType: string, address: string) =>
|
|
53
70
|
mockContactsByAddress[`${channelType}:${address}`] ?? null,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
mock.module("../contacts/guardian-delivery-reader.js", () => ({
|
|
74
|
+
peekCachedGuardianDelivery: (input?: { channelTypes?: string[] }) => {
|
|
75
|
+
if (!input?.channelTypes) return mockGuardianDeliveries;
|
|
76
|
+
return mockGuardianDeliveries.filter((g) =>
|
|
77
|
+
input.channelTypes!.includes(g.channelType),
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
// Warming the cache: reveals the pending guardian to the sync peek above,
|
|
81
|
+
// mirroring how the production single-flight read populates the cache key.
|
|
82
|
+
getGuardianDelivery: async (_input?: { channelTypes?: string[] }) => {
|
|
83
|
+
if (pendingWarmDeliveries) {
|
|
84
|
+
mockGuardianDeliveries = pendingWarmDeliveries;
|
|
85
|
+
pendingWarmDeliveries = null;
|
|
86
|
+
}
|
|
87
|
+
return mockGuardianDeliveries;
|
|
88
|
+
},
|
|
89
|
+
guardianForChannel: (list: GuardianDeliveryStub[], channelType: string) =>
|
|
90
|
+
list.find((g) => g.channelType === channelType && g.status === "active"),
|
|
91
|
+
anyGuardian: (list: GuardianDeliveryStub[]) => list[0],
|
|
57
92
|
}));
|
|
58
93
|
|
|
59
94
|
// Import AFTER mocks so the module under test binds to the stubbed
|
|
60
95
|
// implementations.
|
|
96
|
+
import { getGuardianDelivery } from "../contacts/guardian-delivery-reader.js";
|
|
61
97
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
62
98
|
import {
|
|
63
99
|
ensureGuardianPersonaFile,
|
|
@@ -83,9 +119,9 @@ afterAll(() => {
|
|
|
83
119
|
beforeEach(() => {
|
|
84
120
|
// Fresh workspace per test, so filesystem state doesn't leak.
|
|
85
121
|
mockWorkspaceDir = mkdtempSync(join(testRoot, "ws-"));
|
|
86
|
-
|
|
87
|
-
mockAnyGuardian = null;
|
|
122
|
+
mockGuardianDeliveries = [];
|
|
88
123
|
mockContactsByAddress = {};
|
|
124
|
+
pendingWarmDeliveries = null;
|
|
89
125
|
});
|
|
90
126
|
|
|
91
127
|
afterEach(() => {
|
|
@@ -96,21 +132,35 @@ afterEach(() => {
|
|
|
96
132
|
|
|
97
133
|
describe("resolveGuardianPersonaPath", () => {
|
|
98
134
|
test("returns null when no guardian exists", () => {
|
|
99
|
-
mockVellumGuardian = null;
|
|
100
|
-
mockAnyGuardian = null;
|
|
101
135
|
|
|
102
136
|
expect(resolveGuardianPersonaPath()).toBeNull();
|
|
103
137
|
});
|
|
104
138
|
|
|
105
139
|
test("returns absolute path when guardian has userFile set", () => {
|
|
106
|
-
|
|
107
|
-
contact: { userFile: "alice.md" },
|
|
108
|
-
channel: {},
|
|
109
|
-
};
|
|
140
|
+
seedVellumGuardian("alice.md");
|
|
110
141
|
|
|
111
142
|
const result = resolveGuardianPersonaPath();
|
|
112
143
|
expect(result).toBe(join(mockWorkspaceDir, "users", "alice.md"));
|
|
113
144
|
});
|
|
145
|
+
|
|
146
|
+
test("falls back to default (null path) on a cold cache, but resolves the guardian after a warm", async () => {
|
|
147
|
+
// Cold start: the guardian binding exists upstream but the sync cache is
|
|
148
|
+
// empty, so a bare sync resolution misses it and falls back to default.
|
|
149
|
+
pendingWarmDeliveries = [
|
|
150
|
+
{ channelType: "vellum", address: "vellum:self", status: "active" },
|
|
151
|
+
];
|
|
152
|
+
mockContactsByAddress["vellum:vellum:self"] = { userFile: "alice.md" };
|
|
153
|
+
|
|
154
|
+
expect(resolveGuardianPersonaPath()).toBeNull();
|
|
155
|
+
|
|
156
|
+
// Async callers warm the vellum guardian-delivery cache before the sync
|
|
157
|
+
// resolution; afterwards the guardian slug resolves instead of default.md.
|
|
158
|
+
await getGuardianDelivery({ channelTypes: ["vellum"] });
|
|
159
|
+
|
|
160
|
+
expect(resolveGuardianPersonaPath()).toBe(
|
|
161
|
+
join(mockWorkspaceDir, "users", "alice.md"),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
114
164
|
});
|
|
115
165
|
|
|
116
166
|
// ── ensureGuardianPersonaFile ──────────────────────────────────────
|
|
@@ -156,17 +206,12 @@ describe("ensureGuardianPersonaFile", () => {
|
|
|
156
206
|
|
|
157
207
|
describe("resolveGuardianPersonaStrict", () => {
|
|
158
208
|
test("returns null when no guardian contact exists", () => {
|
|
159
|
-
mockVellumGuardian = null;
|
|
160
|
-
mockAnyGuardian = null;
|
|
161
209
|
|
|
162
210
|
expect(resolveGuardianPersonaStrict()).toBeNull();
|
|
163
211
|
});
|
|
164
212
|
|
|
165
213
|
test("returns null when the guardian's own file is missing, even if default.md exists", () => {
|
|
166
|
-
|
|
167
|
-
contact: { userFile: "alice.md" },
|
|
168
|
-
channel: {},
|
|
169
|
-
};
|
|
214
|
+
seedVellumGuardian("alice.md");
|
|
170
215
|
|
|
171
216
|
// default.md is populated but alice.md is not on disk.
|
|
172
217
|
const usersDir = join(mockWorkspaceDir, "users");
|
|
@@ -185,10 +230,7 @@ describe("resolveGuardianPersonaStrict", () => {
|
|
|
185
230
|
});
|
|
186
231
|
|
|
187
232
|
test("returns guardian file content when present", () => {
|
|
188
|
-
|
|
189
|
-
contact: { userFile: "alice.md" },
|
|
190
|
-
channel: {},
|
|
191
|
-
};
|
|
233
|
+
seedVellumGuardian("alice.md");
|
|
192
234
|
|
|
193
235
|
const usersDir = join(mockWorkspaceDir, "users");
|
|
194
236
|
mkdirSync(usersDir, { recursive: true });
|
|
@@ -260,10 +302,7 @@ describe("isGuardianPersonaCustomized", () => {
|
|
|
260
302
|
|
|
261
303
|
describe("resolveUserSlug (guardian trust, no requester identity)", () => {
|
|
262
304
|
test("guardian trust context without requesterExternalUserId resolves the guardian user file", () => {
|
|
263
|
-
|
|
264
|
-
contact: { userFile: "alice.md" },
|
|
265
|
-
channel: {},
|
|
266
|
-
};
|
|
305
|
+
seedVellumGuardian("alice.md");
|
|
267
306
|
|
|
268
307
|
const trustContext = {
|
|
269
308
|
sourceChannel: "vellum",
|
|
@@ -274,10 +313,7 @@ describe("resolveUserSlug (guardian trust, no requester identity)", () => {
|
|
|
274
313
|
});
|
|
275
314
|
|
|
276
315
|
test("non-guardian trust context without requesterExternalUserId does not borrow the guardian persona", () => {
|
|
277
|
-
|
|
278
|
-
contact: { userFile: "alice.md" },
|
|
279
|
-
channel: {},
|
|
280
|
-
};
|
|
316
|
+
seedVellumGuardian("alice.md");
|
|
281
317
|
|
|
282
318
|
const trustContext = {
|
|
283
319
|
sourceChannel: "vellum",
|
|
@@ -291,10 +327,7 @@ describe("resolveUserSlug (guardian trust, no requester identity)", () => {
|
|
|
291
327
|
// The verdict-bound guardian is looked up by its address, not by the
|
|
292
328
|
// most-recently-verified channel guardian, so a different channel guardian
|
|
293
329
|
// does not shadow the verdict's binding.
|
|
294
|
-
|
|
295
|
-
contact: { userFile: "wrong-guardian.md" },
|
|
296
|
-
channel: {},
|
|
297
|
-
};
|
|
330
|
+
seedVellumGuardian("wrong-guardian.md");
|
|
298
331
|
mockContactsByAddress["telegram:guardian-tg"] = {
|
|
299
332
|
userFile: "alice.md",
|
|
300
333
|
};
|
|
@@ -309,10 +342,7 @@ describe("resolveUserSlug (guardian trust, no requester identity)", () => {
|
|
|
309
342
|
});
|
|
310
343
|
|
|
311
344
|
test("falls back to the channel guardian when the verdict carries no guardian identity", () => {
|
|
312
|
-
|
|
313
|
-
contact: { userFile: "alice.md" },
|
|
314
|
-
channel: {},
|
|
315
|
-
};
|
|
345
|
+
seedVellumGuardian("alice.md");
|
|
316
346
|
|
|
317
347
|
const trustContext = {
|
|
318
348
|
sourceChannel: "vellum",
|
|
@@ -475,11 +475,13 @@ describe("plugin bootstrap", () => {
|
|
|
475
475
|
//
|
|
476
476
|
// A plugin is disabled when a `.disabled` file exists at
|
|
477
477
|
// <workspace>/plugins/<manifest-name>/.disabled. The bootstrap must
|
|
478
|
-
// skip the plugin
|
|
479
|
-
//
|
|
480
|
-
//
|
|
478
|
+
// skip the plugin's init, tools, routes, and shutdown hook. Unlike the
|
|
479
|
+
// requiresFlag gate, the plugin is NOT removed from the registry — its
|
|
480
|
+
// hooks stay registered and are filtered at read time by
|
|
481
|
+
// `isPluginDisabled` in `getHooksFor`, so `assistant plugins enable`
|
|
482
|
+
// takes effect on the next turn without a restart.
|
|
481
483
|
|
|
482
|
-
test(".disabled sentinel: init does not fire and
|
|
484
|
+
test(".disabled sentinel: init does not fire and hooks are filtered at read time", async () => {
|
|
483
485
|
let initFired = false;
|
|
484
486
|
const plugin = buildPlugin("sentinel-off", {
|
|
485
487
|
async init() {
|
|
@@ -497,8 +499,14 @@ describe("plugin bootstrap", () => {
|
|
|
497
499
|
await bootstrapPlugins();
|
|
498
500
|
|
|
499
501
|
expect(initFired).toBe(false);
|
|
502
|
+
// The plugin stays in the registry (not unregistered) so its hooks can
|
|
503
|
+
// be re-enabled at runtime by removing the sentinel.
|
|
500
504
|
const names = getRegisteredPlugins().map((p) => p.manifest.name);
|
|
501
|
-
expect(names).
|
|
505
|
+
expect(names).toContain("sentinel-off");
|
|
506
|
+
// But its hooks are filtered out at read time by `isPluginDisabled`.
|
|
507
|
+
const { getHooksFor } = await import("../plugins/registry.js");
|
|
508
|
+
const hooks = await getHooksFor("init");
|
|
509
|
+
expect(hooks).toHaveLength(0);
|
|
502
510
|
|
|
503
511
|
await rm(sentinelDir, { recursive: true, force: true });
|
|
504
512
|
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for per-surface plugin disabled-state filtering.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `assistant plugins disable default-*` takes effect on the
|
|
5
|
+
* next turn without a daemon restart. The `.disabled` sentinel is checked at
|
|
6
|
+
* read time by each surface (`getHooksFor` for hooks,
|
|
7
|
+
* `getPluginToolDefinitions` for tools) rather than at boot, so toggling the
|
|
8
|
+
* sentinel file at runtime is immediately reflected.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
+
|
|
17
|
+
import { RiskLevel } from "../permissions/types.js";
|
|
18
|
+
import {
|
|
19
|
+
getHooksFor,
|
|
20
|
+
registerPlugin,
|
|
21
|
+
resetPluginRegistryForTests,
|
|
22
|
+
unregisterPlugin,
|
|
23
|
+
} from "../plugins/registry.js";
|
|
24
|
+
import { type HookFunction, type Plugin } from "../plugins/types.js";
|
|
25
|
+
import {
|
|
26
|
+
getPluginToolDefinitions,
|
|
27
|
+
registerPluginTools,
|
|
28
|
+
} from "../tools/registry.js";
|
|
29
|
+
import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
30
|
+
|
|
31
|
+
const TEST_WORKSPACE_DIR = join(
|
|
32
|
+
tmpdir(),
|
|
33
|
+
`vellum-plugin-disabled-state-test-${process.pid}-${Date.now()}`,
|
|
34
|
+
);
|
|
35
|
+
process.env.VELLUM_WORKSPACE_DIR = TEST_WORKSPACE_DIR;
|
|
36
|
+
|
|
37
|
+
async function createSentinel(name: string): Promise<void> {
|
|
38
|
+
const dir = join(TEST_WORKSPACE_DIR, "plugins", name);
|
|
39
|
+
await mkdir(dir, { recursive: true });
|
|
40
|
+
await writeFile(join(dir, ".disabled"), "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function removeSentinel(name: string): Promise<void> {
|
|
44
|
+
const dir = join(TEST_WORKSPACE_DIR, "plugins", name);
|
|
45
|
+
await rm(dir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildPlugin(
|
|
49
|
+
name: string,
|
|
50
|
+
hooks: Record<string, HookFunction> = {},
|
|
51
|
+
): Plugin {
|
|
52
|
+
return {
|
|
53
|
+
manifest: { name, version: "1.0.0" },
|
|
54
|
+
hooks,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeFakeTool(name: string): Tool {
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
description: `Fake ${name}`,
|
|
62
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
63
|
+
executionTarget: "sandbox",
|
|
64
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
65
|
+
category: "plugin",
|
|
66
|
+
async execute(
|
|
67
|
+
_input: Record<string, unknown>,
|
|
68
|
+
_context: ToolContext,
|
|
69
|
+
): Promise<ToolExecutionResult> {
|
|
70
|
+
return { content: "ok", isError: false };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
resetPluginRegistryForTests();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
// Clean up any sentinel files created during the test.
|
|
81
|
+
const pluginsDir = join(TEST_WORKSPACE_DIR, "plugins");
|
|
82
|
+
if (existsSync(pluginsDir)) {
|
|
83
|
+
await rm(pluginsDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("per-surface disabled-state filtering", () => {
|
|
88
|
+
test("getHooksFor filters out hooks from a disabled plugin", async () => {
|
|
89
|
+
const plugin = buildPlugin("default-test-hook", {
|
|
90
|
+
"user-prompt-submit": () => Promise.resolve(),
|
|
91
|
+
});
|
|
92
|
+
registerPlugin(plugin);
|
|
93
|
+
|
|
94
|
+
// Before disabling: hook is included.
|
|
95
|
+
const hooksBefore = await getHooksFor("user-prompt-submit");
|
|
96
|
+
expect(hooksBefore).toHaveLength(1);
|
|
97
|
+
|
|
98
|
+
// Disable via sentinel.
|
|
99
|
+
await createSentinel("default-test-hook");
|
|
100
|
+
|
|
101
|
+
// After disabling: hook is filtered out at read time.
|
|
102
|
+
const hooksAfter = await getHooksFor("user-prompt-submit");
|
|
103
|
+
expect(hooksAfter).toHaveLength(0);
|
|
104
|
+
|
|
105
|
+
// Clean up.
|
|
106
|
+
await removeSentinel("default-test-hook");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("getHooksFor re-includes hooks when a disabled plugin is re-enabled", async () => {
|
|
110
|
+
const plugin = buildPlugin("default-test-reenable", {
|
|
111
|
+
"user-prompt-submit": () => Promise.resolve(),
|
|
112
|
+
});
|
|
113
|
+
registerPlugin(plugin);
|
|
114
|
+
|
|
115
|
+
// Disable.
|
|
116
|
+
await createSentinel("default-test-reenable");
|
|
117
|
+
let hooks = await getHooksFor("user-prompt-submit");
|
|
118
|
+
expect(hooks).toHaveLength(0);
|
|
119
|
+
|
|
120
|
+
// Re-enable by removing the sentinel.
|
|
121
|
+
await removeSentinel("default-test-reenable");
|
|
122
|
+
hooks = await getHooksFor("user-prompt-submit");
|
|
123
|
+
expect(hooks).toHaveLength(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("getPluginToolDefinitions filters out tools from a disabled plugin", async () => {
|
|
127
|
+
const plugin: Plugin = {
|
|
128
|
+
manifest: { name: "default-test-tools", version: "1.0.0" },
|
|
129
|
+
tools: [makeFakeTool("test_tool")],
|
|
130
|
+
};
|
|
131
|
+
registerPlugin(plugin);
|
|
132
|
+
registerPluginTools("default-test-tools", plugin.tools!);
|
|
133
|
+
|
|
134
|
+
// Before disabling: tool is visible.
|
|
135
|
+
let defs = getPluginToolDefinitions();
|
|
136
|
+
expect(defs.some((d) => d.name === "test_tool")).toBe(true);
|
|
137
|
+
|
|
138
|
+
// Disable via sentinel.
|
|
139
|
+
await createSentinel("default-test-tools");
|
|
140
|
+
|
|
141
|
+
// After disabling: tool is filtered out.
|
|
142
|
+
defs = getPluginToolDefinitions();
|
|
143
|
+
expect(defs.some((d) => d.name === "test_tool")).toBe(false);
|
|
144
|
+
|
|
145
|
+
// Re-enable.
|
|
146
|
+
await removeSentinel("default-test-tools");
|
|
147
|
+
defs = getPluginToolDefinitions();
|
|
148
|
+
expect(defs.some((d) => d.name === "test_tool")).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("disabling one plugin does not affect others", async () => {
|
|
152
|
+
const pluginA = buildPlugin("default-test-alpha", {
|
|
153
|
+
"user-prompt-submit": () => Promise.resolve(),
|
|
154
|
+
});
|
|
155
|
+
const pluginB = buildPlugin("default-test-beta", {
|
|
156
|
+
"user-prompt-submit": () => Promise.resolve(),
|
|
157
|
+
});
|
|
158
|
+
registerPlugin(pluginA);
|
|
159
|
+
registerPlugin(pluginB);
|
|
160
|
+
|
|
161
|
+
// Both visible.
|
|
162
|
+
let hooks = await getHooksFor("user-prompt-submit");
|
|
163
|
+
expect(hooks).toHaveLength(2);
|
|
164
|
+
|
|
165
|
+
// Disable only alpha.
|
|
166
|
+
await createSentinel("default-test-alpha");
|
|
167
|
+
hooks = await getHooksFor("user-prompt-submit");
|
|
168
|
+
expect(hooks).toHaveLength(1);
|
|
169
|
+
|
|
170
|
+
// Re-enable alpha.
|
|
171
|
+
await removeSentinel("default-test-alpha");
|
|
172
|
+
hooks = await getHooksFor("user-prompt-submit");
|
|
173
|
+
expect(hooks).toHaveLength(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("unregisterPlugin removes hooks from the hook registry", async () => {
|
|
177
|
+
const plugin = buildPlugin("default-test-unreg", {
|
|
178
|
+
"user-prompt-submit": () => Promise.resolve(),
|
|
179
|
+
});
|
|
180
|
+
registerPlugin(plugin);
|
|
181
|
+
|
|
182
|
+
let hooks = await getHooksFor("user-prompt-submit");
|
|
183
|
+
expect(hooks).toHaveLength(1);
|
|
184
|
+
|
|
185
|
+
unregisterPlugin("default-test-unreg");
|
|
186
|
+
|
|
187
|
+
hooks = await getHooksFor("user-prompt-submit");
|
|
188
|
+
expect(hooks).toHaveLength(0);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -234,7 +234,7 @@ describe("native web-search capability survives the wrapper chain", () => {
|
|
|
234
234
|
// UsageTracking → leaf. The advisor consult reads the flag off the top.
|
|
235
235
|
const wrapped = new CallSiteConfiguredProvider(
|
|
236
236
|
new UsageTrackingProvider(leaf(true)),
|
|
237
|
-
"
|
|
237
|
+
"subagentSpawn",
|
|
238
238
|
);
|
|
239
239
|
expect(wrapped.supportsNativeWebSearch).toBe(true);
|
|
240
240
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold-cache guardian-reaction regression.
|
|
3
|
+
*
|
|
4
|
+
* The sync trust resolver reads the IO-free guardian-delivery cache snapshot
|
|
5
|
+
* (`peekCachedGuardianDelivery`). On a cold process only `vellum` is warmed at
|
|
6
|
+
* daemon startup, so for `slack` the snapshot is empty until some read warms
|
|
7
|
+
* that exact channel key. `handleSlackReactionIntercept` therefore awaits
|
|
8
|
+
* `getGuardianDelivery({ channelTypes: ["slack"] })` BEFORE the sync resolve so
|
|
9
|
+
* a guardian's approval reaction classifies as `guardian` rather than dropping
|
|
10
|
+
* as `unknown`.
|
|
11
|
+
*
|
|
12
|
+
* This test drives the REAL guardian-delivery reader cache (mocking only the
|
|
13
|
+
* gateway `ipcCall`) so the cold→warm transition is exercised end to end.
|
|
14
|
+
*/
|
|
15
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
16
|
+
|
|
17
|
+
const GUARDIAN_USER_ID = "U_GUARDIAN_COLD";
|
|
18
|
+
const SLACK_CHANNEL_ID = "C0COLD";
|
|
19
|
+
|
|
20
|
+
// Gateway IPC stub: returns the slack guardian delivery. The real reader caches
|
|
21
|
+
// the result under the `slack` key, so a subsequent sync `peek` finds it.
|
|
22
|
+
let ipcCalls: Array<{ route: string; input: unknown }> = [];
|
|
23
|
+
mock.module("../ipc/gateway-client.js", () => ({
|
|
24
|
+
ipcCall: async (route: string, input: unknown) => {
|
|
25
|
+
ipcCalls.push({ route, input });
|
|
26
|
+
return {
|
|
27
|
+
guardians: [
|
|
28
|
+
{
|
|
29
|
+
channelType: "slack",
|
|
30
|
+
contactId: "guardian-contact",
|
|
31
|
+
principalId: GUARDIAN_USER_ID,
|
|
32
|
+
address: GUARDIAN_USER_ID,
|
|
33
|
+
externalChatId: SLACK_CHANNEL_ID,
|
|
34
|
+
status: "active",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Contact lookup is irrelevant to guardian classification (address match on the
|
|
42
|
+
// cached delivery decides it); return null so member lookup is a no-op.
|
|
43
|
+
mock.module("../contacts/contact-store.js", () => ({
|
|
44
|
+
findContactByAddress: () => null,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Stub downstream side effects so the test isolates trust classification.
|
|
48
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
49
|
+
addMessage: async () => ({ id: "msg-1" }),
|
|
50
|
+
}));
|
|
51
|
+
mock.module("../memory/delivery-crud.js", () => ({
|
|
52
|
+
recordInbound: () => ({
|
|
53
|
+
eventId: "evt-1",
|
|
54
|
+
conversationId: "conv-1",
|
|
55
|
+
accepted: true,
|
|
56
|
+
duplicate: false,
|
|
57
|
+
}),
|
|
58
|
+
clearPayload: () => {},
|
|
59
|
+
linkMessage: () => {},
|
|
60
|
+
}));
|
|
61
|
+
mock.module("../memory/delivery-status.js", () => ({
|
|
62
|
+
markProcessed: () => {},
|
|
63
|
+
}));
|
|
64
|
+
mock.module("../memory/external-conversation-store.js", () => ({
|
|
65
|
+
upsertBinding: () => {},
|
|
66
|
+
}));
|
|
67
|
+
mock.module("../daemon/disk-pressure-guard.js", () => ({
|
|
68
|
+
getDiskPressureStatus: () => ({ level: "ok" }),
|
|
69
|
+
}));
|
|
70
|
+
mock.module("../daemon/disk-pressure-policy.js", () => ({
|
|
71
|
+
classifyDiskPressureTurnPolicy: () => ({ action: "allow" }),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// Capture the trustClass the guardian decision pipeline receives — this is the
|
|
75
|
+
// classification produced by the sync resolve after the upstream warm.
|
|
76
|
+
let receivedTrustClass: string | undefined;
|
|
77
|
+
mock.module("../runtime/routes/inbound-stages/guardian-reply-intercept.js", () => ({
|
|
78
|
+
handleGuardianReplyIntercept: async (params: { trustClass: string }) => {
|
|
79
|
+
receivedTrustClass = params.trustClass;
|
|
80
|
+
return { response: { accepted: true, canonicalRouter: "applied" } };
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
import {
|
|
85
|
+
__resetGuardianDeliveryCacheForTest,
|
|
86
|
+
peekCachedGuardianDelivery,
|
|
87
|
+
} from "../contacts/guardian-delivery-reader.js";
|
|
88
|
+
import { handleSlackReactionIntercept } from "../runtime/routes/inbound-stages/reaction-intercept.js";
|
|
89
|
+
|
|
90
|
+
function buildParams() {
|
|
91
|
+
return {
|
|
92
|
+
callbackData: "reaction:white_check_mark",
|
|
93
|
+
sourceChannel: "slack" as const,
|
|
94
|
+
sourceInterface: "slack" as const,
|
|
95
|
+
conversationExternalId: SLACK_CHANNEL_ID,
|
|
96
|
+
externalMessageId: `${SLACK_CHANNEL_ID}:1700000000.1:cold`,
|
|
97
|
+
canonicalAssistantId: "assistant-1",
|
|
98
|
+
rawSenderId: GUARDIAN_USER_ID,
|
|
99
|
+
canonicalSenderId: GUARDIAN_USER_ID,
|
|
100
|
+
actorDisplayName: "Guardian",
|
|
101
|
+
actorUsername: undefined,
|
|
102
|
+
replyCallbackUrl: "http://localhost:7830/deliver/slack",
|
|
103
|
+
sourceMetadata: { messageId: "1700000000.1", chatType: "channel" } as never,
|
|
104
|
+
slackChannelName: "general",
|
|
105
|
+
approvalConversationGenerator: undefined,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe("reaction intercept warms the channel guardian cache before sync trust", () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
__resetGuardianDeliveryCacheForTest();
|
|
112
|
+
ipcCalls = [];
|
|
113
|
+
receivedTrustClass = undefined;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("cold slack cache: guardian reaction classifies as guardian after upstream warm", async () => {
|
|
117
|
+
// Precondition: cold cache for slack — the sync peek would miss.
|
|
118
|
+
expect(peekCachedGuardianDelivery({ channelTypes: ["slack"] })).toBeUndefined();
|
|
119
|
+
|
|
120
|
+
await handleSlackReactionIntercept(buildParams());
|
|
121
|
+
|
|
122
|
+
// The intercept warmed the slack-specific key via the async reader.
|
|
123
|
+
expect(
|
|
124
|
+
ipcCalls.some(
|
|
125
|
+
(c) =>
|
|
126
|
+
c.route === "resolve_guardian_delivery" &&
|
|
127
|
+
JSON.stringify(c.input) === JSON.stringify({ channelTypes: ["slack"] }),
|
|
128
|
+
),
|
|
129
|
+
).toBe(true);
|
|
130
|
+
|
|
131
|
+
// The sync resolve, reading the now-warm snapshot, classified the reactor as
|
|
132
|
+
// the guardian — not dropped as `unknown`.
|
|
133
|
+
expect(receivedTrustClass).toBe("guardian");
|
|
134
|
+
});
|
|
135
|
+
});
|