diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da5e35e3f3..decf3c12d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -134,6 +134,12 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 + with: + # The pin (packageManager: bun@1.3.6) lives in the workspace, not at the + # repo root, so without this setup-bun installs the latest release. + # Bun 1.4.2 hung in `bun ci` for the whole 20-minute budget on every + # suite (#517, #518) before a single test ran. + bun-version-file: trios/agent-server/package.json - name: Install dependencies run: bun ci diff --git a/trios/agent-server/.gitignore b/trios/agent-server/.gitignore index 5b07fe5f44..fade51cb84 100644 --- a/trios/agent-server/.gitignore +++ b/trios/agent-server/.gitignore @@ -53,7 +53,9 @@ bower_components build/Release # Dependency directories -node_modules/ +# No trailing slash: a symlinked node_modules is a file, and `node_modules/` +# let one pointing at a local Mac path be committed and hang CI's install. +node_modules jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) diff --git a/trios/agent-server/apps/server/node_modules b/trios/agent-server/apps/server/node_modules deleted file mode 120000 index d9dd43591a..0000000000 --- a/trios/agent-server/apps/server/node_modules +++ /dev/null @@ -1 +0,0 @@ -/Users/playom/queen-patches/work/browseros-deploy/trios/agent-server/apps/server/node_modules \ No newline at end of file diff --git a/trios/agent-server/apps/server/tests/__helpers__/utils.ts b/trios/agent-server/apps/server/tests/__helpers__/utils.ts index 7c684896b4..c808f6e245 100644 --- a/trios/agent-server/apps/server/tests/__helpers__/utils.ts +++ b/trios/agent-server/apps/server/tests/__helpers__/utils.ts @@ -14,22 +14,29 @@ export async function killProcessOnPort(port: number): Promise { try { console.log(`Finding process on port ${port}...`) - const pids = execSync(`lsof -ti :${port}`, { + // LISTEN only, and never this process. A bare `lsof -i :port` also lists + // every client still connected to the port - including this test process, + // whose CDP socket to the previous file's browser outlives it - and the + // SIGTERM that followed ended the whole server-tools run (exit 143). + const pids = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], - }).trim() + }) + .split('\n') + .map((pid) => pid.trim()) + .filter((pid) => pid !== '' && pid !== String(process.pid)) + .join(' ') if (pids) { - const pidList = pids.replace(/\n/g, ', ') - console.log(`Terminating process(es) ${pidList} on port ${port}...`) + console.log(`Terminating process(es) ${pids} on port ${port}...`) try { - execSync(`kill -15 ${pids.replace(/\n/g, ' ')}`, { + execSync(`kill -15 ${pids}`, { stdio: 'ignore', }) await new Promise((resolve) => setTimeout(resolve, 500)) } catch { - execSync(`kill -9 ${pids.replace(/\n/g, ' ')}`, { + execSync(`kill -9 ${pids}`, { stdio: 'ignore', }) } diff --git a/trios/agent-server/apps/server/tests/api/queen-salvage-guards.test.ts b/trios/agent-server/apps/server/tests/api/queen-salvage-guards.test.ts index a533efeecc..f80ba552ca 100644 --- a/trios/agent-server/apps/server/tests/api/queen-salvage-guards.test.ts +++ b/trios/agent-server/apps/server/tests/api/queen-salvage-guards.test.ts @@ -541,7 +541,9 @@ describe('the salvage commit', () => { expect(head).toContain('trios/docs/a/new.md') expect(head).not.toContain('trios/docs/z/old.md') rmSync(f.scratch, { recursive: true, force: true }) - }) + // Real git over 205 files: ~0.2 s here, but once over bun's 5 s default on + // a loaded CI runner (2026-10-01) while passing on the runs either side. + }, 30_000) }) // --------------------------------------------------------------------------- diff --git a/trios/agent-server/apps/server/tests/api/routes/route-guard.test.ts b/trios/agent-server/apps/server/tests/api/routes/route-guard.test.ts index f5950ac673..cee95fcc39 100644 --- a/trios/agent-server/apps/server/tests/api/routes/route-guard.test.ts +++ b/trios/agent-server/apps/server/tests/api/routes/route-guard.test.ts @@ -25,7 +25,7 @@ import { const source = readServerSource() const report = auditServer(source, DEFAULT_ALLOWLIST) -// Regression pin for the --no-allowlist run: exactly these seven mounts carry +// Regression pin for the --no-allowlist run: exactly these nine mounts carry // no guard today, each for a reason the comments beside the mount give. // RE-MEASURED 2026-09-13: /api/inngest joined. It is not a shell - it is the // Queen's scheduler endpoint - and it is unguarded on purpose: Inngest signs @@ -39,6 +39,7 @@ const report = auditServer(source, DEFAULT_ALLOWLIST) const EXPECTED_UNGUARDED_WITHOUT_ALLOWLIST = [ '/api/inngest', '/health', + '/queen/contributor-keys', '/queen/dashboard', '/queen/feed', '/queen/hq', @@ -84,7 +85,10 @@ describe('route-guard audit over src/api/server.ts', () => { // where a route named `public` belongs. Every other number here is // unchanged, which is the part worth stating: no guarded route quietly lost // its guard to make room for it. - expect(report.totalMounts).toBe(45) + // RE-MEASURED 2026-10-02: 45 became 46 with /queen/contributor-keys + // (#522), a server-to-server route behind its own capability token. It is + // allowlisted with that reason; no other number moved. + expect(report.totalMounts).toBe(46) expect(report.prefixGuardCount).toBe(18) expect(report.guardedSubAppCount).toBe(15) expect(report.publicReadCount).toBe(8) @@ -97,7 +101,7 @@ describe('route-guard audit over src/api/server.ts', () => { expect(report.entriesMissingReason).toEqual([]) }) - it('reports exactly the seven reasoned exceptions when the allowlist is dropped', () => { + it('reports exactly the nine reasoned exceptions when the allowlist is dropped', () => { // The classifier reports mounts in file order; the assertion is on the // exact set, so both sides are sorted before comparing. expect([...unguardedMounts(source, [])].sort()).toEqual( @@ -105,7 +109,7 @@ describe('route-guard audit over src/api/server.ts', () => { ) }) - it('splits the twenty-two /queen mounts into 8 public-read, 8 wrapper-guarded and 6 allowlisted shells', () => { + it('splits the twenty-three /queen mounts into 8 public-read, 8 wrapper-guarded and 7 allowlisted', () => { const queenMounts = classifyMounts(source).filter( (mount) => mount.path === '/queen' || mount.path.startsWith('/queen/'), ) @@ -127,7 +131,11 @@ describe('route-guard audit over src/api/server.ts', () => { // issue title, no worker text and no credential: only a key's INDEX ever // reaches the database, so there is nothing here a stranger could read that // the board does not already show. - expect(queenMounts.length).toBe(22) + // RE-MEASURED 2026-10-02: twenty-two became twenty-three. The seventh + // allowlisted mount is /queen/contributor-keys (#522), which serves data + // only to a caller holding QUEEN_CONTRIBUTOR_PROXY_TOKEN - its own guard, + // not the trusted-origin one, because its caller is a server. + expect(queenMounts.length).toBe(23) const counts: Record = { 'public-read': 0, @@ -144,7 +152,7 @@ describe('route-guard audit over src/api/server.ts', () => { 'public-read': 8, 'prefix-guard': 0, wrapper: 8, - unguarded: 6, + unguarded: 7, }) // Every unguarded /queen mount must be one of the allowlisted shells. diff --git a/trios/agent-server/apps/server/tests/tools/dom.test.ts b/trios/agent-server/apps/server/tests/tools/dom.test.ts index d487a8f2cb..53e9ccd686 100644 --- a/trios/agent-server/apps/server/tests/tools/dom.test.ts +++ b/trios/agent-server/apps/server/tests/tools/dom.test.ts @@ -400,10 +400,9 @@ describe('search_dom', () => { const newResult = await execute(new_page, { url: RICH_PAGE }) const pageId = pageIdOf(newResult) - const result = await execute(search_dom, { - page: pageId, - query: '.nav-link', - }) + // Same load race searchUntil exists for: CI found fewer than 3 once + // (2026-10-01) on a run where the identical query passed before. + const result = await searchUntil(execute, pageId, '.nav-link', 'Found 3') assert.ok(!result.isError, textOf(result)) const text = textOf(result) assert.ok(text.includes('Found 3'), 'Should find exactly 3 nav links') diff --git a/trios/agent-server/apps/server/tests/tools/navigation.test.ts b/trios/agent-server/apps/server/tests/tools/navigation.test.ts index f78b9942bf..b69331d2ed 100644 --- a/trios/agent-server/apps/server/tests/tools/navigation.test.ts +++ b/trios/agent-server/apps/server/tests/tools/navigation.test.ts @@ -157,7 +157,15 @@ describe('navigation tools', () => { it('wait_for finds text on page', async () => { await withBrowser(async ({ execute }) => { - const newResult = await execute(new_page, { url: 'https://example.com' }) + // The text arrives half a second after load, from the page itself, so + // this still exercises the waiting - without depending on the live + // https://example.com, which CI's browser never showed it on. + const page = `` + const newResult = await execute(new_page, { + url: `data:text/html,${encodeURIComponent(page)}`, + }) const pageId = structuredOf<{ pageId: number }>(newResult).pageId const waitResult = await execute(wait_for, { diff --git a/trios/agent-server/apps/server/tests/tools/observation.test.ts b/trios/agent-server/apps/server/tests/tools/observation.test.ts index 982296c644..373f2e41fa 100644 --- a/trios/agent-server/apps/server/tests/tools/observation.test.ts +++ b/trios/agent-server/apps/server/tests/tools/observation.test.ts @@ -157,8 +157,17 @@ describe('observation tools', () => { it('get_page_content returns markdown text', async () => { await withBrowser(async ({ execute }) => { - const newResult = await execute(new_page, { url: 'https://example.com' }) + // Constructed in place rather than fetched: reading https://example.com + // raced its own load (CI read it 57 ms after opening, before any text + // arrived), and the test is about extracting text, not about the network. + const newResult = await execute(new_page, { url: 'about:blank' }) const pageId = pageIdOf(newResult) + await execute(evaluate_script, { + page: pageId, + expression: `document.body.innerHTML = ${JSON.stringify( + '

Example Domain

This domain is for use in documentation examples.

', + )}`, + }) const contentResult = await execute(get_page_content, { page: pageId }) assert.ok(!contentResult.isError, textOf(contentResult)) diff --git a/trios/tools/route-guard-audit.mjs b/trios/tools/route-guard-audit.mjs index 0254f4feb8..5dda90e16d 100644 --- a/trios/tools/route-guard-audit.mjs +++ b/trios/tools/route-guard-audit.mjs @@ -105,6 +105,11 @@ export const DEFAULT_ALLOWLIST = [ reason: 'shell only — the operator page holds no state and no token; its numbers come from /queen/lease and its one action POSTs there with a bearer the reader supplies, so both stay guarded (comment at the mount)', }, + { + path: '/queen/contributor-keys', + reason: + 'own capability - a server-to-server route for the app render proxy, with no browser Origin; every request is refused unless its bearer equals QUEEN_CONTRIBUTOR_PROXY_TOKEN (at least 32 bytes, compared with timingSafeEqual) and it carries a verified x-queen-contributor-id, and the route is off while that token is unset (src/api/routes/queen-contributor-keys.ts, tests/api/queen-contributor-keys.test.ts)', + }, { path: '/api/inngest', reason: