__tests__/ws-owner.test.js
Rama main · réplica de lectura del repositorio original.
/**
* THE OWNER OF THE BROWSER COULD NOT WATCH HER OWN BROWSER.
*
* Carla pressed Watch on a running job and got a black square and, in the console, a refused
* WebSocket — while Herald, polling the REST API, showed the same job progressing. Two socket doors,
* one cause: both authorisers answered the cookie sign-in with { owner: username } and nothing else,
* and both handlers then insisted the job or the session be owned by that exact name. Every walk an
* organ dispatches is owned by the KEY's owner ("gb_42c3f"), so the job socket returned 404 (Watch
* failed) and the live-frames socket returned 404 (the viewport stayed black).
*
* The REST list was fixed for this in v194 — `req.client.console` sees listAll() — and the sockets
* never got it. Same door, same rule.
*/
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const read = (p) => readFileSync(fileURLToPath(new URL(p, import.meta.url)), 'utf8');
const server = read('../src/server.js');
const agentWs = read('../src/agent-ws.js');
const live = read('../src/live.js');
describe('the socket doors say who is at them', () => {
it('the cookie sign-in is the console, on the job socket', () => {
expect(server).toMatch(/const wsAuth = \(req, url\) => \{\s*\n\s*const who = accounts\.verifyToken\(accounts\.readCookie\(req\)\);\s*\n\s*if \(who\) return \{ owner: who\.username, console: true \};/);
});
it('and on the live-frames socket', () => {
const liveAuth = server.slice(server.indexOf("require('./live').attach("));
expect(liveAuth).toMatch(/if \(who\) return \{ owner: who\.username, console: true \};/);
});
/* A program is a program: a key still identifies only as its owner. */
it('a key is never mistaken for the console', () => {
expect(server).toMatch(/return found \? \{ owner: found\.owner \} : null;/);
expect(server).not.toMatch(/owner: found\.owner, console/);
});
});
describe('the console may watch anything in this browser; a key only its own', () => {
it('following a job', () => {
expect(agentWs).toMatch(/if \(!job \|\| \(job\.owner !== who\.owner && !who\.console\)\) \{/);
});
it('streaming a session', () => {
expect(live).toMatch(/if \(!session \|\| \(session\.owner !== who\.owner && !who\.console\)\) \{/);
});
/* Still authorised BEFORE the socket is accepted — nothing here moved the check after handleUpgrade. */
it('and both are still refused before the socket is opened', () => {
for (const src of [agentWs, live]) {
const check = src.indexOf('&& !who.console');
const accept = src.indexOf('wss.handleUpgrade(');
expect(check).toBeGreaterThan(0);
expect(check).toBeLessThan(accept);
}
});
});
/*
* WATCHING A JOB SHOWED THE WRONG ROLE.
*
* Carla watched two of Herald's replies and read 'Lead scout' on the LinkedIn one and 'Reddit demand'
* on the Reddit one, with those roles' descriptions above the trace, and concluded — entirely
* reasonably — that the desk was dispatching replies with research roles. It was not: both ran on
* herald-reply-linkedin and herald-reply-reddit. renderJob never touched the picker, so it kept the
* OWNER's own last choice out of localStorage and described that instead.
*/
describe('the role on screen belongs to the job being watched', () => {
const ui = read('../public/js/console.js');
it('rendering a job sets the picker from the job', () => {
expect(ui).toMatch(/function renderJob\(j\) \{[\s\S]{0,40}?showJobRole\(j\);/);
expect(ui).toMatch(/sel\.value = j\.role;/);
});
/* An organ's role is not in the owner's picker; naming it beats showing somebody else's. */
it('a role the picker does not list is still named, not swapped for another', () => {
expect(ui).toMatch(/if \(!has\) \{ const o = document\.createElement\('option'\); o\.value = j\.role;/);
});
it('and it is locked while the job runs, because changing it then changes nothing', () => {
expect(ui).toMatch(/sel\.disabled = !!live;/);
expect(ui).toMatch(/it cannot be changed mid-run/);
});
/* The owner's own choice must survive watching somebody else's work. */
it('a job’s role never overwrites what the owner chose', () => {
expect(ui).toMatch(/if \(!\$\('roleSel'\)\.disabled\) localStorage\.setItem\('gb_role'/);
});
});
/*
* AND IT WENT BLACK AGAIN, for a different reason each half of which was invisible.
*
* The reconnect: adoptAgentSession returned early whenever the session id had not changed. Right
* while the picture is live, wrong the moment it is not — leaving the console for Accounts and
* pressing Watch again on the same job re-enters with `session` already set, so nothing was adopted,
* nothing reconnected, and the dead canvas from the last visit stayed on screen until a reload.
*
* The silence: every refusal on both sockets was a bare status written to a socket that was then
* destroyed. Nothing was logged and nothing was shown, so a wrong owner, an expired session and a
* job that has no browser yet all looked identical — a black rectangle. Watching a job is the one
* feature whose whole purpose is to show what is happening; it must never fail without saying so.
*/
describe('and when it cannot show you, it says so', () => {
const console_ = read('../public/js/console.js');
it('watching the same job twice reconnects rather than trusting the id', () => {
const at = console_.indexOf('function adoptAgentSession');
const body = console_.slice(at, at + 900);
expect(body).toMatch(/const connected = ws && \(ws\.readyState === WebSocket\.OPEN \|\| ws\.readyState === WebSocket\.CONNECTING\)/);
expect(body).toMatch(/if \(sessionId === session && connected\) return;/);
/* The old guard is gone: an unchanged id must no longer be enough to skip connecting. */
expect(body).not.toMatch(/if \(!sessionId \|\| sessionId === session\) return;/);
});
it('a socket that closes without ever answering is reported', () => {
const at = console_.indexOf('function followJob');
/* The whole function, not a window: it grew when the snapshot handler was made safe. */
const body = console_.slice(at, console_.indexOf('\nfunction ', at + 10));
expect(body).toMatch(/let heard = false;/);
expect(body).toMatch(/heard = true;/);
expect(body).toMatch(/if \(heard\) return;/);
/*
* AND ONLY THE CURRENT SOCKET MAY SPEAK. This reported a refusal whenever a socket closed
* without a snapshot — including one this function had just replaced on purpose. Measured: two
* accepts in the browser's own log, and "it refused the connection" on screen. A wrong
* explanation is worse than none, because it sends somebody to fix the wrong thing.
*/
expect(body).toMatch(/ajws\.__replaced = true;/);
expect(body).toMatch(/if \(mine\.__replaced\) return;/);
/* And it carries the close code, which separates a dead connection from a clean close. */
expect(body).toMatch(/code \$\{ev && ev\.code\}/);
});
/* A running job with no browser is a real state — queued, waiting — not a broken viewer. */
it('and a job that has no browser yet is explained, not shown as black', () => {
expect(console_).toMatch(/this job has no browser yet/);
});
it('the job socket writes down which refusal it was', () => {
expect(agentWs).toMatch(/\[agent-ws\] refused: not signed in/);
expect(agentWs).toMatch(/belongs to \$\{job\.owner\}, not \$\{who\.owner\}/);
expect(agentWs).toMatch(/\[agent-ws\] \$\{who\.owner\} is watching job/);
});
it('and so does the live one, including a session that has expired', () => {
expect(live).toMatch(/\[live\] refused: not signed in/);
expect(live).toMatch(/it may have expired/);
expect(live).toMatch(/\[live\] \$\{who\.owner\} is watching/);
});
});
/*
* A LINK STRAIGHT INTO ONE WALK.
*
* Creating a Facebook page fills a real form and clicks through a real flow for several minutes.
* Herald showed a progress bar while that happened, which tells somebody nothing at exactly the
* moment they most want to see what is going on — both to trust it and to step in.
*
* So an organ can send them here with #job=<id>, and the console opens on that walk already watching
* it. Landing them on the account board and asking them to find the right row would be the same as
* not linking at all.
*/
describe('an organ can link straight to the walk it started', () => {
const dash = read('../public/js/dashboard.js');
it('a job in the hash opens the live view on it', () => {
expect(dash).toMatch(/const m = \(location\.hash \|\| ''\)\.match\(\/\[#&\]job=\(\[\^&\]\+\)\/\);/);
expect(dash).toMatch(/if \(typeof followJob === 'function'\) followJob\(id\);/);
/* The app view, not the board — the board is what the link exists to skip. */
expect(dash).toMatch(/deepLinked = true;\s*\n\s*const id = decodeURIComponent\(m\[1\]\);\s*\n\s*showApp\(\);/);
});
/* Once. Otherwise going back to the board and forward again yanks the person into it again. */
it('and it is consumed once, not on every return to the board', () => {
expect(dash).toMatch(/let deepLinked = false;/);
expect(dash).toMatch(/if \(deepLinked\) return;/);
});
it('and it runs after the board is drawn, since it takes you off it', () => {
const at = dash.indexOf('new MutationObserver');
const body = dash.slice(at, at + 700);
expect(body).toMatch(/refresh\(\);\s*\n[^\n]*\n\s*followDeepLink\(\);/);
});
});
/*
* THE BLACK VIEW BEHIND A LINK THAT CONNECTED FINE.
*
* Carla followed the watch link into a running page set-up. The server logged the job socket
* accepted — "is watching job … (running)" — and the frames socket was never attempted at all,
* neither accepted nor refused. So the failure was not in either door; it was between them.
*
* The snapshot handler drew the steps and THEN connected the live view, in one sequence. A browser
* swallows an exception thrown inside a socket handler, so anything that went wrong while drawing
* killed the handler before it ever reached the connect — and produced a black rectangle with
* nothing wrong anywhere on the server to explain it.
*
* The frames are what the link exists for. Drawing the steps is the part that can fail.
*/
describe('the live view does not depend on the transcript drawing', () => {
const console_ = read('../public/js/console.js');
const at = console_.indexOf("if (m.type === 'snapshot')");
const body = console_.slice(at, at + 1800);
it('the session is adopted BEFORE the steps are drawn', () => {
const adopt = body.indexOf('adoptAgentSession(m.job.sessionId');
const render = body.indexOf('renderJob(m.job)');
expect(adopt).toBeGreaterThan(0);
expect(render).toBeGreaterThan(adopt);
});
it('and a failure while drawing cannot take the picture down with it', () => {
expect(body).toMatch(/try \{ renderJob\(m\.job\); \}/);
expect(body).toMatch(/catch \(e\) \{ agentSay\('the transcript could not be drawn: '/);
expect(body).toMatch(/the live view above is unaffected/);
});
/* And it says so, rather than failing silently the way the browser does with a thrown handler. */
it('and it is reported instead of swallowed', () => {
expect(body).toMatch(/agentSay\('the transcript could not be drawn/);
});
});