@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
|
@@ -6,19 +6,21 @@ import { describe, expect,test } from 'bun:test';
|
|
|
6
6
|
const templatesDir = join(import.meta.dirname, '..', 'config', 'templates');
|
|
7
7
|
const bootstrap = readFileSync(join(templatesDir, 'BOOTSTRAP.md'), 'utf-8');
|
|
8
8
|
const identity = readFileSync(join(templatesDir, 'IDENTITY.md'), 'utf-8');
|
|
9
|
+
const user = readFileSync(join(templatesDir, 'USER.md'), 'utf-8');
|
|
9
10
|
|
|
10
11
|
describe('onboarding template contracts', () => {
|
|
11
12
|
describe('BOOTSTRAP.md', () => {
|
|
12
13
|
test('contains identity question prompts', () => {
|
|
13
14
|
const lower = bootstrap.toLowerCase();
|
|
14
15
|
expect(lower).toContain('who am i');
|
|
15
|
-
expect(lower).toContain('who are you');
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
test('
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
expect(
|
|
18
|
+
test('infers personality indirectly instead of asking directly', () => {
|
|
19
|
+
const lower = bootstrap.toLowerCase();
|
|
20
|
+
// Personality step must instruct indirect/organic discovery
|
|
21
|
+
expect(lower).toContain('personality');
|
|
22
|
+
expect(lower).toContain('indirectly');
|
|
23
|
+
expect(lower).toContain('vibe');
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
test('contains emoji auto-selection with change-later instruction', () => {
|
|
@@ -27,30 +29,106 @@ describe('onboarding template contracts', () => {
|
|
|
27
29
|
expect(lower).toContain('change it later');
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
test('
|
|
31
|
-
expect(bootstrap).toMatch(/came up with X ideas/i);
|
|
32
|
-
expect(bootstrap).toMatch(/check this out/i);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('mentions avatar evolution instruction', () => {
|
|
32
|
+
test('creates Home Base silently in the background', () => {
|
|
36
33
|
const lower = bootstrap.toLowerCase();
|
|
37
|
-
expect(lower).toContain('
|
|
38
|
-
expect(lower).toContain('
|
|
34
|
+
expect(lower).toContain('app_create');
|
|
35
|
+
expect(lower).toContain('set_as_home_base');
|
|
36
|
+
// Must NOT open or announce it
|
|
37
|
+
expect(lower).toContain('do not open it with `app_open`');
|
|
38
|
+
expect(lower).toContain('do not announce it');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
test('contains naming intent markers so the first reply includes naming cues', () => {
|
|
42
42
|
const lower = bootstrap.toLowerCase();
|
|
43
43
|
// The template must prompt the assistant to ask about names.
|
|
44
|
-
// These keywords align with the client-side naming intent heuristic
|
|
45
|
-
// (ChatViewModel.replyContainsNamingIntent) so that the first reply
|
|
46
|
-
// naturally passes the quality check without triggering a corrective nudge.
|
|
47
44
|
expect(lower).toContain('name');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// The conversation sequence must include identity/naming as the first step
|
|
45
|
+
// The first step should be about locking in the assistant's name
|
|
46
|
+
expect(lower).toContain('lock in your name');
|
|
47
|
+
// The conversation sequence must include identity/naming
|
|
52
48
|
expect(lower).toContain('who am i');
|
|
53
|
-
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('asks user name AFTER assistant identity is established', () => {
|
|
52
|
+
// Step 1 is locking in the assistant's name, step 3 is asking the user's name
|
|
53
|
+
const assistantNameIdx = bootstrap.indexOf('Lock in your name.');
|
|
54
|
+
const userNameIdx = bootstrap.indexOf('who am I talking to?');
|
|
55
|
+
expect(assistantNameIdx).toBeGreaterThan(-1);
|
|
56
|
+
expect(userNameIdx).toBeGreaterThan(-1);
|
|
57
|
+
expect(assistantNameIdx).toBeLessThan(userNameIdx);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('gathers user context: work role, hobbies, daily tools', () => {
|
|
61
|
+
const lower = bootstrap.toLowerCase();
|
|
62
|
+
expect(lower).toContain('work');
|
|
63
|
+
expect(lower).toContain('hobbies');
|
|
64
|
+
expect(lower).toContain('tools');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('shows exactly 2 suggestions via ui_show card with relay_prompt actions', () => {
|
|
68
|
+
expect(bootstrap).toContain('ui_show');
|
|
69
|
+
expect(bootstrap).toContain('exactly 2');
|
|
70
|
+
// Must use card surface with relay_prompt action buttons
|
|
71
|
+
expect(bootstrap).toContain('surface_type: "card"');
|
|
72
|
+
expect(bootstrap).toContain('relay_prompt');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('contains completion gate with all required conditions', () => {
|
|
76
|
+
const lower = bootstrap.toLowerCase();
|
|
77
|
+
expect(lower).toContain('completion gate');
|
|
78
|
+
expect(lower).toContain('do not delete this file');
|
|
79
|
+
// Assistant name is hard-required
|
|
80
|
+
expect(lower).toContain('you have a name');
|
|
81
|
+
expect(lower).toContain('hard requirement');
|
|
82
|
+
expect(lower).toContain('vibe');
|
|
83
|
+
// User detail fields must be resolved (provided, inferred, or declined)
|
|
84
|
+
expect(lower).toContain('resolved');
|
|
85
|
+
expect(lower).toContain('work role');
|
|
86
|
+
expect(lower).toContain('2 suggestions shown');
|
|
87
|
+
expect(lower).toContain('selected one, deferred both');
|
|
88
|
+
expect(lower).toContain('home base');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('contains privacy/refusal policy', () => {
|
|
92
|
+
const lower = bootstrap.toLowerCase();
|
|
93
|
+
// Must have a privacy section
|
|
94
|
+
expect(lower).toContain('privacy');
|
|
95
|
+
// Assistant name is hard-required, user details are best-effort
|
|
96
|
+
expect(lower).toContain('hard-required');
|
|
97
|
+
expect(lower).toContain('best-effort');
|
|
98
|
+
// Refusal is a valid resolution
|
|
99
|
+
expect(lower).toContain('declined');
|
|
100
|
+
expect(lower).toContain('do not push');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('defines resolved as provided, inferred, or declined', () => {
|
|
104
|
+
const lower = bootstrap.toLowerCase();
|
|
105
|
+
// The template must define what "resolved" means
|
|
106
|
+
expect(lower).toContain('resolved');
|
|
107
|
+
expect(lower).toContain('inferred');
|
|
108
|
+
expect(lower).toContain('declined');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('preserves no em dashes instruction', () => {
|
|
112
|
+
const lower = bootstrap.toLowerCase();
|
|
113
|
+
expect(lower).toContain('em dashes');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('preserves no technical jargon instruction', () => {
|
|
117
|
+
const lower = bootstrap.toLowerCase();
|
|
118
|
+
expect(lower).toContain('technical jargon');
|
|
119
|
+
expect(lower).toContain('system internals');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('preserves comment line format instruction', () => {
|
|
123
|
+
// The template must start with the comment format explanation
|
|
124
|
+
expect(bootstrap).toMatch(/^_ Lines starting with _/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('instructs saving to IDENTITY.md, USER.md, and SOUL.md via file_edit', () => {
|
|
128
|
+
expect(bootstrap).toContain('IDENTITY.md');
|
|
129
|
+
expect(bootstrap).toContain('USER.md');
|
|
130
|
+
expect(bootstrap).toContain('SOUL.md');
|
|
131
|
+
expect(bootstrap).toContain('file_edit');
|
|
54
132
|
});
|
|
55
133
|
});
|
|
56
134
|
|
|
@@ -71,4 +149,21 @@ describe('onboarding template contracts', () => {
|
|
|
71
149
|
expect(identity).toContain('**Style tendency:**');
|
|
72
150
|
});
|
|
73
151
|
});
|
|
152
|
+
|
|
153
|
+
describe('USER.md', () => {
|
|
154
|
+
test('contains onboarding snapshot with all required fields', () => {
|
|
155
|
+
expect(user).toContain('Preferred name/reference:');
|
|
156
|
+
expect(user).toContain('Goals:');
|
|
157
|
+
expect(user).toContain('Locale:');
|
|
158
|
+
expect(user).toContain('Work role:');
|
|
159
|
+
expect(user).toContain('Hobbies/fun:');
|
|
160
|
+
expect(user).toContain('Daily tools:');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('documents resolved-field status conventions', () => {
|
|
164
|
+
const lower = user.toLowerCase();
|
|
165
|
+
expect(lower).toContain('declined_by_user');
|
|
166
|
+
expect(lower).toContain('resolved');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
74
169
|
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-level tests for the device pairing routes.
|
|
3
|
+
*
|
|
4
|
+
* Validates that handlePairingRequest correctly prevents a second device
|
|
5
|
+
* from hijacking an existing pairing request, while allowing the same
|
|
6
|
+
* device to call the endpoint idempotently.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import { PairingStore } from '../daemon/pairing-store.js';
|
|
12
|
+
import type { PairingHandlerContext } from '../runtime/routes/pairing-routes.js';
|
|
13
|
+
import { handlePairingRequest } from '../runtime/routes/pairing-routes.js';
|
|
14
|
+
|
|
15
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const TEST_PAIRING_ID = 'pair-test-001';
|
|
18
|
+
const TEST_SECRET = 'super-secret-value';
|
|
19
|
+
const GATEWAY_URL = 'https://gateway.test';
|
|
20
|
+
|
|
21
|
+
function makeContext(store: PairingStore): PairingHandlerContext {
|
|
22
|
+
return {
|
|
23
|
+
pairingStore: store,
|
|
24
|
+
bearerToken: 'test-bearer-token',
|
|
25
|
+
featureFlagToken: undefined,
|
|
26
|
+
pairingBroadcast: mock(() => {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makePairingRequest(overrides: Record<string, unknown> = {}): Request {
|
|
31
|
+
const body = {
|
|
32
|
+
pairingRequestId: TEST_PAIRING_ID,
|
|
33
|
+
pairingSecret: TEST_SECRET,
|
|
34
|
+
deviceId: 'device-A',
|
|
35
|
+
deviceName: 'iPhone A',
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
return new Request('http://localhost/v1/pairing/request', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Tests ────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
describe('handlePairingRequest — device binding', () => {
|
|
48
|
+
let store: PairingStore;
|
|
49
|
+
let ctx: PairingHandlerContext;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
store = new PairingStore();
|
|
53
|
+
store.start();
|
|
54
|
+
ctx = makeContext(store);
|
|
55
|
+
|
|
56
|
+
// Pre-register the pairing request (simulating QR code display)
|
|
57
|
+
store.register({
|
|
58
|
+
pairingRequestId: TEST_PAIRING_ID,
|
|
59
|
+
pairingSecret: TEST_SECRET,
|
|
60
|
+
gatewayUrl: GATEWAY_URL,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('rejects a second device attempting to pair with the same pairing ID', async () => {
|
|
65
|
+
/**
|
|
66
|
+
* Tests that once a device has initiated pairing, a different device
|
|
67
|
+
* cannot hijack the same pairing request.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
// GIVEN device A has already initiated pairing
|
|
71
|
+
const firstReq = makePairingRequest({
|
|
72
|
+
deviceId: 'device-A',
|
|
73
|
+
deviceName: 'iPhone A',
|
|
74
|
+
});
|
|
75
|
+
const firstRes = await handlePairingRequest(firstReq, ctx);
|
|
76
|
+
expect(firstRes.status).toBe(200);
|
|
77
|
+
|
|
78
|
+
// WHEN device B tries to pair with the same pairing ID and secret
|
|
79
|
+
const secondReq = makePairingRequest({
|
|
80
|
+
deviceId: 'device-B',
|
|
81
|
+
deviceName: 'iPhone B',
|
|
82
|
+
});
|
|
83
|
+
const secondRes = await handlePairingRequest(secondReq, ctx);
|
|
84
|
+
|
|
85
|
+
// THEN the request is rejected with 409 Conflict
|
|
86
|
+
expect(secondRes.status).toBe(409);
|
|
87
|
+
const body = (await secondRes.json()) as { error: { code: string; message: string } };
|
|
88
|
+
expect(body.error.code).toBe('CONFLICT');
|
|
89
|
+
expect(body.error.message).toContain('already bound to another device');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('allows the same device to call pairing request idempotently', async () => {
|
|
93
|
+
/**
|
|
94
|
+
* Tests that calling pairing request twice from the same device
|
|
95
|
+
* succeeds both times without error.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
// GIVEN device A has already initiated pairing
|
|
99
|
+
const firstReq = makePairingRequest({
|
|
100
|
+
deviceId: 'device-A',
|
|
101
|
+
deviceName: 'iPhone A',
|
|
102
|
+
});
|
|
103
|
+
const firstRes = await handlePairingRequest(firstReq, ctx);
|
|
104
|
+
expect(firstRes.status).toBe(200);
|
|
105
|
+
|
|
106
|
+
// WHEN device A calls pairing request again with the same credentials
|
|
107
|
+
const secondReq = makePairingRequest({
|
|
108
|
+
deviceId: 'device-A',
|
|
109
|
+
deviceName: 'iPhone A',
|
|
110
|
+
});
|
|
111
|
+
const secondRes = await handlePairingRequest(secondReq, ctx);
|
|
112
|
+
|
|
113
|
+
// THEN it succeeds (idempotent)
|
|
114
|
+
expect(secondRes.status).toBe(200);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('allows the same device to retrieve token after approval', async () => {
|
|
118
|
+
/**
|
|
119
|
+
* Tests that once a pairing request is approved, the same device
|
|
120
|
+
* can call the endpoint again and receive the bearer token.
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
// GIVEN device A has initiated pairing
|
|
124
|
+
const firstReq = makePairingRequest({
|
|
125
|
+
deviceId: 'device-A',
|
|
126
|
+
deviceName: 'iPhone A',
|
|
127
|
+
});
|
|
128
|
+
const firstRes = await handlePairingRequest(firstReq, ctx);
|
|
129
|
+
expect(firstRes.status).toBe(200);
|
|
130
|
+
|
|
131
|
+
// AND the pairing request has been approved
|
|
132
|
+
store.approve(TEST_PAIRING_ID, 'test-bearer-token');
|
|
133
|
+
|
|
134
|
+
// WHEN device A calls pairing request again
|
|
135
|
+
const secondReq = makePairingRequest({
|
|
136
|
+
deviceId: 'device-A',
|
|
137
|
+
deviceName: 'iPhone A',
|
|
138
|
+
});
|
|
139
|
+
const secondRes = await handlePairingRequest(secondReq, ctx);
|
|
140
|
+
|
|
141
|
+
// THEN the request succeeds (status stays approved, device matches)
|
|
142
|
+
expect(secondRes.status).toBe(200);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('rejects a different device even after the first device was approved', async () => {
|
|
146
|
+
/**
|
|
147
|
+
* Tests that a different device cannot hijack a pairing request
|
|
148
|
+
* even after the original device's request has been approved.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
// GIVEN device A has paired and been approved
|
|
152
|
+
const firstReq = makePairingRequest({
|
|
153
|
+
deviceId: 'device-A',
|
|
154
|
+
deviceName: 'iPhone A',
|
|
155
|
+
});
|
|
156
|
+
await handlePairingRequest(firstReq, ctx);
|
|
157
|
+
store.approve(TEST_PAIRING_ID, 'test-bearer-token');
|
|
158
|
+
|
|
159
|
+
// WHEN device B tries to use the same pairing request
|
|
160
|
+
const hijackReq = makePairingRequest({
|
|
161
|
+
deviceId: 'device-B',
|
|
162
|
+
deviceName: 'Attacker Phone',
|
|
163
|
+
});
|
|
164
|
+
const hijackRes = await handlePairingRequest(hijackReq, ctx);
|
|
165
|
+
|
|
166
|
+
// THEN it is rejected
|
|
167
|
+
expect(hijackRes.status).toBe(409);
|
|
168
|
+
const body = (await hijackRes.json()) as { error: { code: string; message: string } };
|
|
169
|
+
expect(body.error.code).toBe('CONFLICT');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -1,21 +1,72 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
2
|
-
|
|
3
1
|
import { describe, expect, mock,test } from 'bun:test';
|
|
4
2
|
|
|
5
|
-
const retryModulePath = resolve(import.meta.dir, '../util/retry.ts');
|
|
6
|
-
|
|
7
3
|
mock.module('../util/logger.js', () => ({
|
|
8
4
|
getLogger: () =>
|
|
9
5
|
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
10
6
|
isDebug: () => false,
|
|
11
7
|
}));
|
|
12
8
|
|
|
13
|
-
// Only mock sleep so retries complete instantly; keep real retry logic
|
|
14
|
-
mock.module
|
|
15
|
-
|
|
9
|
+
// Only mock sleep so retries complete instantly; keep real retry logic.
|
|
10
|
+
// NOTE: We must NOT use `await import()` inside mock.module — it deadlocks
|
|
11
|
+
// bun's module resolver. Instead, inline the real exports and only replace sleep.
|
|
12
|
+
mock.module('../util/retry.js', () => {
|
|
13
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
14
|
+
const DEFAULT_BASE_DELAY_MS = 1000;
|
|
15
|
+
|
|
16
|
+
function computeRetryDelay(attempt: number, baseDelayMs = DEFAULT_BASE_DELAY_MS): number {
|
|
17
|
+
const cap = baseDelayMs * Math.pow(2, attempt);
|
|
18
|
+
const half = cap / 2;
|
|
19
|
+
return half + Math.random() * half;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseRetryAfterMs(value: string): number | undefined {
|
|
23
|
+
const seconds = Number(value);
|
|
24
|
+
if (!isNaN(seconds)) return seconds * 1000;
|
|
25
|
+
const dateMs = Date.parse(value);
|
|
26
|
+
if (!isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getHttpRetryDelay(
|
|
31
|
+
response: Response,
|
|
32
|
+
attempt: number,
|
|
33
|
+
baseDelayMs = DEFAULT_BASE_DELAY_MS,
|
|
34
|
+
): number {
|
|
35
|
+
const retryAfter = response.headers.get('retry-after');
|
|
36
|
+
if (retryAfter) {
|
|
37
|
+
const parsed = parseRetryAfterMs(retryAfter);
|
|
38
|
+
if (parsed !== undefined) return parsed;
|
|
39
|
+
}
|
|
40
|
+
const effectiveBase = attempt === 0 ? baseDelayMs * 2 : baseDelayMs;
|
|
41
|
+
return Math.max(baseDelayMs, computeRetryDelay(attempt, effectiveBase));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isRetryableStatus(status: number): boolean {
|
|
45
|
+
return status === 429 || status >= 500;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRetryableNetworkError(error: unknown): boolean {
|
|
49
|
+
if (!(error instanceof Error)) return false;
|
|
50
|
+
const retryableCodes = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE']);
|
|
51
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
52
|
+
if (code && retryableCodes.has(code)) return true;
|
|
53
|
+
if (error.cause instanceof Error) {
|
|
54
|
+
const causeCode = (error.cause as NodeJS.ErrnoException).code;
|
|
55
|
+
if (causeCode && retryableCodes.has(causeCode)) return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
16
60
|
return {
|
|
17
|
-
|
|
61
|
+
DEFAULT_MAX_RETRIES,
|
|
62
|
+
DEFAULT_BASE_DELAY_MS,
|
|
63
|
+
computeRetryDelay,
|
|
64
|
+
parseRetryAfterMs,
|
|
65
|
+
getHttpRetryDelay,
|
|
66
|
+
isRetryableStatus,
|
|
67
|
+
isRetryableNetworkError,
|
|
18
68
|
sleep: () => Promise.resolve(),
|
|
69
|
+
abortableSleep: () => Promise.resolve(),
|
|
19
70
|
};
|
|
20
71
|
});
|
|
21
72
|
|
|
@@ -17,6 +17,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
17
17
|
|
|
18
18
|
mock.module('../config/loader.js', () => ({
|
|
19
19
|
getConfig: () => ({
|
|
20
|
+
ui: {},
|
|
21
|
+
|
|
20
22
|
daemon: { standaloneRecording: true },
|
|
21
23
|
provider: 'mock-provider',
|
|
22
24
|
permissions: { mode: 'legacy' },
|
|
@@ -49,6 +51,15 @@ const mockMessages: Array<{ id: string; role: string; content: string }> = [];
|
|
|
49
51
|
let mockMessageIdCounter = 0;
|
|
50
52
|
|
|
51
53
|
mock.module('../memory/conversation-store.js', () => ({
|
|
54
|
+
getConversationThreadType: () => 'default',
|
|
55
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
56
|
+
updateConversationContextWindow: () => {},
|
|
57
|
+
deleteMessageById: () => {},
|
|
58
|
+
updateConversationTitle: () => {},
|
|
59
|
+
updateConversationUsage: () => {},
|
|
60
|
+
provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
|
|
61
|
+
getConversationOriginInterface: () => null,
|
|
62
|
+
getConversationOriginChannel: () => null,
|
|
52
63
|
getMessages: () => mockMessages,
|
|
53
64
|
addMessage: (_convId: string, role: string, content: string) => {
|
|
54
65
|
const msg = { id: `msg-${++mockMessageIdCounter}`, role, content };
|
|
@@ -19,6 +19,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
19
19
|
|
|
20
20
|
mock.module('../config/loader.js', () => ({
|
|
21
21
|
getConfig: () => ({
|
|
22
|
+
ui: {},
|
|
23
|
+
|
|
22
24
|
daemon: { standaloneRecording: true },
|
|
23
25
|
provider: 'mock-provider',
|
|
24
26
|
model: 'mock-model',
|
|
@@ -233,6 +235,14 @@ mock.module('../daemon/handlers/recording.js', () => ({
|
|
|
233
235
|
// ── Mock conversation store ────────────────────────────────────────────────
|
|
234
236
|
|
|
235
237
|
mock.module('../memory/conversation-store.js', () => ({
|
|
238
|
+
getConversationThreadType: () => 'default',
|
|
239
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
240
|
+
updateConversationContextWindow: () => {},
|
|
241
|
+
deleteMessageById: () => {},
|
|
242
|
+
updateConversationUsage: () => {},
|
|
243
|
+
provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
|
|
244
|
+
getConversationOriginInterface: () => null,
|
|
245
|
+
getConversationOriginChannel: () => null,
|
|
236
246
|
getMessages: () => [],
|
|
237
247
|
addMessage: () => ({ id: 'msg-mock', role: 'assistant', content: '' }),
|
|
238
248
|
createConversation: (titleOrOpts?: string | { title?: string }) => {
|
|
@@ -269,6 +279,7 @@ mock.module('../security/secret-ingress.js', () => ({
|
|
|
269
279
|
|
|
270
280
|
mock.module('../security/secret-scanner.js', () => ({
|
|
271
281
|
redactSecrets: (text: string) => text,
|
|
282
|
+
compileCustomPatterns: () => [],
|
|
272
283
|
}));
|
|
273
284
|
|
|
274
285
|
// ── Mock classifier (for task_submit fallthrough) ──────────────────────────
|
|
@@ -307,6 +318,7 @@ mock.module('../providers/provider-send-message.js', () => ({
|
|
|
307
318
|
|
|
308
319
|
mock.module('../memory/external-conversation-store.js', () => ({
|
|
309
320
|
getBindingsForConversations: () => new Map(),
|
|
321
|
+
upsertBinding: () => {},
|
|
310
322
|
}));
|
|
311
323
|
|
|
312
324
|
// ── Mock subagent manager ──────────────────────────────────────────────────
|
|
@@ -376,6 +388,7 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
376
388
|
setTurnChannelContext: noop,
|
|
377
389
|
setTurnInterfaceContext: noop,
|
|
378
390
|
setAssistantId: noop,
|
|
391
|
+
setChannelCapabilities: noop,
|
|
379
392
|
setGuardianContext: noop,
|
|
380
393
|
setCommandIntent: noop,
|
|
381
394
|
processMessage: async () => {},
|
|
@@ -386,6 +399,8 @@ function createCtx(overrides?: Partial<HandlerContext>): {
|
|
|
386
399
|
dispose: noop,
|
|
387
400
|
hasPendingConfirmation: () => false,
|
|
388
401
|
hasPendingSecret: () => false,
|
|
402
|
+
isProcessing: () => false,
|
|
403
|
+
messages: [] as any[],
|
|
389
404
|
};
|
|
390
405
|
|
|
391
406
|
const sessions = new Map<string, any>();
|
|
@@ -16,6 +16,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
16
16
|
|
|
17
17
|
mock.module('../config/loader.js', () => ({
|
|
18
18
|
getConfig: () => ({
|
|
19
|
+
ui: {},
|
|
20
|
+
|
|
19
21
|
daemon: { standaloneRecording: true },
|
|
20
22
|
provider: 'mock-provider',
|
|
21
23
|
permissions: { mode: 'legacy' },
|
|
@@ -48,6 +50,15 @@ const mockMessages: Array<{ id: string; role: string; content: string }> = [];
|
|
|
48
50
|
let mockMessageIdCounter = 0;
|
|
49
51
|
|
|
50
52
|
mock.module('../memory/conversation-store.js', () => ({
|
|
53
|
+
getConversationThreadType: () => 'default',
|
|
54
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
55
|
+
updateConversationContextWindow: () => {},
|
|
56
|
+
deleteMessageById: () => {},
|
|
57
|
+
updateConversationTitle: () => {},
|
|
58
|
+
updateConversationUsage: () => {},
|
|
59
|
+
provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
|
|
60
|
+
getConversationOriginInterface: () => null,
|
|
61
|
+
getConversationOriginChannel: () => null,
|
|
51
62
|
getMessages: () => mockMessages,
|
|
52
63
|
addMessage: (_convId: string, role: string, content: string) => {
|
|
53
64
|
const msg = { id: `msg-${++mockMessageIdCounter}`, role, content };
|
|
@@ -417,7 +428,7 @@ describe('stale completion guard (operation token)', () => {
|
|
|
417
428
|
expect(getActiveRestartToken()).toBeNull();
|
|
418
429
|
});
|
|
419
430
|
|
|
420
|
-
test('allows tokenless recording_status during active restart (old recording ack)', () => {
|
|
431
|
+
test('allows tokenless recording_status during active restart (old recording ack)', async () => {
|
|
421
432
|
const { ctx, sent, fakeSocket } = createCtx();
|
|
422
433
|
const conversationId = 'conv-tokenless-1';
|
|
423
434
|
ctx.socketToSession.set(fakeSocket, conversationId);
|
|
@@ -442,7 +453,7 @@ describe('stale completion guard (operation token)', () => {
|
|
|
442
453
|
attachToConversationId: conversationId,
|
|
443
454
|
// No operationToken — from old recording, should be allowed
|
|
444
455
|
};
|
|
445
|
-
recordingHandlers.recording_status(tokenlessStatus, fakeSocket, ctx);
|
|
456
|
+
await recordingHandlers.recording_status(tokenlessStatus, fakeSocket, ctx);
|
|
446
457
|
|
|
447
458
|
// Should have triggered the deferred restart start
|
|
448
459
|
const newStartMsgs = sent.filter((m) => m.type === 'recording_start');
|
|
@@ -160,15 +160,19 @@ describe('tool manifest', () => {
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
test('manifest declares expected core lazy tools', () => {
|
|
163
|
+
// bash and swarm_delegate moved from lazy to eager registration
|
|
163
164
|
const lazyNames = new Set(lazyTools.map((t) => t.name));
|
|
164
|
-
expect(lazyNames.has('bash')).toBe(
|
|
165
|
+
expect(lazyNames.has('bash')).toBe(false);
|
|
165
166
|
expect(lazyNames.has('evaluate_typescript_code')).toBe(false);
|
|
166
167
|
expect(lazyNames.has('claude_code')).toBe(false);
|
|
167
|
-
expect(lazyNames.has('swarm_delegate')).toBe(
|
|
168
|
+
expect(lazyNames.has('swarm_delegate')).toBe(false);
|
|
169
|
+
// Verify they are in eager tools instead
|
|
170
|
+
expect(eagerModuleToolNames).toContain('bash');
|
|
171
|
+
expect(eagerModuleToolNames).toContain('swarm_delegate');
|
|
168
172
|
});
|
|
169
173
|
|
|
170
174
|
test('eager module tool names list contains expected count', () => {
|
|
171
|
-
expect(eagerModuleToolNames.length).toBe(
|
|
175
|
+
expect(eagerModuleToolNames.length).toBe(15);
|
|
172
176
|
});
|
|
173
177
|
|
|
174
178
|
test('explicit tools list includes memory, credential, watch, and catalog tools', () => {
|