notebooklm-api-client 0.1.155 → 0.1.156
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/package.json +6 -2
- package/test/login.test.ts +212 -0
- package/test/stubs/cloudflare-workers.ts +20 -0
- package/test/stubs/puppeteer.ts +18 -0
- package/test/worker.test.ts +202 -0
- package/vitest.config.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notebooklm-api-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.156",
|
|
4
4
|
"description": "NotebookLM API via Cloudflare Containers — on-demand Python sandbox that sleeps when idle",
|
|
5
5
|
"author": "vtempest <grokthiscontact@gmail.com>",
|
|
6
6
|
"license": "rights.institute/PROSPER",
|
|
@@ -14,14 +14,18 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"dev": "wrangler dev",
|
|
16
16
|
"deploy": "wrangler deploy",
|
|
17
|
-
"types": "wrangler types"
|
|
17
|
+
"types": "wrangler types",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:coverage": "vitest run --coverage"
|
|
18
20
|
},
|
|
19
21
|
"dependencies": {
|
|
20
22
|
"@cloudflare/puppeteer": "^1.0.0"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@cloudflare/workers-types": "^4.20241127.0",
|
|
26
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
24
27
|
"typescript": "^5.9.3",
|
|
28
|
+
"vitest": "^4.0.18",
|
|
25
29
|
"wrangler": "^4.0.0"
|
|
26
30
|
}
|
|
27
31
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import worker, { type Env } from '../src/index';
|
|
3
|
+
import { setLaunchImpl } from './stubs/puppeteer';
|
|
4
|
+
|
|
5
|
+
const API_TOKEN = 'test-token';
|
|
6
|
+
|
|
7
|
+
/** A puppeteer Page double recording the interactions handleLogin performs. */
|
|
8
|
+
function createPage(overrides: Record<string, unknown> = {}) {
|
|
9
|
+
return {
|
|
10
|
+
typed: [] as { selector: string; value: string }[],
|
|
11
|
+
clicked: [] as string[],
|
|
12
|
+
setUserAgent: vi.fn(async () => {}),
|
|
13
|
+
goto: vi.fn(async () => {}),
|
|
14
|
+
waitForSelector: vi.fn(async () => {}),
|
|
15
|
+
waitForNavigation: vi.fn(async () => {}),
|
|
16
|
+
type: vi.fn(async function (this: never, selector: string, value: string) {
|
|
17
|
+
(page.typed as { selector: string; value: string }[]).push({ selector, value });
|
|
18
|
+
}),
|
|
19
|
+
click: vi.fn(async (selector: string) => {
|
|
20
|
+
(page.clicked as string[]).push(selector);
|
|
21
|
+
}),
|
|
22
|
+
$: vi.fn(async () => null),
|
|
23
|
+
cookies: vi.fn(async () => [{ name: 'SID', value: 'abc' }]),
|
|
24
|
+
evaluate: vi.fn(async () => ({ token: 'xyz' })),
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let page: ReturnType<typeof createPage>;
|
|
30
|
+
|
|
31
|
+
function createEnv() {
|
|
32
|
+
const containerFetch = vi.fn(async () => new Response('{}', { status: 200 }));
|
|
33
|
+
const env = {
|
|
34
|
+
NOTEBOOK_RUNNER: { idFromName: vi.fn(() => 'id'), get: vi.fn(() => ({ fetch: containerFetch })) },
|
|
35
|
+
BROWSER: { binding: true } as unknown as Fetcher,
|
|
36
|
+
API_TOKEN,
|
|
37
|
+
GOOGLE_EMAIL: 'user@example.com',
|
|
38
|
+
GOOGLE_PASSWORD: 'hunter2',
|
|
39
|
+
} as unknown as Env;
|
|
40
|
+
return { env, containerFetch };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loginRequest(body: Record<string, unknown> = {}) {
|
|
44
|
+
return new Request('https://worker.example/', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { authorization: `Bearer ${API_TOKEN}`, 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({ action: 'login', ...body }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let close: ReturnType<typeof vi.fn>;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
page = createPage();
|
|
55
|
+
close = vi.fn(async () => {});
|
|
56
|
+
setLaunchImpl(async () => ({ newPage: async () => page, close }));
|
|
57
|
+
vi.useFakeTimers();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.useRealTimers();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** handleLogin awaits fixed delays; advance them while the promise is pending. */
|
|
65
|
+
async function runLogin(request: Request, env: Env) {
|
|
66
|
+
const promise = worker.fetch(request, env);
|
|
67
|
+
await vi.runAllTimersAsync();
|
|
68
|
+
return promise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('handleLogin', () => {
|
|
72
|
+
it('navigates to NotebookLM and signs in with the configured credentials', async () => {
|
|
73
|
+
const { env } = createEnv();
|
|
74
|
+
|
|
75
|
+
const res = await runLogin(loginRequest(), env);
|
|
76
|
+
|
|
77
|
+
expect(res.status).toBe(200);
|
|
78
|
+
expect(page.goto).toHaveBeenCalledWith('https://notebooklm.google.com', {
|
|
79
|
+
waitUntil: 'networkidle0',
|
|
80
|
+
});
|
|
81
|
+
expect(page.typed).toEqual([
|
|
82
|
+
{ selector: 'input[type="email"]', value: 'user@example.com' },
|
|
83
|
+
{ selector: 'input[type="password"]', value: 'hunter2' },
|
|
84
|
+
]);
|
|
85
|
+
expect(page.clicked).toEqual(['#identifierNext', '#passwordNext']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('sets a desktop user agent before navigating', async () => {
|
|
89
|
+
const { env } = createEnv();
|
|
90
|
+
|
|
91
|
+
await runLogin(loginRequest(), env);
|
|
92
|
+
|
|
93
|
+
expect(page.setUserAgent).toHaveBeenCalledWith(expect.stringContaining('Mozilla/5.0'));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('stores the harvested cookies and localStorage on the container', async () => {
|
|
97
|
+
const { env, containerFetch } = createEnv();
|
|
98
|
+
|
|
99
|
+
await runLogin(loginRequest(), env);
|
|
100
|
+
|
|
101
|
+
const [url, init] = containerFetch.mock.calls[0] as unknown as [string, RequestInit];
|
|
102
|
+
expect(url).toBe('http://container/store-auth');
|
|
103
|
+
const stored = JSON.parse(init.body as string);
|
|
104
|
+
expect(stored.cookies).toEqual([{ name: 'SID', value: 'abc' }]);
|
|
105
|
+
expect(stored.localStorage).toEqual({ token: 'xyz' });
|
|
106
|
+
expect(stored.timestamp).toEqual(expect.any(String));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('reports success with the cookie count', async () => {
|
|
110
|
+
const { env } = createEnv();
|
|
111
|
+
|
|
112
|
+
const res = await runLogin(loginRequest(), env);
|
|
113
|
+
|
|
114
|
+
expect(await res.json()).toEqual({
|
|
115
|
+
success: true,
|
|
116
|
+
message: 'Login complete, auth stored for container',
|
|
117
|
+
cookieCount: 1,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('skips the 2FA step when no security code is supplied', async () => {
|
|
122
|
+
const { env } = createEnv();
|
|
123
|
+
|
|
124
|
+
await runLogin(loginRequest(), env);
|
|
125
|
+
|
|
126
|
+
expect(page.$).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('enters a supplied security code and submits it', async () => {
|
|
130
|
+
const typedCode: string[] = [];
|
|
131
|
+
const codeInput = { type: vi.fn(async (value: string) => void typedCode.push(value)) };
|
|
132
|
+
const nextBtn = { click: vi.fn(async () => {}) };
|
|
133
|
+
page = createPage({
|
|
134
|
+
$: vi.fn(async (selector: string) =>
|
|
135
|
+
selector.includes('tel') ? codeInput : nextBtn
|
|
136
|
+
),
|
|
137
|
+
});
|
|
138
|
+
setLaunchImpl(async () => ({ newPage: async () => page, close }));
|
|
139
|
+
const { env } = createEnv();
|
|
140
|
+
|
|
141
|
+
await runLogin(loginRequest({ securityCode: '123456' }), env);
|
|
142
|
+
|
|
143
|
+
expect(typedCode).toEqual(['123456']);
|
|
144
|
+
expect(nextBtn.click).toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('tolerates the 2FA input not being present', async () => {
|
|
148
|
+
const { env } = createEnv();
|
|
149
|
+
|
|
150
|
+
const res = await runLogin(loginRequest({ securityCode: '123456' }), env);
|
|
151
|
+
|
|
152
|
+
expect(res.status).toBe(200);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns 500 with the failure detail when a step throws', async () => {
|
|
156
|
+
page = createPage({
|
|
157
|
+
waitForSelector: vi.fn(async () => {
|
|
158
|
+
throw new Error('email field never appeared');
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
setLaunchImpl(async () => ({ newPage: async () => page, close }));
|
|
162
|
+
const { env } = createEnv();
|
|
163
|
+
|
|
164
|
+
const res = await runLogin(loginRequest(), env);
|
|
165
|
+
|
|
166
|
+
expect(res.status).toBe(500);
|
|
167
|
+
expect(await res.json()).toEqual({
|
|
168
|
+
error: 'Login failed',
|
|
169
|
+
detail: 'email field never appeared',
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('reports a non-Error throw as an unknown error', async () => {
|
|
174
|
+
page = createPage({
|
|
175
|
+
waitForSelector: vi.fn(async () => {
|
|
176
|
+
throw 'just a string';
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
setLaunchImpl(async () => ({ newPage: async () => page, close }));
|
|
180
|
+
const { env } = createEnv();
|
|
181
|
+
|
|
182
|
+
const res = await runLogin(loginRequest(), env);
|
|
183
|
+
|
|
184
|
+
expect(await res.json()).toEqual({ error: 'Login failed', detail: 'Unknown error' });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('closes the browser on both the success and failure paths', async () => {
|
|
188
|
+
const { env } = createEnv();
|
|
189
|
+
await runLogin(loginRequest(), env);
|
|
190
|
+
expect(close).toHaveBeenCalledTimes(1);
|
|
191
|
+
|
|
192
|
+
page = createPage({
|
|
193
|
+
goto: vi.fn(async () => {
|
|
194
|
+
throw new Error('nope');
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
const secondClose = vi.fn(async () => {});
|
|
198
|
+
setLaunchImpl(async () => ({ newPage: async () => page, close: secondClose }));
|
|
199
|
+
|
|
200
|
+
await runLogin(loginRequest(), createEnv().env);
|
|
201
|
+
expect(secondClose).toHaveBeenCalledTimes(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('never reaches the container run endpoint for a login action', async () => {
|
|
205
|
+
const { env, containerFetch } = createEnv();
|
|
206
|
+
|
|
207
|
+
await runLogin(loginRequest(), env);
|
|
208
|
+
|
|
209
|
+
const urls = containerFetch.mock.calls.map((call) => call[0]);
|
|
210
|
+
expect(urls).not.toContain('http://container/run');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stand-in for the `cloudflare:workers` runtime module, which only resolves
|
|
3
|
+
* inside workerd. `NotebookRunner` extends `Container` purely to inherit
|
|
4
|
+
* lifecycle plumbing, so an empty base class is enough to import the worker.
|
|
5
|
+
*/
|
|
6
|
+
export class Container {
|
|
7
|
+
ctx: {
|
|
8
|
+
container: { start: (options: unknown) => void };
|
|
9
|
+
storage: { get: <T>(key: string) => Promise<T | undefined> };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.ctx = {
|
|
14
|
+
container: { start: () => {} },
|
|
15
|
+
storage: { get: async () => undefined },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class DurableObject {}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stand-in for `@cloudflare/puppeteer`. Tests swap in their own page/browser
|
|
5
|
+
* behaviour via `setLaunchImpl`; the default is a browser that never resolves
|
|
6
|
+
* a selector, which is the "login page did not appear" path.
|
|
7
|
+
*/
|
|
8
|
+
let launchImpl: (binding: unknown) => Promise<unknown> = async () => {
|
|
9
|
+
throw new Error('launch not stubbed');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function setLaunchImpl(impl: (binding: unknown) => Promise<unknown>) {
|
|
13
|
+
launchImpl = impl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const launch = vi.fn((binding: unknown) => launchImpl(binding));
|
|
17
|
+
|
|
18
|
+
export default { launch };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import worker, { NotebookRunner, type Env, type NotebookJob } from '../src/index';
|
|
3
|
+
|
|
4
|
+
const API_TOKEN = 'test-token';
|
|
5
|
+
|
|
6
|
+
function createEnv(overrides: Partial<Env> = {}) {
|
|
7
|
+
const containerFetch = vi.fn(
|
|
8
|
+
async () => new Response(JSON.stringify({ ok: true }), { status: 200 })
|
|
9
|
+
);
|
|
10
|
+
const stub = { fetch: containerFetch };
|
|
11
|
+
const idFromName = vi.fn(() => 'do-id');
|
|
12
|
+
const get = vi.fn(() => stub);
|
|
13
|
+
|
|
14
|
+
const env = {
|
|
15
|
+
NOTEBOOK_RUNNER: { idFromName, get },
|
|
16
|
+
BROWSER: {} as Fetcher,
|
|
17
|
+
API_TOKEN,
|
|
18
|
+
GOOGLE_EMAIL: 'user@example.com',
|
|
19
|
+
GOOGLE_PASSWORD: 'hunter2',
|
|
20
|
+
...overrides,
|
|
21
|
+
} as unknown as Env;
|
|
22
|
+
|
|
23
|
+
return { env, containerFetch, idFromName, get };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function post(body: unknown, { token = API_TOKEN }: { token?: string | null } = {}) {
|
|
27
|
+
const headers: Record<string, string> = { 'content-type': 'application/json' };
|
|
28
|
+
if (token !== null) headers.authorization = `Bearer ${token}`;
|
|
29
|
+
return new Request('https://worker.example/', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers,
|
|
32
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('worker fetch: CORS preflight', () => {
|
|
37
|
+
it('answers OPTIONS with permissive CORS headers and no body', async () => {
|
|
38
|
+
const { env } = createEnv();
|
|
39
|
+
|
|
40
|
+
const res = await worker.fetch(new Request('https://worker.example/', { method: 'OPTIONS' }), env);
|
|
41
|
+
|
|
42
|
+
expect(res.status).toBe(200);
|
|
43
|
+
expect(res.headers.get('access-control-allow-origin')).toBe('*');
|
|
44
|
+
expect(res.headers.get('access-control-allow-methods')).toBe('POST, OPTIONS');
|
|
45
|
+
expect(res.headers.get('access-control-allow-headers')).toBe('content-type, authorization');
|
|
46
|
+
expect(await res.text()).toBe('');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not require authorization for the preflight', async () => {
|
|
50
|
+
const { env, get } = createEnv();
|
|
51
|
+
|
|
52
|
+
const res = await worker.fetch(new Request('https://worker.example/', { method: 'OPTIONS' }), env);
|
|
53
|
+
|
|
54
|
+
expect(res.status).toBe(200);
|
|
55
|
+
expect(get).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('worker fetch: method and auth guards', () => {
|
|
60
|
+
it.each(['GET', 'PUT', 'DELETE'])('rejects %s with 405', async (method) => {
|
|
61
|
+
const { env } = createEnv();
|
|
62
|
+
|
|
63
|
+
const res = await worker.fetch(new Request('https://worker.example/', { method }), env);
|
|
64
|
+
|
|
65
|
+
expect(res.status).toBe(405);
|
|
66
|
+
expect(await res.json()).toEqual({ error: 'Method not allowed' });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects a missing authorization header with 401', async () => {
|
|
70
|
+
const { env } = createEnv();
|
|
71
|
+
|
|
72
|
+
const res = await worker.fetch(post({ action: 'list' }, { token: null }), env);
|
|
73
|
+
|
|
74
|
+
expect(res.status).toBe(401);
|
|
75
|
+
expect(await res.json()).toEqual({ error: 'Unauthorized' });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects a wrong bearer token with 401', async () => {
|
|
79
|
+
const { env } = createEnv();
|
|
80
|
+
|
|
81
|
+
const res = await worker.fetch(post({ action: 'list' }, { token: 'nope' }), env);
|
|
82
|
+
|
|
83
|
+
expect(res.status).toBe(401);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('never reaches the container when unauthorized', async () => {
|
|
87
|
+
const { env, containerFetch } = createEnv();
|
|
88
|
+
|
|
89
|
+
await worker.fetch(post({ action: 'list' }, { token: 'nope' }), env);
|
|
90
|
+
|
|
91
|
+
expect(containerFetch).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('worker fetch: request body validation', () => {
|
|
96
|
+
it('rejects a malformed JSON body with 400', async () => {
|
|
97
|
+
const { env } = createEnv();
|
|
98
|
+
|
|
99
|
+
const res = await worker.fetch(post('{not json'), env);
|
|
100
|
+
|
|
101
|
+
expect(res.status).toBe(400);
|
|
102
|
+
expect(await res.json()).toEqual({ error: 'Invalid JSON body' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects a body with no action with 400', async () => {
|
|
106
|
+
const { env } = createEnv();
|
|
107
|
+
|
|
108
|
+
const res = await worker.fetch(post({ notebookId: 'abc' }), env);
|
|
109
|
+
|
|
110
|
+
expect(res.status).toBe(400);
|
|
111
|
+
expect(await res.json()).toEqual({ error: 'Missing action field' });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('worker fetch: container proxying', () => {
|
|
116
|
+
const actions: NotebookJob['action'][] = ['create', 'ask', 'summarize', 'list', 'delete'];
|
|
117
|
+
|
|
118
|
+
it.each(actions)('forwards the %s job to the container', async (action) => {
|
|
119
|
+
const { env, containerFetch, idFromName } = createEnv();
|
|
120
|
+
const job = { action, notebookId: 'nb-1' };
|
|
121
|
+
|
|
122
|
+
await worker.fetch(post(job), env);
|
|
123
|
+
|
|
124
|
+
expect(idFromName).toHaveBeenCalledWith('default');
|
|
125
|
+
const [url, init] = containerFetch.mock.calls[0] as unknown as [string, RequestInit];
|
|
126
|
+
expect(url).toBe('http://container/run');
|
|
127
|
+
expect(init.method).toBe('POST');
|
|
128
|
+
expect(JSON.parse(init.body as string)).toEqual(job);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('passes the container response body and status straight through', async () => {
|
|
132
|
+
const { env, containerFetch } = createEnv();
|
|
133
|
+
containerFetch.mockResolvedValue(
|
|
134
|
+
new Response(JSON.stringify({ notebookId: 'nb-9' }), { status: 201 })
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const res = await worker.fetch(post({ action: 'create' }), env);
|
|
138
|
+
|
|
139
|
+
expect(res.status).toBe(201);
|
|
140
|
+
expect(await res.json()).toEqual({ notebookId: 'nb-9' });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('propagates a container error status', async () => {
|
|
144
|
+
const { env, containerFetch } = createEnv();
|
|
145
|
+
containerFetch.mockResolvedValue(new Response(JSON.stringify({ error: 'boom' }), { status: 500 }));
|
|
146
|
+
|
|
147
|
+
const res = await worker.fetch(post({ action: 'ask', prompt: 'why' }), env);
|
|
148
|
+
|
|
149
|
+
expect(res.status).toBe(500);
|
|
150
|
+
expect(await res.json()).toEqual({ error: 'boom' });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('always answers with JSON and CORS headers', async () => {
|
|
154
|
+
const { env } = createEnv();
|
|
155
|
+
|
|
156
|
+
const res = await worker.fetch(post({ action: 'list' }), env);
|
|
157
|
+
|
|
158
|
+
expect(res.headers.get('content-type')).toBe('application/json');
|
|
159
|
+
expect(res.headers.get('access-control-allow-origin')).toBe('*');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('NotebookRunner', () => {
|
|
164
|
+
it('is configured for port 8080 with a 5 minute idle timeout', () => {
|
|
165
|
+
const runner = new NotebookRunner();
|
|
166
|
+
|
|
167
|
+
expect(runner.defaultPort).toBe(8080);
|
|
168
|
+
expect(runner.sleepAfter).toBe('5m');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('starts the container with stored auth and internet enabled', async () => {
|
|
172
|
+
const runner = new NotebookRunner();
|
|
173
|
+
const start = vi.fn();
|
|
174
|
+
runner.ctx = {
|
|
175
|
+
container: { start },
|
|
176
|
+
storage: { get: vi.fn(async () => 'stored-auth-json') },
|
|
177
|
+
} as never;
|
|
178
|
+
|
|
179
|
+
await runner.onStart();
|
|
180
|
+
|
|
181
|
+
expect(start).toHaveBeenCalledWith({
|
|
182
|
+
env: { NOTEBOOKLM_AUTH_JSON: 'stored-auth-json' },
|
|
183
|
+
enableInternet: true,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('passes an empty auth string when nothing is stored yet', async () => {
|
|
188
|
+
const runner = new NotebookRunner();
|
|
189
|
+
const start = vi.fn();
|
|
190
|
+
runner.ctx = {
|
|
191
|
+
container: { start },
|
|
192
|
+
storage: { get: vi.fn(async () => undefined) },
|
|
193
|
+
} as never;
|
|
194
|
+
|
|
195
|
+
await runner.onStart();
|
|
196
|
+
|
|
197
|
+
expect(start).toHaveBeenCalledWith({
|
|
198
|
+
env: { NOTEBOOKLM_AUTH_JSON: '' },
|
|
199
|
+
enableInternet: true,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { defineConfig } from 'vitest/config';
|
|
4
|
+
|
|
5
|
+
const dir = fileURLToPath(new URL('.', import.meta.url));
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
// Both of these only resolve inside workerd; test/stubs supplies the
|
|
11
|
+
// small slice of their surface that src/index.ts actually touches.
|
|
12
|
+
'cloudflare:workers': resolve(dir, 'test/stubs/cloudflare-workers.ts'),
|
|
13
|
+
'@cloudflare/puppeteer': resolve(dir, 'test/stubs/puppeteer.ts'),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
test: {
|
|
17
|
+
globals: true,
|
|
18
|
+
environment: 'node',
|
|
19
|
+
include: ['test/**/*.test.ts'],
|
|
20
|
+
restoreMocks: true,
|
|
21
|
+
coverage: {
|
|
22
|
+
provider: 'v8',
|
|
23
|
+
reporter: ['text', 'json', 'html', 'lcov'],
|
|
24
|
+
include: ['src/**/*.ts'],
|
|
25
|
+
exclude: ['node_modules/**', 'dist/**', '**/*.test.ts'],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|