/ clis / notebooklm / rpc.test.js
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  });