Ctrl K
ComunidadGuías…

test/app-connection-production.test.js

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

Archivo original ↗Preguntar a KiBot
var test = require("node:test");
var assert = require("node:assert/strict");
var fs = require("node:fs");
var vm = require("node:vm");
var path = require("node:path");

var root = path.join(__dirname, "..");

function productionHarness() {
  var now = 0;
  var nextTimer = 1;
  var timers = new Map();
  var listeners = {};
  var sockets = [];
  var fetches = [];
  var socket = null;
  var state = { connected: false, wsPath: "/ws", socketPath: "/ws", currentSlug: "project-a", activeSessionId: 7 };
  var context = {
    console: { warn: function () {}, log: function () {} },
    Math: Object.assign(Object.create(Math), { random: function () { return 0.5; } }),
    Date: { now: function () { return now; } },
    navigator: { onLine: true },
    location: { protocol: "http:", host: "fixture", reload: function () {} },
    localStorage: { removeItem: function () {} },
    state: state,
    store: {
      get: function (key) { return state[key]; },
      set: function (next) { Object.assign(state, next); },
      subscribe: function () {}
    },
    getWs: function () { return socket; },
    setWs: function (next) { socket = next; },
    window: {
      addEventListener: function (name, callback) { listeners[name] = callback; },
      dispatchEvent: function () {}
    },
    document: {
      hidden: false,
      getElementById: function () { return null; },
      addEventListener: function (name, callback) { listeners[name] = callback; }
    },
    fetch: function () {
      var success = null;
      var failure = null;
      var pending = {
        then: function (callback) { success = callback; return pending; },
        catch: function (callback) { failure = callback; return pending; },
        resolve: function (value) {
          if (value && value.status === 200 && !value.json) value.json = function () { return { then: function (callback) { callback({ projects: [{ slug: "project-a", contentAccess: true }] }); } }; };
          if (success) success(value);
        },
        reject: function () { if (failure) failure(); }
      };
      fetches.push(pending);
      return pending;
    },
    CustomEvent: function (type, init) { this.type = type; this.detail = init.detail; },
    AbortController: function () { this.signal = {}; this.abort = function () {}; },
    processMessage: function () {}
  };
  context.paneOutgoing = function (data) { return data; };
  var imported = fs.readFileSync(path.join(root, "lib/public/modules/app-connection.js"), "utf8");
  Array.from(imported.matchAll(/import \{([^}]+)\}/g)).forEach(function (match) {
    match[1].split(",").forEach(function (name) {
      name = name.trim();
      if (name && !context[name]) context[name] = function () {};
    });
  });
  context.setTimeout = function (fn, delay) {
    var id = nextTimer++;
    timers.set(id, { fn: fn, at: now + delay });
    return id;
  };
  context.clearTimeout = function (id) { timers.delete(id); };
  context.setInterval = function (fn, delay) {
    var id = nextTimer++;
    timers.set(id, { fn: fn, at: now + delay, interval: delay });
    return id;
  };
  context.clearInterval = context.clearTimeout;
  context.WebSocket = function () {
    var current = this;
    current.readyState = 0;
    current.send = function () {};
    current.close = function () {
      current.readyState = 3;
      if (current.onclose) current.onclose({ code: 1000, wasClean: true });
    };
    sockets.push(current);
  };
  vm.createContext(context);
  var lifecycle = fs.readFileSync(path.join(root, "lib/public/modules/websocket-lifecycle.js"), "utf8");
  var watchdog = fs.readFileSync(path.join(root, "lib/public/modules/websocket-watchdog.js"), "utf8");
  vm.runInContext(lifecycle.replace(/export \{[^}]+\};?/, ""), context);
  vm.runInContext(watchdog.replace(/export \{[^}]+\};?/, ""), context);
  var replay = fs.readFileSync(path.join(root, "lib/public/modules/history-replay-batch.js"), "utf8");
  vm.runInContext(replay.replace(/^import .*;\n/gm, "").replace(/export /g, ""), context);
  var recovery = fs.readFileSync(path.join(root, "lib/public/modules/project-access-recovery.js"), "utf8");
  vm.runInContext(recovery.replace(/^import .*;\n/gm, "").replace(/export /g, ""), context);
  vm.runInContext(imported.replace(/^import .*;\n/gm, "").replace(/export /g, ""), context);
  function tick(duration) {
    var end = now + duration;
    while (true) {
      var selected = null;
      timers.forEach(function (entry, id) {
        if (entry.at <= end && (!selected || entry.at < selected.entry.at)) selected = { id: id, entry: entry };
      });
      if (!selected) break;
      now = selected.entry.at;
      timers.delete(selected.id);
      if (selected.entry.interval) timers.set(selected.id, { fn: selected.entry.fn, at: now + selected.entry.interval, interval: selected.entry.interval });
      selected.entry.fn();
    }
    now = end;
  }
  function jump(duration) {
    now += duration;
  }
  function timerCallbacks() {
    return Array.from(timers.values()).map(function (entry) { return entry.fn; });
  }
  return { context: context, listeners: listeners, sockets: sockets, fetches: fetches, timers: timers, tick: tick, jump: jump, timerCallbacks: timerCallbacks, getSocket: function () { return socket; } };
}

test("production connection cancels close stability, stale messages, probes, and resume deadlines", function () {
  var f = productionHarness();
  f.context.connect();
  var first = f.sockets[0];
  first.readyState = 1;
  first.onopen();
  first.close();
  f.tick(14999);
  var retryBeforeOldStableDeadline = f.context.lifecycle.getRetryAttempt();
  f.tick(1);
  assert.equal(f.context.lifecycle.getRetryAttempt(), retryBeforeOldStableDeadline, "closed sockets cannot reset retry state through an old stable timer");

  var g = productionHarness();
  g.context.initConnection();
  g.context.connect();
  var live = g.sockets[0];
  live.readyState = 1;
  live.send = function (data) {
    if (JSON.parse(data).type === "ping" && live.onmessage) live.onmessage({ data: JSON.stringify({ type: "pong" }) });
  };
  live.onopen();
  g.listeners.visibilitychange();
  g.context.cancelReconnect();
  var sendsAfterCancel = 0;
  live.send = function () { sendsAfterCancel += 1; };
  g.tick(10000);
  assert.equal(sendsAfterCancel, 0, "cancel prevents a pending probe from sending");
  g.context.state.connected = false;
  live.onmessage({ data: JSON.stringify({ type: "pong" }) });
  assert.equal(g.context.state.connected, false, "cancelled socket messages cannot restore connected state");

  var h = productionHarness();
  h.context.initConnection();
  h.context.connect();
  var offlineSocket = h.sockets[0];
  offlineSocket.readyState = 1;
  offlineSocket.onopen();
  h.listeners.offline();
  assert.equal(h.getSocket(), null, "offline detaches the old OPEN socket");
  assert.equal(offlineSocket.onmessage, null, "offline detaches stale message handlers");

  var resumed = productionHarness();
  resumed.context.initConnection();
  resumed.context.connect();
  var resumedSocket = resumed.sockets[0];
  resumedSocket.readyState = 1;
  resumedSocket.send = function (data) {
    if (JSON.parse(data).type === "ping" && resumedSocket.onmessage) resumedSocket.onmessage({ data: JSON.stringify({ type: "pong" }) });
  };
  resumedSocket.onopen();
  resumed.tick(25000);
  resumed.context.document.hidden = true;
  resumed.listeners.visibilitychange();
  resumed.context.document.hidden = false;
  resumed.listeners.visibilitychange();
  resumed.tick(10000);
  assert.equal(resumed.getSocket(), resumedSocket, "visible resume replaces the old heartbeat deadline");
});

test("hidden suspension invalidates queued watchdog callbacks before a fresh visible probe", function () {
  var f = productionHarness();
  f.context.initConnection();
  f.context.connect();
  var socket = f.sockets[0];
  var pings = 0;
  socket.readyState = 1;
  socket.send = function (data) { if (JSON.parse(data).type === "ping") pings += 1; };
  socket.onopen();
  f.tick(25000);
  f.listeners["clay-message-delivery-timeout"]({ detail: { socket: socket } });
  var queuedCallbacks = f.timerCallbacks();

  f.context.document.hidden = true;
  f.listeners.visibilitychange();
  f.jump(60000);
  queuedCallbacks.forEach(function (callback) { callback(); });
  assert.equal(f.getSocket(), socket, "callbacks queued before hidden cannot replace the retained socket");

  socket.send = function (data) {
    if (JSON.parse(data).type !== "ping") return;
    pings += 1;
    socket.onmessage({ data: JSON.stringify({ type: "pong" }) });
  };
  f.context.document.hidden = false;
  f.listeners.visibilitychange();
  f.tick(10000);
  assert.equal(f.getSocket(), socket, "the fresh visible probe proves the retained socket is healthy");
  assert.equal(pings, 3, "one heartbeat, one old probe, and one fresh visible probe are sent");
});

test("freeze resume coalesces repeated events and accepts a queued pong", function () {
  var f = productionHarness();
  f.context.initConnection();
  f.context.connect();
  var socket = f.sockets[0];
  var pings = 0;
  socket.readyState = 1;
  socket.send = function (data) { if (JSON.parse(data).type === "ping") pings += 1; };
  socket.onopen();
  f.listeners.freeze();
  f.jump(60000);
  f.listeners.resume();
  f.listeners.resume();
  assert.equal(pings, 1, "repeated resume events share one in-flight liveness probe");
  socket.onmessage({ data: JSON.stringify({ type: "pong" }) });
  f.tick(4000);
  assert.equal(f.getSocket(), socket, "a queued pong after resume clears the fresh probe");
});

test("online while hidden waits for visibility and hidden open starts no watchdog", function () {
  var hiddenOpen = productionHarness();
  hiddenOpen.context.document.hidden = true;
  hiddenOpen.context.initConnection();
  hiddenOpen.context.connect();
  var hiddenSocket = hiddenOpen.sockets[0];
  var hiddenPings = 0;
  hiddenSocket.readyState = 1;
  hiddenSocket.send = function (data) { if (JSON.parse(data).type === "ping") hiddenPings += 1; };
  hiddenSocket.onopen();
  hiddenOpen.tick(25000);
  assert.equal(hiddenPings, 0, "a socket opened while hidden has no background watchdog");
  hiddenOpen.context.document.hidden = false;
  hiddenOpen.listeners.visibilitychange();
  assert.equal(hiddenPings, 1, "visibility starts one fresh probe for a hidden-opened socket");

  var offline = productionHarness();
  offline.context.initConnection();
  offline.context.connect();
  var offlineSocket = offline.sockets[0];
  offlineSocket.readyState = 1;
  offlineSocket.onopen();
  offline.context.document.hidden = true;
  offline.listeners.visibilitychange();
  offline.listeners.offline();
  offline.listeners.online();
  offline.tick(20000);
  assert.equal(offline.fetches.length, 0, "online while hidden does not start reconnect authentication");
  assert.equal(offline.sockets.length, 1, "online while hidden does not create a socket");
  offline.context.document.hidden = false;
  offline.listeners.visibilitychange();
  offline.tick(1000);
  assert.equal(offline.fetches.length, 1, "visible recovery resumes the existing reconnect policy");
});

test("a dead resumed socket is replaced in four seconds and an old socket pong is ignored", function () {
  var f = productionHarness();
  f.context.initConnection();
  f.context.connect();
  var first = f.sockets[0];
  first.readyState = 1;
  first.send = function () {};
  first.onopen();
  var oldMessage = first.onmessage;
  f.context.document.hidden = true;
  f.listeners.visibilitychange();
  f.context.document.hidden = false;
  f.listeners.visibilitychange();
  f.tick(3999);
  assert.equal(f.getSocket(), first, "the resumed probe retains its full four-second budget");
  f.tick(1);
  assert.equal(f.getSocket(), null, "an actually dead resumed socket is replaced at four seconds");

  f.tick(1000);
  f.fetches[0].resolve({ status: 200 });
  var second = f.sockets[1];
  second.readyState = 1;
  second.send = function () {};
  second.onopen();
  f.listeners.visibilitychange();
  oldMessage({ data: JSON.stringify({ type: "pong" }) });
  f.tick(4000);
  assert.equal(f.getSocket(), null, "a late pong from the retired socket cannot clear the current probe");
});

test("production handshake timeout replaces a stalled socket and server health resets stability", function () {
  var f = productionHarness();
  f.context.connect();
  assert.equal(f.sockets.length, 1);
  f.tick(11999);
  assert.equal(f.sockets.length, 1, "the production handshake remains within its 12-second budget");
  f.tick(1);
  assert.equal(f.getSocket(), null, "a stalled handshake is replaced without waiting for close");

  var healthy = productionHarness();
  healthy.context.lifecycle.schedule("prior", function () {});
  healthy.context.connect();
  var healthySocket = healthy.sockets[0];
  healthySocket.readyState = 1;
  healthySocket.onopen();
  healthy.tick(14999);
  assert.equal(healthy.context.lifecycle.getRetryAttempt(), 1, "open alone does not reset retry state");
  healthySocket.onmessage({ data: JSON.stringify({ type: "bootstrap" }) });
  healthy.tick(15000);
  assert.equal(healthy.context.lifecycle.getRetryAttempt(), 0, "server health proof resets retry state after stability");
});

test("production auth ignores late resolve and reject from a timed-out request", function () {
  ["resolve", "reject"].forEach(function (outcome) {
    var f = productionHarness();
    f.context.scheduleReconnect("auth-race");
    f.tick(1000);
    assert.equal(f.fetches.length, 1);
    f.tick(4000);
    f.tick(1500);
    assert.equal(f.fetches.length, 2, "a replacement auth request is pending in the same epoch");
    f.fetches[0][outcome]({ status: 200 });
    assert.equal(f.getSocket(), null, "the stale auth result cannot start a socket");
  });
});

test("wakeups do not replace a CONNECTING handshake and errors replace directly", function () {
  var f = productionHarness();
  f.context.initConnection();
  f.context.connect();
  var connecting = f.sockets[0];
  f.listeners.online();
  f.listeners.visibilitychange();
  f.listeners["clay-message-delivery-timeout"]({ detail: { socket: connecting } });
  f.tick(1000);
  assert.equal(f.sockets.length, 1, "wakeups do not create parallel sockets during the handshake");
  assert.equal(f.getSocket(), connecting, "the allowed handshake remains live");
  connecting.onerror();
  assert.equal(f.getSocket(), null, "onerror replaces without waiting for close");
  assert.equal(connecting.onclose, null, "replacement detaches the close callback");
});

test("a real ACK-timeout event probes a healthy OPEN socket without reconnecting", function () {
  var f = productionHarness();
  f.context.initConnection();
  f.context.connect();
  var socket = f.sockets[0];
  var pings = 0;
  socket.readyState = 1;
  socket.send = function (data) {
    if (JSON.parse(data).type === "ping") {
      pings++;
      socket.onmessage({ data: JSON.stringify({ type: "pong" }) });
    }
  };
  socket.onopen();
  f.listeners["clay-message-delivery-timeout"]({ detail: { socket: socket, clientMessageId: "cm-health" } });
  assert.equal(f.getSocket(), socket, "a healthy ACK-timeout probe does not replace the socket");
  assert.equal(f.sockets.length, 1, "a healthy ACK-timeout probe does not create another socket");
  assert.equal(pings, 1, "the ACK-timeout event sends one immediate ping");
});

test("returning to a background pane replaces queued output with an exact-session snapshot", function () {
  var f = productionHarness();
  f.context.state.paneMode = true;
  f.context.state.paneSessionId = 42;
  f.context.initConnection(); f.context.connect();
  var first = f.sockets[0]; first.readyState = 1; first.onopen();
  var delivered = [];
  f.context.processMessage = function (message) { delivered.push(message); };
  var queued = first.onmessage;
  f.context.state.historyReplayBatch = { items: [{ type: 'delta', text: 'interrupted history' }] };
  f.context.document.hidden = true; f.listeners.visibilitychange();
  queued({ data: JSON.stringify({ type: 'text_delta', text: 'old backlog' }) });
  assert.equal(delivered.length, 0);
  f.context.document.hidden = false;
  // Browser delivery can run before visibilitychange; it must not drain backlog.
  queued({ data: JSON.stringify({ type: 'text_delta', text: 'old backlog' }) });
  f.listeners.visibilitychange();
  assert.equal(f.sockets.length, 2, 'resume creates exactly one replacement');
  assert.equal(first.readyState, 3);
  assert.equal(f.context.state.historyReplayBatch, null, 'replacement drops partially received history');
  queued({ data: JSON.stringify({ type: 'text_delta', text: 'stale socket' }) });
  assert.equal(delivered.length, 0);
  var current = f.sockets[1]; current.readyState = 1; current.onopen();
  assert.equal(f.context.state.panePinPending, true);
  assert.equal(f.context.state.paneSessionId, 42);
  current.onmessage({ data: JSON.stringify({ type: 'history_done' }) });
  assert.equal(delivered.length, 1);
});

test("frozen pane resumes once and offline pane waits for the network", function () {
  var f = productionHarness();
  f.context.state.paneMode = true; f.context.state.paneSessionId = 42;
  f.context.initConnection(); f.context.connect();
  var socket = f.sockets[0]; socket.readyState = 1; socket.onopen();
  f.listeners.freeze(); f.listeners.resume(); f.listeners.resume();
  assert.equal(f.sockets.length, 2);
  f.context.document.hidden = true; f.listeners.visibilitychange(); f.listeners.offline();
  f.context.document.hidden = false; f.listeners.visibilitychange();
  assert.equal(f.sockets.length, 2, 'visibility cannot reconnect while offline');
  f.listeners.online(); f.tick(1000);
  assert.equal(f.fetches.length, 1, 'online uses authenticated reconnect recovery');
});

test("reconnect clears revoked content and enters administrator metadata instead of reopening the project socket", function () {
  var f = productionHarness();
  var destination = null;
  var cleared = false;
  f.context.document.body = { replaceChildren: function () { cleared = true; } };
  f.context.window.location = { replace: function (url) { destination = url; } };
  f.context.initConnection();
  f.context.scheduleReconnect("close");
  f.tick(1000);
  f.fetches[0].resolve({ status: 200, json: function () { return { then: function (callback) { callback({ projects: [{ slug: "project-a", contentAccess: false }] }); } }; } });
  assert.equal(destination, "/p/project-a/");
  assert.equal(cleared, true);
  assert.equal(f.sockets.length, 0);
  assert.equal(f.context.state.activeSessionId, null);
});