view hg-web/e2e/app_e2e_test.js @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents c5129452493e
children
line wrap: on
line source

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawn, spawnSync } = require('node:child_process');
const { chromium } = require('playwright-core');

const BASE_URL = 'http://127.0.0.1:6970';
const RUNFILES = process.env.JS_BINARY__RUNFILES;
const WORKSPACE = process.env.JS_BINARY__WORKSPACE;
let hgCommand = 'hg';

function run(command, args, options = {}) {
  const result = spawnSync(command, args, {
    encoding: 'utf8',
    ...options,
  });
  if (result.status !== 0) {
    throw new Error(
      `${command} ${args.join(' ')} failed\n${result.stdout || ''}${result.stderr || ''}`,
    );
  }
  return result.stdout.trim();
}

function mercurialEnvironment(home) {
  return {
    ...process.env,
    HGPLAIN: '1',
    HGRCPATH: '',
    HOME: home,
  };
}

function stopProcess(child) {
  if (!child || child.exitCode !== null) return Promise.resolve();
  child.kill('SIGTERM');
  return new Promise(resolve => {
    const timer = setTimeout(() => {
      if (child.exitCode === null) child.kill('SIGKILL');
    }, 3000);
    child.once('exit', () => {
      clearTimeout(timer);
      resolve();
    });
  });
}

async function waitForHttp(url, child, logs) {
  const deadline = Date.now() + 15000;
  while (Date.now() < deadline) {
    if (child.exitCode !== null) {
      throw new Error(`Server exited early with ${child.exitCode}\n${logs.join('')}`);
    }
    try {
      const response = await fetch(url);
      if (response.ok) return;
    } catch {
      // Keep waiting for startup.
    }
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  throw new Error(`Timed out waiting for ${url}\n${logs.join('')}`);
}

function createFixtureRepository(root) {
  const options = { env: mercurialEnvironment(root) };
  run(hgCommand, ['init', root], options);
  fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
  fs.mkdirSync(path.join(root, 'src'), { recursive: true });
  fs.writeFileSync(
    path.join(root, 'README.md'),
    '# Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n',
  );
  fs.writeFileSync(path.join(root, 'docs', 'README.md'), '# Documentation\n\nNested README.\n');
  fs.writeFileSync(path.join(root, 'src', 'main.c'), 'int main(void) { return 0; }\n');
  fs.writeFileSync(
    path.join(root, 'BUILD'),
    'cc_library(\n    name = "fixture",\n    srcs = ["src/main.c"],\n)\n',
  );
  fs.writeFileSync(
    path.join(root, 'pixel.png'),
    Buffer.from(
      'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2nWQAAAAASUVORK5CYII=',
      'base64',
    ),
  );
  fs.writeFileSync(path.join(root, 'movie.mp4'), Buffer.from('00000018667479706d703432', 'hex'));
  fs.writeFileSync(path.join(root, 'sound.mp3'), Buffer.from('ID3'));
  fs.writeFileSync(path.join(root, 'document.pdf'), Buffer.from('%PDF-1.4\n%%EOF\n'));
  fs.writeFileSync(path.join(root, 'archive.bin'), Buffer.from([0, 1, 2, 3]));
  run(hgCommand, ['--repository', root, 'add'], options);
  run(hgCommand, ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Initial fixture'], options);
  const firstNode = run(hgCommand, ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options);

  fs.writeFileSync(
    path.join(root, 'README.md'),
    '# Updated Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n\nSecond revision.\n',
  );
  fs.writeFileSync(path.join(root, 'new-file.txt'), 'new file\n');
  run(hgCommand, ['add', 'new-file.txt'], { ...options, cwd: root });
  run(hgCommand, ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Update fixture'], options);
  const tipNode = run(hgCommand, ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options);
  return { firstNode, tipNode };
}

async function assertJson(pathname, status = 200) {
  const response = await fetch(`${BASE_URL}${pathname}`);
  const body = await response.text();
  assert.equal(response.status, status, `${pathname} status: ${body}`);
  return JSON.parse(body);
}

async function assertPageHasNoBrowserErrors(browser, pathname, assertion) {
  const page = await browser.newPage();
  const errors = [];
  page.on('pageerror', error => errors.push(`pageerror: ${error.stack || error.message}`));
  page.on('console', message => {
    if (message.type() === 'error') errors.push(`console: ${message.text()}`);
  });
  page.on('requestfailed', request => {
    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
  });
  page.on('response', response => {
    if (response.status() >= 400) {
      errors.push(`response: ${response.status()} ${response.url()}`);
    }
  });

  const response = await page.goto(`${BASE_URL}${pathname}`, { waitUntil: 'networkidle' });
  assert.equal(response.status(), 200, `${pathname} document status`);
  await assertion(page);
  if (errors.length > 0) {
    throw new Error(`${pathname} browser errors\n${errors.join('\n')}`);
  }
  await page.close();
}

async function main() {
  assert.ok(RUNFILES, 'rules_js runfiles path is required');
  assert.ok(WORKSPACE, 'rules_js workspace name is required');

  const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hg-web-e2e-'));
  const appLogs = [];
  const hgLogs = [];
  let hgServer;
  let appServer;
  let browser;

  try {
    const runfilesWorkspace = path.join(RUNFILES, WORKSPACE);
    const serverBinary = path.join(runfilesWorkspace, 'hg-web', 'hg_web_server');
    hgCommand = path.join(
      runfilesWorkspace,
      'third_party',
      'mercurial',
      'runtime',
      'bin',
      'hg',
    );
    const chromiumPath = path.resolve(process.env.CHROMIUM_PATH);

    assert.ok(fs.existsSync(serverBinary), `missing server binary: ${serverBinary}`);
    assert.ok(fs.existsSync(hgCommand), `missing bundled Mercurial: ${hgCommand}`);
    assert.ok(fs.existsSync(chromiumPath), `missing Chromium binary: ${chromiumPath}`);
    const { firstNode, tipNode } = createFixtureRepository(fixtureRoot);

    hgServer = spawn(
      hgCommand,
      [
        '--repository', fixtureRoot,
        'serve',
        '--address', '127.0.0.1',
        '--port', '4444',
        '--accesslog', '-',
        '--errorlog', '-',
      ],
      {
        env: mercurialEnvironment(fixtureRoot),
        stdio: ['ignore', 'pipe', 'pipe'],
      },
    );
    hgServer.stdout.on('data', chunk => hgLogs.push(chunk.toString()));
    hgServer.stderr.on('data', chunk => hgLogs.push(chunk.toString()));

    appServer = spawn(serverBinary, [], {
      cwd: runfilesWorkspace,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    appServer.stdout.on('data', chunk => appLogs.push(chunk.toString()));
    appServer.stderr.on('data', chunk => appLogs.push(chunk.toString()));

    await waitForHttp(`${BASE_URL}/`, appServer, appLogs);
    if (hgServer.exitCode !== null) {
      throw new Error(`Mercurial server exited early with ${hgServer.exitCode}\n${hgLogs.join('')}`);
    }
    await waitForHttp(`${BASE_URL}/api/repo/list`, hgServer, hgLogs);

    for (const pathname of ['/', '/directory', '/directory?path=docs', '/graph', `/changeset/${tipNode}`]) {
      const response = await fetch(`${BASE_URL}${pathname}`);
      assert.equal(response.status, 200, `${pathname} shell route`);
      assert.match(await response.text(), /<title>Zenbu Repository<\/title>/);
    }
    for (const pathname of ['/page.js', '/index.css', '/base.css', '/pencil_lines.png', '/panda.png']) {
      const response = await fetch(`${BASE_URL}${pathname}`);
      assert.equal(response.status, 200, `${pathname} static asset`);
      assert.ok((await response.arrayBuffer()).byteLength > 0, `${pathname} is non-empty`);
    }
    for (const pathname of ['/hg-web-background.jpg', '/pencil_texture.png']) {
      assert.equal((await fetch(`${BASE_URL}${pathname}`)).status, 404);
    }

    const rootList = await assertJson('/api/repo/list');
    assert.ok(rootList.directories.some(entry => entry.basename === 'docs'));
    assert.ok(rootList.files.some(entry => entry.basename === 'README.md'));

    const nestedList = await assertJson('/api/repo/list?path=docs');
    assert.ok(nestedList.files.some(entry => entry.basename === 'README.md'));

    const fileResponse = await fetch(`${BASE_URL}/api/repo/file?path=src%2Fmain.c`);
    assert.equal(fileResponse.status, 200);
    assert.match(await fileResponse.text(), /int main/);

    for (const [filename, contentType, disposition] of [
      ['pixel.png', 'image/png', 'inline'],
      ['movie.mp4', 'video/mp4', 'inline'],
      ['sound.mp3', 'audio/mpeg', 'inline'],
      ['document.pdf', 'application/pdf', 'inline'],
      ['archive.bin', 'application/octet-stream', 'attachment'],
    ]) {
      const response = await fetch(
        `${BASE_URL}/api/repo/file?path=${encodeURIComponent(filename)}`,
      );
      assert.equal(response.status, 200, `${filename} response`);
      assert.match(response.headers.get('content-type') || '', new RegExp(`^${contentType}`));
      assert.equal(response.headers.get('content-disposition'), disposition);
      assert.equal(response.headers.get('x-content-type-options'), 'nosniff');
      assert.ok((await response.arrayBuffer()).byteLength > 0);
    }
    const binaryResponse = await fetch(`${BASE_URL}/api/repo/file?path=archive.bin`);
    assert.deepEqual(
      Buffer.from(await binaryResponse.arrayBuffer()),
      Buffer.from([0, 1, 2, 3]),
    );

    const readmeResponse = await fetch(`${BASE_URL}/api/repo/readme?path=docs`);
    assert.equal(readmeResponse.status, 200);
    assert.match(await readmeResponse.text(), /Nested README/);
    assert.equal((await fetch(`${BASE_URL}/api/repo/readme?path=src`)).status, 204);

    const graph = await assertJson('/api/graph/tip?style=json');
    assert.equal(graph.node, tipNode);
    assert.ok(graph.changesets.length >= 2);

    const changeset = await assertJson(`/api/changeset/${tipNode}`);
    assert.equal(changeset.node, tipNode);
    assert.equal(changeset.desc, 'Update fixture');
    assert.ok(changeset.files.some(entry => entry.file === 'new-file.txt' && entry.status === 'added'));
    assert.ok(changeset.diff.length > 0);

    for (const pathname of [
      '/api/repo/list?path=..%2Fetc',
      '/api/repo/file?path=..%2FREADME.md',
      '/api/graph/not-a-node?style=json',
      '/api/changeset/not-a-node',
    ]) {
      const response = await fetch(`${BASE_URL}${pathname}`);
      assert.equal(response.status, 400, `${pathname} rejects invalid input`);
    }
    assert.equal((await fetch(`${BASE_URL}/missing-route`)).status, 404);

    const identify = run(
      hgCommand,
      ['identify', `${BASE_URL}/repo`],
      { env: mercurialEnvironment(fixtureRoot) },
    );
    assert.match(identify, new RegExp(`^${tipNode.slice(0, 12)}`));

    browser = await chromium.launch({
      executablePath: chromiumPath,
      headless: true,
      args: ['--no-sandbox'],
    });

    await assertPageHasNoBrowserErrors(browser, '/', async page => {
      await page.getByRole('heading', { name: 'Zenbu Repository' }).waitFor();
      await page.getByText('Recent Commits').waitFor();
      await page.getByText('Repository Files').waitFor();
      assert.equal(await page.evaluate(() => window.__hgWebXss), undefined);
      assert.equal(
        await page.locator('.graph-container').evaluate(
          element => getComputedStyle(element).backgroundImage,
        ),
        'none',
      );
      await page.locator('.theme-toggle').click();
    });

    await assertPageHasNoBrowserErrors(browser, '/directory', async page => {
      await page.getByText('Repository Files').waitFor();
      await page.getByRole('link', { name: 'README.md' }).first().click();
      await page.getByText('Updated Fixture Repository').waitFor();
      assert.equal(await page.evaluate(() => window.__hgWebXss), undefined);
      await page.keyboard.press('Escape');

      let delayedReadmeRequested = false;
      let markReadmeRequested;
      const readmeRequested = new Promise(resolve => {
        markReadmeRequested = resolve;
      });
      await page.route(/\/api\/repo\/readme\?path=/, async route => {
        delayedReadmeRequested = true;
        markReadmeRequested();
        await new Promise(resolve => setTimeout(resolve, 500));
        await route.continue();
      });
      await page.getByRole('link', { name: 'docs' }).click();
      await Promise.race([
        readmeRequested,
        new Promise((_, reject) => {
          setTimeout(() => reject(new Error('Timed out waiting for delayed README request')), 5000);
        }),
      ]);
      await page.getByRole('link', { name: 'root' }).click();
      await page.getByText('Updated Fixture Repository').waitFor();
      await page.waitForTimeout(600);
      assert.equal(await page.getByText('Documentation').count(), 0);
      assert.equal(delayedReadmeRequested, true);

      await page.getByRole('link', { name: 'pixel.png' }).click();
      const image = page.locator('.static-file-image');
      await image.waitFor();
      await image.evaluate(element => {
        const imageElement = element;
        if (imageElement.complete) return;
        return new Promise((resolve, reject) => {
          imageElement.addEventListener('load', resolve, { once: true });
          imageElement.addEventListener('error', reject, { once: true });
        });
      });
      assert.equal(await image.evaluate(element => element.naturalWidth), 1);
      await page.getByRole('dialog', { name: 'Preview pixel.png' }).waitFor();
      await page.keyboard.press('Escape');

      await page.getByRole('link', { name: 'BUILD' }).click();
      const buildCode = page.locator('code.language-python');
      await buildCode.waitFor();
      await page.getByText('cc_library').waitFor();
      assert.match(await buildCode.textContent(), /name = "fixture"/);
      await page.keyboard.press('Escape');

      await page.getByRole('link', { name: 'src' }).click();
      await page.getByRole('link', { name: 'main.c' }).click();
      await page.getByText('int main(void)').waitFor();
      await page.keyboard.press('Escape');
    });

    await assertPageHasNoBrowserErrors(browser, '/directory?path=docs', async page => {
      await page.getByText('Documentation').waitFor();
      await page.getByRole('link', { name: 'README.md' }).click();
      await page.getByText('Nested README.').waitFor();
    });

    await assertPageHasNoBrowserErrors(browser, '/graph', async page => {
      await page.getByText('Commit Graph').waitFor();
      await page.waitForFunction(() => new URL(window.location.href).searchParams.has('tip'));
      const graphUrl = page.url();
      const rows = page.locator('.graph-row');
      await rows.first().click();
      await page.waitForURL(/\/changeset\/[0-9a-f]+$/);
      await page.getByRole('heading', { name: 'Update fixture' }).waitFor();
      await page.getByRole('button', { name: 'Back', exact: true }).click();
      await page.waitForFunction(expected => window.location.href === expected, graphUrl);
      await page.getByText('Commit Graph').waitFor();
    });

    await assertPageHasNoBrowserErrors(browser, `/changeset/${tipNode}`, async page => {
      await page.getByRole('heading', { name: 'Update fixture' }).waitFor();
      await page.locator('.changeset-files code').filter({ hasText: 'new-file.txt' }).waitFor();
      await page.getByText('added', { exact: true }).waitFor();
      await page.getByRole('region', { name: 'Changeset diff' }).waitFor();
      await page.locator('.diff-column-headings').getByText('Before').first().waitFor();
      await page.locator('.diff-column-headings').getByText('After').first().waitFor();
      await page.locator('.diff-left.diff-remove').filter({ hasText: '# Fixture Repository' }).waitFor();
      await page.locator('.diff-right.diff-add').filter({ hasText: '# Updated Fixture Repository' }).waitFor();
      assert.equal(
        await page.locator('.diff-left.diff-context').filter({ hasText: '<script>' }).first().textContent(),
        '<script>window.__hgWebXss = true</script>',
      );
      assert.equal(
        await page.locator('.changeset-paper').evaluate(
          element => getComputedStyle(element).backgroundImage,
        ),
        'none',
      );
      await page.getByRole('button', { name: firstNode.slice(0, 12) }).click();
      await page.waitForFunction(
        expectedPath => window.location.pathname === expectedPath,
        `/changeset/${firstNode}`,
      );
      await page.getByRole('heading', { name: 'Initial fixture' }).waitFor();
      await page.getByRole('button', { name: 'Back', exact: true }).click();
      await page.getByRole('heading', { name: 'Update fixture' }).waitFor();
      await page.getByRole('button', { name: 'Back', exact: true }).click();
      await page.getByText('Commit Graph').waitFor();
    });

    await assertPageHasNoBrowserErrors(browser, '/changeset/not-a-node', async page => {
      await page.getByText('Recent Commits').waitFor();
    });

    await stopProcess(hgServer);
    const unavailable = await fetch(`${BASE_URL}/api/repo/list`);
    assert.equal(unavailable.status, 502);
    assert.equal((await fetch(`${BASE_URL}/`)).status, 200);
  } finally {
    if (browser) await browser.close();
    await stopProcess(appServer);
    await stopProcess(hgServer);
    fs.rmSync(fixtureRoot, { recursive: true, force: true });
  }
}

main().catch(error => {
  console.error(error.stack || error);
  process.exitCode = 1;
});