rpc.test.js
1 import { describe, expect, it, vi } from 'vitest'; 2 import { AuthRequiredError } from '@jackwener/opencli/errors'; 3 import { buildNotebooklmRpcBody, extractNotebooklmRpcResult, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, } from './rpc.js'; 4 describe('notebooklm rpc transport', () => { 5 it('extracts auth tokens from the page html via page evaluation', async () => { 6 const page = { 7 evaluate: vi.fn(async (script) => { 8 expect(script).toContain('document.documentElement.innerHTML'); 9 return { 10 html: '<html>"SNlM0e":"csrf-123","FdrFJe":"sess-456"</html>', 11 sourcePath: '/', 12 }; 13 }), 14 }; 15 await expect(getNotebooklmPageAuth(page)).resolves.toEqual({ 16 csrfToken: 'csrf-123', 17 sessionId: 'sess-456', 18 sourcePath: '/', 19 }); 20 expect(page.evaluate).toHaveBeenCalledTimes(1); 21 }); 22 it('falls back to WIZ_global_data tokens when html regex data is missing', async () => { 23 const page = { 24 evaluate: vi.fn(async () => ({ 25 html: '<html><body>NotebookLM</body></html>', 26 sourcePath: '/notebook/nb-demo', 27 readyState: 'complete', 28 csrfToken: 'csrf-wiz', 29 sessionId: 'sess-wiz', 30 })), 31 }; 32 await expect(getNotebooklmPageAuth(page)).resolves.toEqual({ 33 csrfToken: 'csrf-wiz', 34 sessionId: 'sess-wiz', 35 sourcePath: '/notebook/nb-demo', 36 }); 37 }); 38 it('retries token extraction once when the first probe returns no tokens', async () => { 39 const page = { 40 evaluate: vi.fn() 41 .mockResolvedValueOnce({ 42 html: '<html><body>Loading…</body></html>', 43 sourcePath: '/notebook/nb-demo', 44 readyState: 'interactive', 45 csrfToken: '', 46 sessionId: '', 47 }) 48 .mockResolvedValueOnce({ 49 html: '<html>"SNlM0e":"csrf-123","FdrFJe":"sess-456"</html>', 50 sourcePath: '/notebook/nb-demo', 51 readyState: 'complete', 52 csrfToken: '', 53 sessionId: '', 54 }), 55 wait: vi.fn(async () => undefined), 56 }; 57 await expect(getNotebooklmPageAuth(page)).resolves.toEqual({ 58 csrfToken: 'csrf-123', 59 sessionId: 'sess-456', 60 sourcePath: '/notebook/nb-demo', 61 }); 62 expect(page.evaluate).toHaveBeenCalledTimes(2); 63 }); 64 it('builds the rpc body with the expected notebooklm payload shape', () => { 65 const body = buildNotebooklmRpcBody('wXbhsf', [null, 1, null, [2]], 'csrf-123'); 66 expect(body).toContain('f.req='); 67 expect(body).toContain('at=csrf-123'); 68 expect(body.endsWith('&')).toBe(true); 69 expect(decodeURIComponent(body)).toContain('"[null,1,null,[2]]"'); 70 }); 71 it('parses chunked batchexecute responses into json chunks', () => { 72 const raw = `)]}'\n107\n[["wrb.fr","wXbhsf","[[[\\\"Notebook One\\\",null,\\\"nb1\\\",null,null,[null,false,null,null,null,[1704067200]]]]]"]]`; 73 const chunks = parseNotebooklmChunkedResponse(raw); 74 expect(chunks).toHaveLength(1); 75 expect(Array.isArray(chunks[0])).toBe(true); 76 expect(chunks[0]).toEqual([ 77 [ 78 'wrb.fr', 79 'wXbhsf', 80 '[[["Notebook One",null,"nb1",null,null,[null,false,null,null,null,[1704067200]]]]]', 81 ], 82 ]); 83 }); 84 it('extracts the rpc payload from wrb.fr responses', () => { 85 const raw = `)]}'\n107\n[["wrb.fr","wXbhsf","[[[\\\"Notebook One\\\",null,\\\"nb1\\\",null,null,[null,false,null,null,null,[1704067200]]]]]"]]`; 86 const result = extractNotebooklmRpcResult(raw, 'wXbhsf'); 87 expect(result).toEqual([ 88 [ 89 ['Notebook One', null, 'nb1', null, null, [null, false, null, null, null, [1704067200]]], 90 ], 91 ]); 92 }); 93 it('classifies auth errors as AuthRequiredError', () => { 94 const raw = `)]}'\n25\n[["er",null,null,null,null,401,"generic"]]`; 95 expect(() => extractNotebooklmRpcResult(raw, 'wXbhsf')).toThrow(AuthRequiredError); 96 try { 97 extractNotebooklmRpcResult(raw, 'wXbhsf'); 98 } 99 catch (error) { 100 expect(error).toBeInstanceOf(AuthRequiredError); 101 expect(error.domain).toBe('notebooklm.google.com'); 102 expect(error.code).toBe('AUTH_REQUIRED'); 103 } 104 }); 105 });