@vellumai/assistant 0.3.19 → 0.3.20

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.
Files changed (189) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/docs/architecture/integrations.md +7 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  7. package/src/__tests__/approval-primitive.test.ts +540 -0
  8. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  9. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  11. package/src/__tests__/call-controller.test.ts +439 -108
  12. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  13. package/src/__tests__/cli.test.ts +42 -1
  14. package/src/__tests__/config-schema.test.ts +11 -127
  15. package/src/__tests__/config-watcher.test.ts +0 -8
  16. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  17. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  18. package/src/__tests__/diff.test.ts +22 -0
  19. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  20. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  21. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  22. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  23. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  24. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  25. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  26. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  27. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  28. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  29. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  30. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  31. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  32. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  33. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  34. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  35. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  36. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  37. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  38. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  39. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  40. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  41. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  42. package/src/__tests__/system-prompt.test.ts +1 -1
  43. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  44. package/src/__tests__/terminal-tools.test.ts +2 -93
  45. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  46. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  47. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  48. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  49. package/src/agent/loop.ts +36 -1
  50. package/src/approvals/approval-primitive.ts +381 -0
  51. package/src/approvals/guardian-decision-primitive.ts +191 -0
  52. package/src/calls/call-controller.ts +252 -209
  53. package/src/calls/call-domain.ts +44 -6
  54. package/src/calls/guardian-dispatch.ts +48 -0
  55. package/src/calls/types.ts +1 -1
  56. package/src/calls/voice-session-bridge.ts +46 -30
  57. package/src/cli/core-commands.ts +0 -4
  58. package/src/cli.ts +76 -34
  59. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  60. package/src/config/assistant-feature-flags.ts +162 -0
  61. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  62. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  63. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  64. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  65. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  66. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  67. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  68. package/src/config/core-schema.ts +1 -1
  69. package/src/config/env-registry.ts +10 -0
  70. package/src/config/feature-flag-registry.json +61 -0
  71. package/src/config/loader.ts +22 -1
  72. package/src/config/sandbox-schema.ts +0 -39
  73. package/src/config/schema.ts +6 -2
  74. package/src/config/skill-state.ts +34 -0
  75. package/src/config/skills-schema.ts +0 -1
  76. package/src/config/skills.ts +9 -0
  77. package/src/config/system-prompt.ts +110 -46
  78. package/src/config/templates/SOUL.md +1 -1
  79. package/src/config/types.ts +19 -1
  80. package/src/config/vellum-skills/catalog.json +1 -1
  81. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  82. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  83. package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
  84. package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  86. package/src/daemon/config-watcher.ts +0 -1
  87. package/src/daemon/daemon-control.ts +1 -1
  88. package/src/daemon/guardian-invite-intent.ts +124 -0
  89. package/src/daemon/handlers/avatar.ts +68 -0
  90. package/src/daemon/handlers/browser.ts +2 -2
  91. package/src/daemon/handlers/guardian-actions.ts +120 -0
  92. package/src/daemon/handlers/index.ts +4 -0
  93. package/src/daemon/handlers/sessions.ts +19 -0
  94. package/src/daemon/handlers/shared.ts +3 -1
  95. package/src/daemon/install-cli-launchers.ts +58 -13
  96. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  97. package/src/daemon/ipc-contract/sessions.ts +8 -2
  98. package/src/daemon/ipc-contract/settings.ts +25 -2
  99. package/src/daemon/ipc-contract-inventory.json +10 -0
  100. package/src/daemon/ipc-contract.ts +4 -0
  101. package/src/daemon/lifecycle.ts +6 -2
  102. package/src/daemon/main.ts +1 -0
  103. package/src/daemon/server.ts +1 -0
  104. package/src/daemon/session-lifecycle.ts +52 -7
  105. package/src/daemon/session-memory.ts +45 -0
  106. package/src/daemon/session-process.ts +258 -432
  107. package/src/daemon/session-runtime-assembly.ts +12 -0
  108. package/src/daemon/session-skill-tools.ts +14 -1
  109. package/src/daemon/session-tool-setup.ts +5 -0
  110. package/src/daemon/session.ts +11 -0
  111. package/src/daemon/tool-side-effects.ts +35 -9
  112. package/src/index.ts +0 -2
  113. package/src/memory/conversation-display-order-migration.ts +44 -0
  114. package/src/memory/conversation-queries.ts +2 -0
  115. package/src/memory/conversation-store.ts +91 -0
  116. package/src/memory/db-init.ts +5 -1
  117. package/src/memory/embedding-local.ts +13 -8
  118. package/src/memory/guardian-action-store.ts +125 -2
  119. package/src/memory/ingress-invite-store.ts +95 -1
  120. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  121. package/src/memory/migrations/index.ts +2 -1
  122. package/src/memory/schema.ts +5 -1
  123. package/src/memory/scoped-approval-grants.ts +14 -5
  124. package/src/messaging/providers/slack/client.ts +12 -0
  125. package/src/messaging/providers/slack/types.ts +5 -0
  126. package/src/notifications/decision-engine.ts +49 -12
  127. package/src/notifications/emit-signal.ts +7 -0
  128. package/src/notifications/signal.ts +7 -0
  129. package/src/notifications/thread-seed-composer.ts +2 -1
  130. package/src/runtime/channel-approval-types.ts +16 -6
  131. package/src/runtime/channel-approvals.ts +19 -15
  132. package/src/runtime/channel-invite-transport.ts +85 -0
  133. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  134. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  135. package/src/runtime/guardian-action-message-composer.ts +30 -0
  136. package/src/runtime/guardian-decision-types.ts +91 -0
  137. package/src/runtime/http-server.ts +23 -1
  138. package/src/runtime/ingress-service.ts +22 -0
  139. package/src/runtime/invite-redemption-service.ts +181 -0
  140. package/src/runtime/invite-redemption-templates.ts +39 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  143. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  144. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  145. package/src/runtime/routes/pairing-routes.ts +4 -0
  146. package/src/security/encrypted-store.ts +31 -17
  147. package/src/security/keychain.ts +176 -2
  148. package/src/security/secure-keys.ts +97 -0
  149. package/src/security/tool-approval-digest.ts +1 -1
  150. package/src/tools/browser/browser-execution.ts +2 -2
  151. package/src/tools/browser/browser-manager.ts +46 -32
  152. package/src/tools/browser/browser-screencast.ts +2 -2
  153. package/src/tools/calls/call-start.ts +1 -1
  154. package/src/tools/executor.ts +22 -17
  155. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  156. package/src/tools/skills/load.ts +22 -8
  157. package/src/tools/system/avatar-generator.ts +119 -0
  158. package/src/tools/system/navigate-settings.ts +65 -0
  159. package/src/tools/system/open-system-settings.ts +75 -0
  160. package/src/tools/system/voice-config.ts +121 -32
  161. package/src/tools/terminal/backends/native.ts +40 -19
  162. package/src/tools/terminal/backends/types.ts +3 -3
  163. package/src/tools/terminal/parser.ts +1 -1
  164. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  165. package/src/tools/terminal/sandbox.ts +1 -12
  166. package/src/tools/terminal/shell.ts +3 -31
  167. package/src/tools/tool-approval-handler.ts +141 -3
  168. package/src/tools/tool-manifest.ts +6 -0
  169. package/src/tools/types.ts +6 -0
  170. package/src/util/diff.ts +36 -13
  171. package/Dockerfile.sandbox +0 -5
  172. package/src/__tests__/doordash-client.test.ts +0 -187
  173. package/src/__tests__/doordash-session.test.ts +0 -154
  174. package/src/__tests__/signup-e2e.test.ts +0 -354
  175. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  176. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  177. package/src/cli/doordash.ts +0 -1057
  178. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  179. package/src/config/templates/LOOKS.md +0 -25
  180. package/src/doordash/cart-queries.ts +0 -787
  181. package/src/doordash/client.ts +0 -1016
  182. package/src/doordash/order-queries.ts +0 -85
  183. package/src/doordash/queries.ts +0 -13
  184. package/src/doordash/query-extractor.ts +0 -94
  185. package/src/doordash/search-queries.ts +0 -203
  186. package/src/doordash/session.ts +0 -84
  187. package/src/doordash/store-queries.ts +0 -246
  188. package/src/doordash/types.ts +0 -367
  189. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -1,187 +0,0 @@
1
- import { describe, expect,it } from 'bun:test';
2
-
3
- import { SessionExpiredError } from '../doordash/client.js';
4
-
5
- describe('SessionExpiredError', () => {
6
- it('is an instance of Error', () => {
7
- const err = new SessionExpiredError('test reason');
8
- expect(err).toBeInstanceOf(Error);
9
- });
10
-
11
- it('has name set to SessionExpiredError', () => {
12
- const err = new SessionExpiredError('test reason');
13
- expect(err.name).toBe('SessionExpiredError');
14
- });
15
-
16
- it('preserves the reason as the message', () => {
17
- const err = new SessionExpiredError('DoorDash session has expired.');
18
- expect(err.message).toBe('DoorDash session has expired.');
19
- });
20
-
21
- it('can be distinguished from plain Error via instanceof', () => {
22
- const sessionErr = new SessionExpiredError('expired');
23
- const plainErr = new Error('something else');
24
- expect(sessionErr instanceof SessionExpiredError).toBe(true);
25
- expect(plainErr instanceof SessionExpiredError).toBe(false);
26
- });
27
-
28
- it('produces a useful stack trace', () => {
29
- const err = new SessionExpiredError('no session');
30
- expect(err.stack).toBeDefined();
31
- expect(err.stack).toContain('SessionExpiredError');
32
- });
33
- });
34
-
35
- describe('expired session classification', () => {
36
- // The CDP response handler in cdpFetch classifies certain HTTP statuses
37
- // as session-expired. We test the classification logic by simulating
38
- // the parsed response structure that cdpFetch evaluates.
39
-
40
- function classifyResponse(parsed: Record<string, unknown>): Error {
41
- // Mirrors the classification logic from cdpFetch (client.ts lines 154-159)
42
- if (parsed.__error) {
43
- if (parsed.__status === 403 || parsed.__status === 401) {
44
- return new SessionExpiredError('DoorDash session has expired.');
45
- }
46
- return new Error(
47
- (parsed.__message as string) ??
48
- `HTTP ${parsed.__status}: ${(parsed.__body as string) ?? ''}`,
49
- );
50
- }
51
- return new Error('No error');
52
- }
53
-
54
- it('classifies HTTP 401 as SessionExpiredError', () => {
55
- const err = classifyResponse({ __error: true, __status: 401, __body: 'Unauthorized' });
56
- expect(err).toBeInstanceOf(SessionExpiredError);
57
- expect(err.message).toBe('DoorDash session has expired.');
58
- });
59
-
60
- it('classifies HTTP 403 as SessionExpiredError', () => {
61
- const err = classifyResponse({ __error: true, __status: 403, __body: 'Forbidden' });
62
- expect(err).toBeInstanceOf(SessionExpiredError);
63
- expect(err.message).toBe('DoorDash session has expired.');
64
- });
65
-
66
- it('classifies HTTP 500 as a generic Error, not session expired', () => {
67
- const err = classifyResponse({ __error: true, __status: 500, __body: 'Internal Server Error' });
68
- expect(err).not.toBeInstanceOf(SessionExpiredError);
69
- expect(err.message).toBe('HTTP 500: Internal Server Error');
70
- });
71
-
72
- it('classifies HTTP 429 as a generic Error', () => {
73
- const err = classifyResponse({ __error: true, __status: 429, __body: 'Rate limited' });
74
- expect(err).not.toBeInstanceOf(SessionExpiredError);
75
- expect(err.message).toBe('HTTP 429: Rate limited');
76
- });
77
-
78
- it('uses __message when available', () => {
79
- const err = classifyResponse({ __error: true, __message: 'fetch failed' });
80
- expect(err).not.toBeInstanceOf(SessionExpiredError);
81
- expect(err.message).toBe('fetch failed');
82
- });
83
-
84
- it('handles response with no __body or __message gracefully', () => {
85
- const err = classifyResponse({ __error: true, __status: 502 });
86
- expect(err).not.toBeInstanceOf(SessionExpiredError);
87
- expect(err.message).toBe('HTTP 502: ');
88
- });
89
- });
90
-
91
- describe('CDP failure scenarios', () => {
92
- // These test the error conditions that cdpFetch can encounter:
93
- // 1. CDP protocol error (msg.error present)
94
- // 2. Empty CDP response (no value in result)
95
- // 3. Timeout (30s)
96
- // 4. WebSocket connection failure
97
-
98
- // We can test the error construction logic without connecting to a real CDP
99
-
100
- it('CDP protocol error produces a descriptive message', () => {
101
- // Simulates the error path at client.ts line 143
102
- const cdpError = { message: 'Cannot find context with specified id' };
103
- const err = new Error(`CDP error: ${cdpError.message}`);
104
- expect(err.message).toBe('CDP error: Cannot find context with specified id');
105
- });
106
-
107
- it('Empty CDP response produces a clear error', () => {
108
- // Simulates the error path at client.ts line 149
109
- const value = undefined;
110
- const err = !value ? new Error('Empty CDP response') : null;
111
- expect(err).not.toBeNull();
112
- expect(err!.message).toBe('Empty CDP response');
113
- });
114
-
115
- it('CDP timeout error message includes the timeout duration', () => {
116
- // Simulates the timeout error at client.ts line 92
117
- const err = new Error('CDP fetch timed out after 30s');
118
- expect(err.message).toContain('30s');
119
- });
120
-
121
- it('WebSocket connection failure produces SessionExpiredError', () => {
122
- // Simulates ws.onerror at client.ts line 172
123
- const err = new SessionExpiredError('CDP connection failed.');
124
- expect(err).toBeInstanceOf(SessionExpiredError);
125
- expect(err.message).toBe('CDP connection failed.');
126
- });
127
-
128
- it('findDoordashTab failure when CDP is unavailable', () => {
129
- // Simulates findDoordashTab at client.ts line 67
130
- const err = new SessionExpiredError(
131
- 'Chrome CDP not available. Run `vellum doordash refresh` first.',
132
- );
133
- expect(err).toBeInstanceOf(SessionExpiredError);
134
- expect(err.message).toContain('Chrome CDP not available');
135
- });
136
-
137
- it('findDoordashTab failure when no tab is available', () => {
138
- // Simulates findDoordashTab at client.ts line 76
139
- const err = new SessionExpiredError(
140
- 'No Chrome tab available for DoorDash requests.',
141
- );
142
- expect(err).toBeInstanceOf(SessionExpiredError);
143
- expect(err.message).toContain('No Chrome tab available');
144
- });
145
-
146
- it('requireSession throws SessionExpiredError when no session exists', () => {
147
- // Simulates requireSession at client.ts line 56
148
- const session = null;
149
- const err = !session
150
- ? new SessionExpiredError('No DoorDash session found.')
151
- : null;
152
- expect(err).toBeInstanceOf(SessionExpiredError);
153
- expect(err!.message).toBe('No DoorDash session found.');
154
- });
155
-
156
- it('GraphQL errors are joined with semicolons', () => {
157
- // Simulates the error handling at client.ts lines 192-194
158
- const errors = [
159
- { message: 'Field "x" not found' },
160
- { message: 'Unauthorized' },
161
- ];
162
- const msgs = errors.map(e => e.message || JSON.stringify(e)).join('; ');
163
- const err = new Error(`GraphQL errors: ${msgs}`);
164
- expect(err.message).toBe(
165
- 'GraphQL errors: Field "x" not found; Unauthorized',
166
- );
167
- });
168
-
169
- it('GraphQL errors use JSON.stringify for errors without message', () => {
170
- const errors = [{ extensions: { code: 'INTERNAL_ERROR' } }];
171
- const msgs = errors
172
- .map(e => (e as Record<string, unknown>).message || JSON.stringify(e))
173
- .join('; ');
174
- const err = new Error(`GraphQL errors: ${msgs}`);
175
- expect(err.message).toContain('INTERNAL_ERROR');
176
- });
177
-
178
- it('Empty GraphQL response throws', () => {
179
- // Simulates client.ts lines 196-198
180
- const data = undefined;
181
- const err = !data
182
- ? new Error('Empty response from DoorDash API')
183
- : null;
184
- expect(err).not.toBeNull();
185
- expect(err!.message).toBe('Empty response from DoorDash API');
186
- });
187
- });
@@ -1,154 +0,0 @@
1
- import { existsSync, mkdirSync, rmSync,writeFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
-
5
- import { afterEach,beforeEach, describe, expect, it } from 'bun:test';
6
-
7
- import {
8
- type DoorDashSession,
9
- getCookieHeader,
10
- getCsrfToken,
11
- importFromRecording,
12
- } from '../doordash/session.js';
13
-
14
- // Override getDataDir to use a temp directory during tests
15
- const TEST_DIR = join(tmpdir(), `vellum-dd-test-${process.pid}`);
16
- let originalDataDir: string | undefined;
17
-
18
- // We mock getDataDir by patching the module at the fs level:
19
- // session.ts calls getSessionDir() -> join(getDataDir(), 'doordash')
20
- // We'll test session.ts helpers that don't depend on getDataDir directly,
21
- // and test the persistence functions via the actual file system with a known path.
22
-
23
- function makeCookie(name: string, value: string): {
24
- name: string;
25
- value: string;
26
- domain: string;
27
- path: string;
28
- httpOnly: boolean;
29
- secure: boolean;
30
- } {
31
- return { name, value, domain: '.doordash.com', path: '/', httpOnly: false, secure: false };
32
- }
33
-
34
- function makeSession(overrides?: Partial<DoorDashSession>): DoorDashSession {
35
- return {
36
- cookies: [
37
- makeCookie('dd_session', 'abc123'),
38
- makeCookie('csrf_token', 'tok456'),
39
- ],
40
- importedAt: '2025-01-15T12:00:00.000Z',
41
- recordingId: 'rec-001',
42
- ...overrides,
43
- };
44
- }
45
-
46
- describe('DoorDash session helpers', () => {
47
- describe('getCookieHeader', () => {
48
- it('joins all cookies into a single header string', () => {
49
- const session = makeSession();
50
- const header = getCookieHeader(session);
51
- expect(header).toBe('dd_session=abc123; csrf_token=tok456');
52
- });
53
-
54
- it('returns empty string for a session with no cookies', () => {
55
- const session = makeSession({ cookies: [] });
56
- expect(getCookieHeader(session)).toBe('');
57
- });
58
-
59
- it('handles a single cookie without trailing semicolons', () => {
60
- const session = makeSession({ cookies: [makeCookie('a', '1')] });
61
- expect(getCookieHeader(session)).toBe('a=1');
62
- });
63
- });
64
-
65
- describe('getCsrfToken', () => {
66
- it('extracts the csrf_token value when present', () => {
67
- const session = makeSession();
68
- expect(getCsrfToken(session)).toBe('tok456');
69
- });
70
-
71
- it('returns undefined when csrf_token is absent', () => {
72
- const session = makeSession({
73
- cookies: [makeCookie('dd_session', 'abc123')],
74
- });
75
- expect(getCsrfToken(session)).toBeUndefined();
76
- });
77
- });
78
- });
79
-
80
- describe('DoorDash session persistence', () => {
81
- // These tests exercise the real loadSession/saveSession/clearSession
82
- // by writing to the actual session path. We need to mock getDataDir.
83
- // Since the module uses a private function we can't easily mock,
84
- // we test via importFromRecording which exercises save+load.
85
-
86
- beforeEach(() => {
87
- originalDataDir = process.env.BASE_DATA_DIR;
88
- process.env.BASE_DATA_DIR = TEST_DIR;
89
- // Ensure test dir exists
90
- mkdirSync(TEST_DIR, { recursive: true });
91
- });
92
-
93
- afterEach(() => {
94
- // Restore original BASE_DATA_DIR
95
- if (originalDataDir === undefined) {
96
- delete process.env.BASE_DATA_DIR;
97
- } else {
98
- process.env.BASE_DATA_DIR = originalDataDir;
99
- }
100
- // Clean up test dir
101
- if (existsSync(TEST_DIR)) {
102
- rmSync(TEST_DIR, { recursive: true, force: true });
103
- }
104
- });
105
-
106
- describe('importFromRecording', () => {
107
- it('throws when the recording file does not exist', () => {
108
- expect(() => importFromRecording('/nonexistent/recording.json')).toThrow(
109
- 'Recording not found',
110
- );
111
- });
112
-
113
- it('throws when the recording contains no cookies', () => {
114
- const recordingPath = join(TEST_DIR, 'empty-recording.json');
115
- writeFileSync(
116
- recordingPath,
117
- JSON.stringify({
118
- id: 'rec-empty',
119
- startedAt: 0,
120
- endedAt: 1,
121
- targetDomain: 'doordash.com',
122
- networkEntries: [],
123
- cookies: [],
124
- observations: [],
125
- }),
126
- );
127
- expect(() => importFromRecording(recordingPath)).toThrow(
128
- 'Recording contains no cookies',
129
- );
130
- });
131
-
132
- it('successfully imports a recording with cookies', () => {
133
- const recordingPath = join(TEST_DIR, 'valid-recording.json');
134
- writeFileSync(
135
- recordingPath,
136
- JSON.stringify({
137
- id: 'rec-valid',
138
- startedAt: 0,
139
- endedAt: 1,
140
- targetDomain: 'doordash.com',
141
- networkEntries: [],
142
- cookies: [makeCookie('session_id', 'xyz')],
143
- observations: [],
144
- }),
145
- );
146
- const session = importFromRecording(recordingPath);
147
- expect(session.cookies).toHaveLength(1);
148
- expect(session.cookies[0].name).toBe('session_id');
149
- expect(session.cookies[0].value).toBe('xyz');
150
- expect(session.recordingId).toBe('rec-valid');
151
- expect(session.importedAt).toBeTruthy();
152
- });
153
- });
154
- });
@@ -1,354 +0,0 @@
1
- import { mkdirSync,mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
-
5
- import { afterAll, beforeAll, beforeEach, describe, expect, mock,test } from 'bun:test';
6
-
7
- // ── Mocks (before any app imports) ──────────────────────────────────
8
-
9
- const testDir = mkdtempSync(join(tmpdir(), 'signup-e2e-'));
10
-
11
- mock.module('../util/logger.js', () => ({
12
- getLogger: () =>
13
- new Proxy({} as Record<string, unknown>, {
14
- get: () => () => {},
15
- }),
16
- }));
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
- getPlatformName: () => process.platform,
29
- }));
30
-
31
- mock.module('../tools/registry.js', () => ({
32
- registerTool: () => {},
33
- }));
34
-
35
- // Force encrypted backend (no keychain) with temp store path
36
- import { _overrideDeps, _resetDeps } from '../security/keychain.js';
37
-
38
- _overrideDeps({
39
- isMacOS: () => false,
40
- isLinux: () => false,
41
- execFileSync: (() => '') as unknown as typeof import('node:child_process').execFileSync,
42
- });
43
-
44
- import { _setStorePath } from '../security/encrypted-store.js';
45
- import { _resetBackend } from '../security/secure-keys.js';
46
-
47
- const STORE_PATH = join(testDir, 'keys.enc');
48
-
49
- // ── Imports (after mocks) ───────────────────────────────────────────
50
-
51
- import {
52
- createAccount,
53
- listAccounts,
54
- } from '../memory/account-store.js';
55
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
56
- import {
57
- deleteSecureKey,
58
- getSecureKey,
59
- listSecureKeys,
60
- setSecureKey,
61
- } from '../security/secure-keys.js';
62
- import {
63
- executeBrowserClick,
64
- executeBrowserClose,
65
- executeBrowserExtract,
66
- executeBrowserFillCredential,
67
- executeBrowserNavigate,
68
- executeBrowserType,
69
- } from '../tools/browser/headless-browser.js';
70
- import { _setMetadataPath,upsertCredentialMetadata } from '../tools/credentials/metadata-store.js';
71
- import type { ToolContext } from '../tools/types.js';
72
- import { createMockSignupServer, type MockSignupServer } from './fixtures/mock-signup-server.js';
73
-
74
- // ── Setup ───────────────────────────────────────────────────────────
75
-
76
- initializeDb();
77
-
78
- const ctx: ToolContext = {
79
- sessionId: 'e2e-test',
80
- conversationId: 'e2e-conv',
81
- workingDir: '/tmp',
82
- };
83
-
84
- // Test-only password (assembled to avoid pre-commit false positives)
85
- const TEST_PASSWORD = ['S3cure', '!Pass', '789'].join('');
86
-
87
- let server: MockSignupServer;
88
- let url: string;
89
-
90
- beforeAll(async () => {
91
- _resetBackend();
92
- mkdirSync(join(testDir, 'browser-profile'), { recursive: true });
93
- _setStorePath(STORE_PATH);
94
- _setMetadataPath(join(testDir, 'metadata.json'));
95
-
96
- server = createMockSignupServer();
97
- ({ url } = await server.start());
98
- });
99
-
100
- afterAll(async () => {
101
- resetDb();
102
- await executeBrowserClose({ close_all_pages: true }, ctx);
103
- await server.stop();
104
- _setMetadataPath(null);
105
- _setStorePath(null);
106
- _resetBackend();
107
- _resetDeps();
108
- mock.restore();
109
- try {
110
- rmSync(testDir, { recursive: true });
111
- } catch {
112
- /* best effort */
113
- }
114
- });
115
-
116
- beforeEach(() => {
117
- server.reset();
118
- // Clear accounts table
119
- const db = getDb();
120
- db.run('DELETE FROM accounts');
121
- // Clear credentials
122
- for (const key of listSecureKeys()) {
123
- deleteSecureKey(key);
124
- }
125
- });
126
-
127
- // ── Tests ───────────────────────────────────────────────────────────
128
-
129
- describe('end-to-end signup flow', () => {
130
- test('happy path: full signup with credential fill', async () => {
131
- // Store credential in vault with metadata (broker requires metadata)
132
- const storeOk = setSecureKey(`credential:mockservice:password`, TEST_PASSWORD);
133
- expect(storeOk).toBe(true);
134
- upsertCredentialMetadata('mockservice', 'password', { allowedTools: ['browser_fill_credential'] });
135
- expect(getSecureKey('credential:mockservice:password')).toBe(TEST_PASSWORD);
136
-
137
- // Navigate to signup
138
- const navResult = await executeBrowserNavigate(
139
- { url: `${url}/signup`, allow_private_network: true },
140
- ctx,
141
- );
142
- expect(navResult.isError).toBe(false);
143
- expect(navResult.content).toContain('Status: 200');
144
-
145
- // Step 1: Name
146
- await executeBrowserType(
147
- { selector: 'input[name="first_name"]', text: 'Jane' },
148
- ctx,
149
- );
150
- await executeBrowserType(
151
- { selector: 'input[name="last_name"]', text: 'Doe' },
152
- ctx,
153
- );
154
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
155
-
156
- // Step 2: Username + password (via credential fill)
157
- await executeBrowserType(
158
- { selector: 'input[name="username"]', text: 'janedoe' },
159
- ctx,
160
- );
161
- const fillResult = await executeBrowserFillCredential(
162
- {
163
- service: 'mockservice',
164
- field: 'password',
165
- selector: 'input[name="password"]',
166
- },
167
- ctx,
168
- );
169
- expect(fillResult.isError).toBe(false);
170
- // Credential value must NEVER appear in output
171
- expect(fillResult.content).not.toContain(TEST_PASSWORD);
172
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
173
-
174
- // Step 3: Verification code
175
- const code = server.getVerificationCode();
176
- expect(code).toMatch(/^\d{6}$/);
177
- await executeBrowserType(
178
- { selector: 'input[name="code"]', text: code },
179
- ctx,
180
- );
181
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
182
-
183
- // Step 4: Solve CAPTCHA checkbox and submit
184
- await executeBrowserClick(
185
- { selector: 'input[name="captcha_solved"]' },
186
- ctx,
187
- );
188
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
189
-
190
- // Verify success page
191
- const extractResult = await executeBrowserExtract({}, ctx);
192
- expect(extractResult.content).toContain('Account created successfully');
193
-
194
- // Register account in the account registry
195
- const acct = createAccount({
196
- service: 'mockservice',
197
- username: 'janedoe',
198
- credentialRef: 'mockservice',
199
- status: 'active',
200
- });
201
- expect(acct.id).toBeTruthy();
202
-
203
- // List accounts
204
- const accounts = listAccounts();
205
- expect(accounts).toHaveLength(1);
206
- expect(accounts[0].username).toBe('janedoe');
207
-
208
- // Verify server recorded the account
209
- const serverAccounts = server.getAccounts();
210
- expect(serverAccounts).toHaveLength(1);
211
- expect(serverAccounts[0].username).toBe('janedoe');
212
- }, 60_000);
213
-
214
- test('credential not found produces helpful error', async () => {
215
- await executeBrowserNavigate(
216
- { url: `${url}/signup`, allow_private_network: true },
217
- ctx,
218
- );
219
- const result = await executeBrowserFillCredential(
220
- {
221
- service: 'nonexistent',
222
- field: 'password',
223
- selector: 'input[name="first_name"]',
224
- },
225
- ctx,
226
- );
227
- expect(result.isError).toBe(true);
228
- expect(result.content).toContain('No credential stored');
229
- expect(result.content).toContain('credential_store');
230
- }, 30_000);
231
-
232
- test('taken username shows validation error', async () => {
233
- // Store credential so fill works
234
- setSecureKey(`credential:mockservice:password`, TEST_PASSWORD);
235
- upsertCredentialMetadata('mockservice', 'password', { allowedTools: ['browser_fill_credential'] });
236
-
237
- await executeBrowserNavigate(
238
- { url: `${url}/signup`, allow_private_network: true },
239
- ctx,
240
- );
241
-
242
- // Complete step 1
243
- await executeBrowserType(
244
- { selector: 'input[name="first_name"]', text: 'Test' },
245
- ctx,
246
- );
247
- await executeBrowserType(
248
- { selector: 'input[name="last_name"]', text: 'User' },
249
- ctx,
250
- );
251
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
252
-
253
- // Try taken username
254
- await executeBrowserType(
255
- { selector: 'input[name="username"]', text: 'taken' },
256
- ctx,
257
- );
258
- await executeBrowserFillCredential(
259
- {
260
- service: 'mockservice',
261
- field: 'password',
262
- selector: 'input[name="password"]',
263
- },
264
- ctx,
265
- );
266
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
267
-
268
- // Should see error
269
- const extract = await executeBrowserExtract({}, ctx);
270
- expect(extract.content).toContain('taken');
271
- }, 30_000);
272
-
273
- test('wrong verification code shows error', async () => {
274
- // Store credential so fill works
275
- setSecureKey(`credential:mockservice:password`, TEST_PASSWORD);
276
- upsertCredentialMetadata('mockservice', 'password', { allowedTools: ['browser_fill_credential'] });
277
-
278
- await executeBrowserNavigate(
279
- { url: `${url}/signup`, allow_private_network: true },
280
- ctx,
281
- );
282
-
283
- // Step 1: name
284
- await executeBrowserType(
285
- { selector: 'input[name="first_name"]', text: 'Test' },
286
- ctx,
287
- );
288
- await executeBrowserType(
289
- { selector: 'input[name="last_name"]', text: 'User' },
290
- ctx,
291
- );
292
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
293
-
294
- // Step 2: username/password
295
- await executeBrowserType(
296
- { selector: 'input[name="username"]', text: 'testuser' },
297
- ctx,
298
- );
299
- await executeBrowserFillCredential(
300
- {
301
- service: 'mockservice',
302
- field: 'password',
303
- selector: 'input[name="password"]',
304
- },
305
- ctx,
306
- );
307
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
308
-
309
- // Step 3: wrong code
310
- await executeBrowserType(
311
- { selector: 'input[name="code"]', text: '000000' },
312
- ctx,
313
- );
314
- await executeBrowserClick({ selector: 'button[type="submit"]' }, ctx);
315
-
316
- // Verify error message
317
- const extract = await executeBrowserExtract({}, ctx);
318
- expect(extract.content).toContain('Invalid verification code');
319
- }, 30_000);
320
-
321
- test('credential value never leaks into any tool output', async () => {
322
- const secret = ['MyS3cret', '!Value', '42'].join('');
323
- setSecureKey(`credential:leak-test:password`, secret);
324
- upsertCredentialMetadata('leak-test', 'password', { allowedTools: ['browser_fill_credential'] });
325
-
326
- await executeBrowserNavigate(
327
- { url: `${url}/signup`, allow_private_network: true },
328
- ctx,
329
- );
330
-
331
- // Fill credential
332
- const fillResult = await executeBrowserFillCredential(
333
- {
334
- service: 'leak-test',
335
- field: 'password',
336
- selector: 'input[name="first_name"]',
337
- },
338
- ctx,
339
- );
340
-
341
- // Secret must not appear in fill output
342
- expect(fillResult.content).not.toContain(secret);
343
-
344
- // List credentials — should only show metadata
345
- const allKeys = listSecureKeys();
346
- const credentialKeys = allKeys.filter((k) => k.startsWith('credential:'));
347
- const entries = credentialKeys.map((k) => {
348
- const parts = k.split(':');
349
- return { service: parts[1], field: parts.slice(2).join(':') };
350
- });
351
- const listOutput = JSON.stringify(entries);
352
- expect(listOutput).not.toContain(secret);
353
- }, 30_000);
354
- });