@vellumai/assistant 0.3.27 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +81 -4
- package/Dockerfile +2 -2
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +9 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +21 -19
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +212 -36
- package/src/__tests__/notification-decision-fallback.test.ts +63 -3
- package/src/__tests__/notification-decision-strategy.test.ts +78 -0
- package/src/__tests__/notification-guardian-path.test.ts +15 -15
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +126 -59
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +358 -24
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +22 -16
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +33 -6
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +68 -326
- package/src/daemon/session-runtime-assembly.ts +119 -25
- package/src/daemon/session-tool-setup.ts +3 -2
- package/src/daemon/session.ts +4 -3
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +586 -0
- package/src/memory/channel-guardian-store.ts +2 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +56 -0
- package/src/notifications/copy-composer.ts +31 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +173 -0
- package/src/runtime/actor-trust-resolver.ts +221 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -71
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +717 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +20 -2
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +205 -529
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +53 -10
- package/src/tools/types.ts +13 -2
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the non-guardian tool grant escalation path:
|
|
3
|
+
*
|
|
4
|
+
* 1. ToolApprovalHandler grant-miss escalation behavior
|
|
5
|
+
* 2. tool_grant_request resolver registration and behavior
|
|
6
|
+
* 3. Canonical decision primitive grant minting for tool_grant_request kind
|
|
7
|
+
* 4. End-to-end: deny -> approve -> consume grant flow
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
15
|
+
|
|
16
|
+
const testDir = mkdtempSync(join(tmpdir(), 'tool-grant-escalation-test-'));
|
|
17
|
+
|
|
18
|
+
mock.module('../util/platform.js', () => ({
|
|
19
|
+
getDataDir: () => testDir,
|
|
20
|
+
isMacOS: () => process.platform === 'darwin',
|
|
21
|
+
isLinux: () => process.platform === 'linux',
|
|
22
|
+
isWindows: () => process.platform === 'win32',
|
|
23
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
24
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
25
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
26
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
27
|
+
ensureDataDir: () => {},
|
|
28
|
+
migrateToDataLayout: () => {},
|
|
29
|
+
migrateToWorkspaceLayout: () => {},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
mock.module('../util/logger.js', () => ({
|
|
33
|
+
getLogger: () =>
|
|
34
|
+
new Proxy({} as Record<string, unknown>, {
|
|
35
|
+
get: () => () => {},
|
|
36
|
+
}),
|
|
37
|
+
isDebug: () => false,
|
|
38
|
+
truncateForLog: (value: string) => value,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Mock guardian control-plane policy — not targeting control-plane by default
|
|
42
|
+
mock.module('../tools/guardian-control-plane-policy.js', () => ({
|
|
43
|
+
enforceGuardianOnlyPolicy: () => ({ denied: false }),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Mock task run rules — no task run rules by default
|
|
47
|
+
mock.module('../tasks/ephemeral-permissions.js', () => ({
|
|
48
|
+
getTaskRunRules: () => [],
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Mock tool registry — return a fake tool for 'bash'
|
|
52
|
+
const fakeTool = {
|
|
53
|
+
name: 'bash',
|
|
54
|
+
description: 'Run a shell command',
|
|
55
|
+
category: 'shell',
|
|
56
|
+
defaultRiskLevel: 'high',
|
|
57
|
+
getDefinition: () => ({ name: 'bash', description: 'Run a shell command', input_schema: {} }),
|
|
58
|
+
execute: async () => ({ content: 'ok', isError: false }),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
mock.module('../tools/registry.js', () => ({
|
|
62
|
+
getTool: (name: string) => (name === 'bash' ? fakeTool : undefined),
|
|
63
|
+
getAllTools: () => [fakeTool],
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Mock notification emission — capture calls without running the full pipeline
|
|
67
|
+
const emittedSignals: Array<Record<string, unknown>> = [];
|
|
68
|
+
mock.module('../notifications/emit-signal.js', () => ({
|
|
69
|
+
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
70
|
+
emittedSignals.push(params);
|
|
71
|
+
return { signalId: 'test-signal', deduplicated: false, dispatched: true, reason: 'ok', deliveryResults: [] };
|
|
72
|
+
},
|
|
73
|
+
registerBroadcastFn: () => {},
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// Mock channel guardian service — provide a guardian binding for 'self' + 'telegram'
|
|
77
|
+
mock.module('../runtime/channel-guardian-service.js', () => ({
|
|
78
|
+
getGuardianBinding: (assistantId: string, channel: string) => {
|
|
79
|
+
if (assistantId === 'self' && channel === 'telegram') {
|
|
80
|
+
return {
|
|
81
|
+
id: 'binding-1',
|
|
82
|
+
assistantId: 'self',
|
|
83
|
+
channel: 'telegram',
|
|
84
|
+
guardianExternalUserId: 'guardian-1',
|
|
85
|
+
guardianDeliveryChatId: 'guardian-chat-1',
|
|
86
|
+
status: 'active',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
},
|
|
91
|
+
createOutboundSession: () => ({
|
|
92
|
+
sessionId: 'test-session',
|
|
93
|
+
secret: '123456',
|
|
94
|
+
}),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// Mock gateway client — capture delivery calls
|
|
98
|
+
const deliveredReplies: Array<{ chatId: string; text: string }> = [];
|
|
99
|
+
mock.module('../runtime/gateway-client.js', () => ({
|
|
100
|
+
deliverChannelReply: async (_url: string, payload: { chatId: string; text: string }) => {
|
|
101
|
+
deliveredReplies.push(payload);
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
import {
|
|
106
|
+
applyCanonicalGuardianDecision,
|
|
107
|
+
} from '../approvals/guardian-decision-primitive.js';
|
|
108
|
+
import type { ActorContext } from '../approvals/guardian-request-resolvers.js';
|
|
109
|
+
import { getRegisteredKinds, getResolver } from '../approvals/guardian-request-resolvers.js';
|
|
110
|
+
import {
|
|
111
|
+
createCanonicalGuardianRequest,
|
|
112
|
+
getCanonicalGuardianRequest,
|
|
113
|
+
listCanonicalGuardianRequests,
|
|
114
|
+
} from '../memory/canonical-guardian-store.js';
|
|
115
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
116
|
+
import { scopedApprovalGrants } from '../memory/schema.js';
|
|
117
|
+
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
118
|
+
import { ToolApprovalHandler } from '../tools/tool-approval-handler.js';
|
|
119
|
+
import type { ToolContext, ToolLifecycleEvent } from '../tools/types.js';
|
|
120
|
+
|
|
121
|
+
initializeDb();
|
|
122
|
+
|
|
123
|
+
function resetTables(): void {
|
|
124
|
+
const db = getDb();
|
|
125
|
+
db.delete(scopedApprovalGrants).run();
|
|
126
|
+
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
127
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
afterAll(() => {
|
|
131
|
+
resetDb();
|
|
132
|
+
try {
|
|
133
|
+
rmSync(testDir, { recursive: true });
|
|
134
|
+
} catch {
|
|
135
|
+
/* best effort */
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Helpers
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
144
|
+
return {
|
|
145
|
+
workingDir: testDir,
|
|
146
|
+
sessionId: 'session-1',
|
|
147
|
+
conversationId: 'conv-1',
|
|
148
|
+
assistantId: 'self',
|
|
149
|
+
requestId: 'req-1',
|
|
150
|
+
guardianTrustClass: 'trusted_contact',
|
|
151
|
+
executionChannel: 'telegram',
|
|
152
|
+
requesterExternalUserId: 'requester-1',
|
|
153
|
+
...overrides,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
158
|
+
return {
|
|
159
|
+
externalUserId: 'guardian-1',
|
|
160
|
+
channel: 'telegram',
|
|
161
|
+
isTrusted: false,
|
|
162
|
+
...overrides,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ===========================================================================
|
|
167
|
+
// TESTS
|
|
168
|
+
// ===========================================================================
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// 1. tool_grant_request resolver registration
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
describe('tool_grant_request resolver registration', () => {
|
|
175
|
+
test('tool_grant_request resolver is registered', () => {
|
|
176
|
+
const kinds = getRegisteredKinds();
|
|
177
|
+
expect(kinds).toContain('tool_grant_request');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('getResolver returns resolver for tool_grant_request', () => {
|
|
181
|
+
const resolver = getResolver('tool_grant_request');
|
|
182
|
+
expect(resolver).toBeDefined();
|
|
183
|
+
expect(resolver!.kind).toBe('tool_grant_request');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// 2. Grant-miss escalation behavior in ToolApprovalHandler
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
describe('ToolApprovalHandler / grant-miss escalation', () => {
|
|
192
|
+
const handler = new ToolApprovalHandler();
|
|
193
|
+
const events: ToolLifecycleEvent[] = [];
|
|
194
|
+
const emitLifecycleEvent = (event: ToolLifecycleEvent) => { events.push(event); };
|
|
195
|
+
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
resetTables();
|
|
198
|
+
events.length = 0;
|
|
199
|
+
emittedSignals.length = 0;
|
|
200
|
+
deliveredReplies.length = 0;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('non-guardian + grant miss + host tool creates canonical tool_grant_request', async () => {
|
|
204
|
+
const toolName = 'bash';
|
|
205
|
+
const input = { command: 'cat /etc/passwd' };
|
|
206
|
+
|
|
207
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
208
|
+
const result = await handler.checkPreExecutionGates(
|
|
209
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(result.allowed).toBe(false);
|
|
213
|
+
if (result.allowed) return;
|
|
214
|
+
|
|
215
|
+
// A canonical tool_grant_request should have been created
|
|
216
|
+
const requests = listCanonicalGuardianRequests({
|
|
217
|
+
kind: 'tool_grant_request',
|
|
218
|
+
status: 'pending',
|
|
219
|
+
});
|
|
220
|
+
expect(requests.length).toBe(1);
|
|
221
|
+
expect(requests[0].toolName).toBe('bash');
|
|
222
|
+
expect(requests[0].requesterExternalUserId).toBe('requester-1');
|
|
223
|
+
expect(requests[0].guardianExternalUserId).toBe('guardian-1');
|
|
224
|
+
|
|
225
|
+
// Notification signal should have been emitted
|
|
226
|
+
expect(emittedSignals.length).toBe(1);
|
|
227
|
+
expect(emittedSignals[0].sourceEventName).toBe('guardian.question');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('non-guardian grant-miss response includes request code', async () => {
|
|
231
|
+
const toolName = 'bash';
|
|
232
|
+
const input = { command: 'deploy' };
|
|
233
|
+
|
|
234
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
235
|
+
const result = await handler.checkPreExecutionGates(
|
|
236
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(result.allowed).toBe(false);
|
|
240
|
+
if (result.allowed) return;
|
|
241
|
+
expect(result.result.content).toContain('request has been sent to the guardian');
|
|
242
|
+
expect(result.result.content).toContain('request code:');
|
|
243
|
+
expect(result.result.content).toContain('Please retry after the guardian approves');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('non-guardian duplicate grant-miss deduplicates the request', async () => {
|
|
247
|
+
const toolName = 'bash';
|
|
248
|
+
const input = { command: 'rm -rf /' };
|
|
249
|
+
|
|
250
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
251
|
+
|
|
252
|
+
// First invocation creates the request
|
|
253
|
+
await handler.checkPreExecutionGates(
|
|
254
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const firstRequests = listCanonicalGuardianRequests({
|
|
258
|
+
kind: 'tool_grant_request',
|
|
259
|
+
status: 'pending',
|
|
260
|
+
});
|
|
261
|
+
expect(firstRequests.length).toBe(1);
|
|
262
|
+
|
|
263
|
+
// Reset notification tracking
|
|
264
|
+
emittedSignals.length = 0;
|
|
265
|
+
|
|
266
|
+
// Second invocation with same tool+input deduplicates
|
|
267
|
+
const result = await handler.checkPreExecutionGates(
|
|
268
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(result.allowed).toBe(false);
|
|
272
|
+
if (result.allowed) return;
|
|
273
|
+
expect(result.result.content).toContain('already pending');
|
|
274
|
+
|
|
275
|
+
// Still only one canonical request
|
|
276
|
+
const requests = listCanonicalGuardianRequests({
|
|
277
|
+
kind: 'tool_grant_request',
|
|
278
|
+
status: 'pending',
|
|
279
|
+
});
|
|
280
|
+
expect(requests.length).toBe(1);
|
|
281
|
+
|
|
282
|
+
// No duplicate notification
|
|
283
|
+
expect(emittedSignals.length).toBe(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('unverified_channel does NOT create escalation request', async () => {
|
|
287
|
+
const toolName = 'bash';
|
|
288
|
+
const input = { command: 'ls' };
|
|
289
|
+
|
|
290
|
+
const context = makeContext({
|
|
291
|
+
guardianTrustClass: 'unknown',
|
|
292
|
+
executionChannel: 'telegram',
|
|
293
|
+
requesterExternalUserId: 'unknown-user',
|
|
294
|
+
});
|
|
295
|
+
const result = await handler.checkPreExecutionGates(
|
|
296
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
expect(result.allowed).toBe(false);
|
|
300
|
+
if (result.allowed) return;
|
|
301
|
+
// Should get the generic denial message, not escalation
|
|
302
|
+
expect(result.result.content).toContain('verified channel identity');
|
|
303
|
+
|
|
304
|
+
// No canonical request should have been created
|
|
305
|
+
const requests = listCanonicalGuardianRequests({
|
|
306
|
+
kind: 'tool_grant_request',
|
|
307
|
+
status: 'pending',
|
|
308
|
+
});
|
|
309
|
+
expect(requests.length).toBe(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('non-guardian without executionChannel falls back to generic denial', async () => {
|
|
313
|
+
const toolName = 'bash';
|
|
314
|
+
const input = { command: 'deploy' };
|
|
315
|
+
|
|
316
|
+
const context = makeContext({
|
|
317
|
+
guardianTrustClass: 'trusted_contact',
|
|
318
|
+
executionChannel: undefined, // no channel info
|
|
319
|
+
});
|
|
320
|
+
const result = await handler.checkPreExecutionGates(
|
|
321
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect(result.allowed).toBe(false);
|
|
325
|
+
if (result.allowed) return;
|
|
326
|
+
// Generic denial, no escalation attempted
|
|
327
|
+
expect(result.result.content).toContain('guardian approval');
|
|
328
|
+
expect(result.result.content).not.toContain('request has been sent');
|
|
329
|
+
|
|
330
|
+
const requests = listCanonicalGuardianRequests({
|
|
331
|
+
kind: 'tool_grant_request',
|
|
332
|
+
status: 'pending',
|
|
333
|
+
});
|
|
334
|
+
expect(requests.length).toBe(0);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// 3. Canonical decision and grant minting for tool_grant_request kind
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
describe('applyCanonicalGuardianDecision / tool_grant_request', () => {
|
|
343
|
+
beforeEach(() => {
|
|
344
|
+
resetTables();
|
|
345
|
+
deliveredReplies.length = 0;
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('approving tool_grant_request with tool metadata mints a grant', async () => {
|
|
349
|
+
const req = createCanonicalGuardianRequest({
|
|
350
|
+
kind: 'tool_grant_request',
|
|
351
|
+
sourceType: 'channel',
|
|
352
|
+
sourceChannel: 'telegram',
|
|
353
|
+
conversationId: 'conv-1',
|
|
354
|
+
requesterExternalUserId: 'requester-1',
|
|
355
|
+
guardianExternalUserId: 'guardian-1',
|
|
356
|
+
toolName: 'bash',
|
|
357
|
+
inputDigest: 'sha256:testdigest',
|
|
358
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const result = await applyCanonicalGuardianDecision({
|
|
362
|
+
requestId: req.id,
|
|
363
|
+
action: 'approve_once',
|
|
364
|
+
actorContext: guardianActor(),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(result.applied).toBe(true);
|
|
368
|
+
if (!result.applied) return;
|
|
369
|
+
expect(result.grantMinted).toBe(true);
|
|
370
|
+
|
|
371
|
+
// Verify canonical request is approved
|
|
372
|
+
const resolved = getCanonicalGuardianRequest(req.id);
|
|
373
|
+
expect(resolved!.status).toBe('approved');
|
|
374
|
+
expect(resolved!.decidedByExternalUserId).toBe('guardian-1');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('rejecting tool_grant_request does NOT mint a grant', async () => {
|
|
378
|
+
const req = createCanonicalGuardianRequest({
|
|
379
|
+
kind: 'tool_grant_request',
|
|
380
|
+
sourceType: 'channel',
|
|
381
|
+
sourceChannel: 'telegram',
|
|
382
|
+
conversationId: 'conv-1',
|
|
383
|
+
requesterExternalUserId: 'requester-1',
|
|
384
|
+
guardianExternalUserId: 'guardian-1',
|
|
385
|
+
toolName: 'bash',
|
|
386
|
+
inputDigest: 'sha256:testdigest',
|
|
387
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const result = await applyCanonicalGuardianDecision({
|
|
391
|
+
requestId: req.id,
|
|
392
|
+
action: 'reject',
|
|
393
|
+
actorContext: guardianActor(),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(result.applied).toBe(true);
|
|
397
|
+
if (!result.applied) return;
|
|
398
|
+
expect(result.grantMinted).toBe(false);
|
|
399
|
+
|
|
400
|
+
const resolved = getCanonicalGuardianRequest(req.id);
|
|
401
|
+
expect(resolved!.status).toBe('denied');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('identity mismatch blocks tool_grant_request approval', async () => {
|
|
405
|
+
const req = createCanonicalGuardianRequest({
|
|
406
|
+
kind: 'tool_grant_request',
|
|
407
|
+
sourceType: 'channel',
|
|
408
|
+
sourceChannel: 'telegram',
|
|
409
|
+
conversationId: 'conv-1',
|
|
410
|
+
requesterExternalUserId: 'requester-1',
|
|
411
|
+
guardianExternalUserId: 'guardian-1',
|
|
412
|
+
toolName: 'bash',
|
|
413
|
+
inputDigest: 'sha256:testdigest',
|
|
414
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const result = await applyCanonicalGuardianDecision({
|
|
418
|
+
requestId: req.id,
|
|
419
|
+
action: 'approve_once',
|
|
420
|
+
actorContext: guardianActor({ externalUserId: 'imposter-99' }),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
expect(result.applied).toBe(false);
|
|
424
|
+
if (result.applied) return;
|
|
425
|
+
expect(result.reason).toBe('identity_mismatch');
|
|
426
|
+
|
|
427
|
+
const unchanged = getCanonicalGuardianRequest(req.id);
|
|
428
|
+
expect(unchanged!.status).toBe('pending');
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// 4. End-to-end: deny -> approve -> consume grant flow
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
describe('end-to-end: tool grant escalation -> approval -> consume', () => {
|
|
437
|
+
const handler = new ToolApprovalHandler();
|
|
438
|
+
const events: ToolLifecycleEvent[] = [];
|
|
439
|
+
const emitLifecycleEvent = (event: ToolLifecycleEvent) => { events.push(event); };
|
|
440
|
+
|
|
441
|
+
beforeEach(() => {
|
|
442
|
+
resetTables();
|
|
443
|
+
events.length = 0;
|
|
444
|
+
emittedSignals.length = 0;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('first invocation denied + request created; guardian approves; second invocation succeeds; replay denied', async () => {
|
|
448
|
+
const toolName = 'bash';
|
|
449
|
+
const input = { command: 'echo secret' };
|
|
450
|
+
const _inputDigest = computeToolApprovalDigest(toolName, input);
|
|
451
|
+
|
|
452
|
+
const context = makeContext({ guardianTrustClass: 'trusted_contact' });
|
|
453
|
+
|
|
454
|
+
// Step 1: First invocation is denied, but a tool_grant_request is created
|
|
455
|
+
const firstResult = await handler.checkPreExecutionGates(
|
|
456
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
457
|
+
);
|
|
458
|
+
expect(firstResult.allowed).toBe(false);
|
|
459
|
+
|
|
460
|
+
// Verify the canonical request was created
|
|
461
|
+
const pendingRequests = listCanonicalGuardianRequests({
|
|
462
|
+
kind: 'tool_grant_request',
|
|
463
|
+
status: 'pending',
|
|
464
|
+
toolName: 'bash',
|
|
465
|
+
});
|
|
466
|
+
expect(pendingRequests.length).toBe(1);
|
|
467
|
+
const canonicalRequestId = pendingRequests[0].id;
|
|
468
|
+
|
|
469
|
+
// Step 2: Guardian approves the canonical request -> grant is minted
|
|
470
|
+
const approvalResult = await applyCanonicalGuardianDecision({
|
|
471
|
+
requestId: canonicalRequestId,
|
|
472
|
+
action: 'approve_once',
|
|
473
|
+
actorContext: guardianActor(),
|
|
474
|
+
});
|
|
475
|
+
expect(approvalResult.applied).toBe(true);
|
|
476
|
+
if (!approvalResult.applied) return;
|
|
477
|
+
expect(approvalResult.grantMinted).toBe(true);
|
|
478
|
+
|
|
479
|
+
// Verify request is now approved
|
|
480
|
+
const resolvedRequest = getCanonicalGuardianRequest(canonicalRequestId);
|
|
481
|
+
expect(resolvedRequest!.status).toBe('approved');
|
|
482
|
+
|
|
483
|
+
// Step 3: Second identical invocation consumes the grant and succeeds
|
|
484
|
+
const secondResult = await handler.checkPreExecutionGates(
|
|
485
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
486
|
+
);
|
|
487
|
+
expect(secondResult.allowed).toBe(true);
|
|
488
|
+
if (!secondResult.allowed) return;
|
|
489
|
+
expect(secondResult.grantConsumed).toBe(true);
|
|
490
|
+
|
|
491
|
+
// Step 4: Replay is denied (one-time grant semantics)
|
|
492
|
+
const replayResult = await handler.checkPreExecutionGates(
|
|
493
|
+
toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
|
|
494
|
+
);
|
|
495
|
+
expect(replayResult.allowed).toBe(false);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
@@ -419,6 +419,54 @@ describe('trusted contact activated notification signal', () => {
|
|
|
419
419
|
expect(hints.urgency).toBe('low');
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
+
test('re-verification preserves an existing guardian-managed member display name', async () => {
|
|
423
|
+
createBinding({
|
|
424
|
+
assistantId: 'self',
|
|
425
|
+
channel: 'telegram',
|
|
426
|
+
guardianExternalUserId: 'guardian-user-789',
|
|
427
|
+
guardianDeliveryChatId: 'guardian-chat-789',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
upsertMember({
|
|
431
|
+
assistantId: 'self',
|
|
432
|
+
sourceChannel: 'telegram',
|
|
433
|
+
externalUserId: 'requester-user-456',
|
|
434
|
+
externalChatId: 'chat-123',
|
|
435
|
+
status: 'revoked',
|
|
436
|
+
policy: 'allow',
|
|
437
|
+
displayName: 'Jeff',
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const session = createOutboundSession({
|
|
441
|
+
assistantId: 'self',
|
|
442
|
+
channel: 'telegram',
|
|
443
|
+
expectedExternalUserId: 'requester-user-456',
|
|
444
|
+
expectedChatId: 'chat-123',
|
|
445
|
+
identityBindingStatus: 'bound',
|
|
446
|
+
destinationAddress: 'chat-123',
|
|
447
|
+
verificationPurpose: 'trusted_contact',
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const verifyReq = buildInboundRequest({
|
|
451
|
+
content: session.secret,
|
|
452
|
+
externalChatId: 'chat-123',
|
|
453
|
+
senderExternalUserId: 'requester-user-456',
|
|
454
|
+
senderName: 'Noa Flaherty',
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await handleChannelInbound(verifyReq, undefined, TEST_BEARER_TOKEN);
|
|
458
|
+
|
|
459
|
+
const member = findMember({
|
|
460
|
+
assistantId: 'self',
|
|
461
|
+
sourceChannel: 'telegram',
|
|
462
|
+
externalUserId: 'requester-user-456',
|
|
463
|
+
externalChatId: 'chat-123',
|
|
464
|
+
});
|
|
465
|
+
expect(member).not.toBeNull();
|
|
466
|
+
expect(member!.status).toBe('active');
|
|
467
|
+
expect(member!.displayName).toBe('Jeff');
|
|
468
|
+
});
|
|
469
|
+
|
|
422
470
|
test('guardian verification does NOT emit activated signal', async () => {
|
|
423
471
|
// Create an inbound challenge (guardian flow, not trusted contact)
|
|
424
472
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -59,6 +59,19 @@ mock.module('../notifications/emit-signal.js', () => ({
|
|
|
59
59
|
deliveryResults: [],
|
|
60
60
|
};
|
|
61
61
|
},
|
|
62
|
+
registerBroadcastFn: () => {},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Mock access-request-helper directly to capture notification calls.
|
|
66
|
+
// Bun's mock.module does not intercept transitive imports reliably, so
|
|
67
|
+
// mocking emit-signal.js alone is not sufficient — access-request-helper
|
|
68
|
+
// imports emit-signal before the mock takes effect.
|
|
69
|
+
const notifyGuardianCalls: Array<Record<string, unknown>> = [];
|
|
70
|
+
mock.module('../runtime/access-request-helper.js', () => ({
|
|
71
|
+
notifyGuardianOfAccessRequest: (params: Record<string, unknown>) => {
|
|
72
|
+
notifyGuardianCalls.push(params);
|
|
73
|
+
return { notified: true, created: true, requestId: `mock-req-${Date.now()}` };
|
|
74
|
+
},
|
|
62
75
|
}));
|
|
63
76
|
|
|
64
77
|
const deliverReplyCalls: Array<{ url: string; payload: Record<string, unknown> }> = [];
|
|
@@ -75,7 +88,6 @@ mock.module('../runtime/approval-message-composer.js', () => ({
|
|
|
75
88
|
|
|
76
89
|
import {
|
|
77
90
|
createBinding,
|
|
78
|
-
findPendingAccessRequestForRequester,
|
|
79
91
|
} from '../memory/channel-guardian-store.js';
|
|
80
92
|
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
81
93
|
import { findMember, upsertMember } from '../memory/ingress-member-store.js';
|
|
@@ -109,6 +121,7 @@ function resetState(): void {
|
|
|
109
121
|
db.run('DELETE FROM notification_events');
|
|
110
122
|
db.run('DELETE FROM assistant_ingress_members');
|
|
111
123
|
emitSignalCalls.length = 0;
|
|
124
|
+
notifyGuardianCalls.length = 0;
|
|
112
125
|
deliverReplyCalls.length = 0;
|
|
113
126
|
}
|
|
114
127
|
|
|
@@ -186,7 +199,10 @@ for (const config of CHANNEL_CONFIGS) {
|
|
|
186
199
|
expect(json.denied).toBe(true);
|
|
187
200
|
expect(json.reason).toBe('not_a_member');
|
|
188
201
|
expect(deliverReplyCalls.length).toBe(1);
|
|
189
|
-
|
|
202
|
+
const replyText = (deliverReplyCalls[0].payload as Record<string, unknown>).text as string;
|
|
203
|
+
expect(
|
|
204
|
+
replyText.includes("you haven't been approved") || replyText.includes("you don't have access"),
|
|
205
|
+
).toBe(true);
|
|
190
206
|
});
|
|
191
207
|
|
|
192
208
|
test('guardian is notified when a non-member messages', async () => {
|
|
@@ -203,23 +219,10 @@ for (const config of CHANNEL_CONFIGS) {
|
|
|
203
219
|
|
|
204
220
|
expect(json.denied).toBe(true);
|
|
205
221
|
|
|
206
|
-
//
|
|
207
|
-
expect(
|
|
208
|
-
expect(
|
|
209
|
-
expect(
|
|
210
|
-
|
|
211
|
-
const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
|
|
212
|
-
expect(payload.senderExternalUserId).toBe(config.senderExternalUserId);
|
|
213
|
-
|
|
214
|
-
// Approval request was created for the correct channel
|
|
215
|
-
const pending = findPendingAccessRequestForRequester(
|
|
216
|
-
'self',
|
|
217
|
-
config.channel,
|
|
218
|
-
config.senderExternalUserId,
|
|
219
|
-
'ingress_access_request',
|
|
220
|
-
);
|
|
221
|
-
expect(pending).not.toBeNull();
|
|
222
|
-
expect(pending!.channel).toBe(config.channel);
|
|
222
|
+
// Guardian notification helper was called for the correct channel
|
|
223
|
+
expect(notifyGuardianCalls.length).toBe(1);
|
|
224
|
+
expect(notifyGuardianCalls[0].sourceChannel).toBe(config.channel);
|
|
225
|
+
expect(notifyGuardianCalls[0].senderExternalUserId).toBe(config.senderExternalUserId);
|
|
223
226
|
});
|
|
224
227
|
|
|
225
228
|
test('verification creates active member for channel', () => {
|