-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.test.mjs
More file actions
150 lines (140 loc) · 6.4 KB
/
Copy pathserver.test.mjs
File metadata and controls
150 lines (140 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import assert from 'node:assert/strict';
import test from 'node:test';
import { createFakePluginHost, makeThreadResponse } from '@get-bb/plugin-sdk/testing';
import { createBrowserbasePlugin } from './server.ts';
const success = (data) => ({ content: [{ type: 'text', text: JSON.stringify({ success: true, data }) }] });
function fixture(options = {}) {
const calls = [];
const clients = [];
const host = createFakePluginHost({ pluginId: 'browserbase', settings: options.noKey ? {} : { apiKey: 'test-private-key' } });
const connect = async (key, signal) => {
signal.throwIfAborted();
const id = `session-${clients.length + 1}`;
const state = { id, key, active: 0, maximum: 0, closed: false };
clients.push(state);
return {
async callTool(params, schema, request) {
request.signal?.throwIfAborted();
state.maximum = Math.max(state.maximum, ++state.active);
calls.push({ id, name: params.name, args: params.arguments, key });
try {
await new Promise(resolve => setTimeout(resolve, 2));
if (options.handler) {
const result = await options.handler(params, id);
if (result) return result;
}
if (params.name === 'start') return success({ sessionId: id });
return success({ tool: params.name });
} finally { state.active--; }
},
async close() { state.closed = true; },
};
};
createBrowserbasePlugin(connect)(host.bb);
return {
...host, calls, clients,
call: (name, args = {}, threadId = 'thread-a') => host.harness.behavior.callAgentTool(`browserbase_${name}`, args, { threadId }),
};
}
test('missing credentials produces setup guidance without connecting', async () => {
const f = fixture({ noKey: true });
try {
const result = await f.call('start');
assert.equal(result.isError, true);
assert.match(result.content[0].text, /Settings/);
assert.equal(f.clients.length, 0);
} finally { await f.harness.lifecycle.dispose(); }
});
test('threads are isolated, repeated start reuses, and IDs are forwarded', async () => {
const f = fixture();
try {
await Promise.all([f.call('start'), f.call('start', {}, 'thread-b')]);
await f.call('start');
await f.call('navigate', { url: 'https://example.com' });
await f.call('extract', {}, 'thread-b');
assert.equal(f.clients.length, 2);
assert.equal(f.calls.filter(c => c.name === 'start').length, 2);
assert.equal(f.calls.find(c => c.name === 'navigate').args.sessionId, 'session-1');
assert.equal(f.calls.find(c => c.name === 'extract').args.sessionId, 'session-2');
await f.call('end');
assert.equal(f.clients[0].closed, true);
assert.equal(f.clients[1].closed, false);
} finally { await f.harness.lifecycle.dispose(); }
assert.equal(f.clients[1].closed, true);
});
test('same-thread calls serialize and invalid URLs or session overrides are rejected', async () => {
const f = fixture();
try {
await Promise.all([f.call('start'), f.call('start')]);
await Promise.all([f.call('observe', { instruction: 'Find search' }), f.call('act', { action: 'Click search' })]);
assert.equal(f.clients.length, 1);
assert.equal(f.clients[0].maximum, 1);
await assert.rejects(f.call('navigate', { url: 'file:///etc/passwd' }));
await assert.rejects(f.call('extract', { sessionId: 'someone-else' }));
} finally { await f.harness.lifecycle.dispose(); }
});
test('upstream errors stay errors and secrets are redacted', async () => {
const f = fixture({ handler: params => params.name === 'extract' ? {
isError: true, content: [{ type: 'text', text: 'Denied: test-private-key' }],
} : undefined });
try {
await f.call('start');
const result = await f.call('extract');
assert.equal(result.isError, true);
assert.doesNotMatch(JSON.stringify(result), /test-private-key/);
} finally { await f.harness.lifecycle.dispose(); }
});
test('failed actions are not retried and thrown URLs are not exposed', async () => {
const f = fixture({ handler: params => {
if (params.name === 'act') throw Error('https://example.com/?key=test-private-key');
} });
try {
await f.call('start');
const result = await f.call('act', { action: 'Click submit' });
assert.equal(result.isError, true);
assert.doesNotMatch(JSON.stringify(result), /test-private-key/);
assert.equal(f.calls.filter(c => c.name === 'act').length, 1);
} finally { await f.harness.lifecycle.dispose(); }
});
test('key rotation requires ending the old session with its original connection', async () => {
const f = fixture();
try {
await f.call('start');
await f.harness.behavior.setSettings({ apiKey: 'replacement-key' });
assert.equal((await f.call('start')).isError, true);
await f.call('end');
assert.equal(f.calls.find(c => c.name === 'end').key, 'test-private-key');
await f.call('start');
assert.equal(f.clients[1].key, 'replacement-key');
} finally { await f.harness.lifecycle.dispose(); }
});
test('archiving ends only the matching session', async () => {
const f = fixture();
try {
await f.call('start');
await f.call('start', {}, 'thread-b');
await f.harness.behavior.emitThreadEvent('thread.archived', { thread: makeThreadResponse({ id: 'thread-a' }) });
assert.equal(f.clients[0].closed, true);
assert.equal(f.clients[1].closed, false);
} finally { await f.harness.lifecycle.dispose(); }
});
test('unrecognized start response attempts cleanup rather than leaking a reusable default', async () => {
const f = fixture({ handler: params => params.name === 'start' ? success({ changedSchema: true }) : undefined });
try {
assert.equal((await f.call('start')).isError, true);
assert.equal(f.clients[0].closed, true);
assert.equal(f.calls.filter(c => c.name === 'end').length, 1);
assert.equal((await f.call('extract')).isError, true);
} finally { await f.harness.lifecycle.dispose(); }
});
test('structured session IDs and bounded extraction output work', async () => {
const f = fixture({ handler: (params, id) => params.name === 'start' ? {
content: [], structuredContent: { sessionId: id },
} : params.name === 'extract' ? { content: [{ type: 'text', text: 'x'.repeat(100_000) }] } : undefined });
try {
await f.call('start');
const result = await f.call('extract');
assert.ok(result.content[0].text.length < 50_000);
assert.match(result.content[0].text, /truncated/);
} finally { await f.harness.lifecycle.dispose(); }
});