Ctrl K
ComunidadGuías…

__tests__/ws-owner.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * 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/);
  });
});