Ctrl K
ComunidadGuías…

test/generation-stop.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 createTurnStop = require('../lib/yoke/codex-turn-stop').createTurnStop;
var codex = require('../lib/yoke/adapters/codex');

function fixture() {
  var calls = [];
  var errors = [];
  var server = { started: true, send: function (method, params) {
    calls.push({ method: method, params: params }); return Promise.resolve({});
  } };
  return { server: server, calls: calls, errors: errors, stop: createTurnStop(server, function (err) { errors.push(err); }) };
}

test('Codex Stop sends exact current turn identity once', async function () {
  var f = fixture();
  f.stop.begin('thread'); f.stop.started('old'); f.stop.completed('old');
  f.stop.begin('thread'); f.stop.started('current');
  var pending = f.stop.abort();
  assert.equal(f.stop.abort(), pending);
  f.stop.completed('current');
  await pending;
  assert.deepEqual(f.calls, [{ method: 'turn/interrupt', params: { threadId: 'thread', turnId: 'current' } }]);
});

test('Stop during turn/start waits for its identity without interrupting the previous turn', async function () {
  var f = fixture();
  f.stop.begin('thread'); f.stop.started('old'); f.stop.completed('old');
  f.stop.begin('thread');
  var pending = f.stop.abort();
  await Promise.resolve(); assert.equal(f.calls.length, 0);
  f.stop.startSettled('new');
  await pending;
  assert.equal(f.calls[0].params.turnId, 'new');
});

test('turn notification can deliver cancellation identity before the start response', async function () {
  var f = fixture(); f.stop.begin('thread');
  var pending = f.stop.abort();
  f.stop.started('new'); f.stop.startSettled('new');
  await pending; assert.equal(f.calls.length, 1);
});

test('a late start response cannot resurrect a completed turn', async function () {
  var f = fixture(); f.stop.begin('thread'); f.stop.started('done');
  f.stop.completed('done'); f.stop.startSettled('done');
  await f.stop.abort(); assert.equal(f.calls.length, 0);
});

test('idle and failed-start Stop do not send an invalid interrupt', async function () {
  var f = fixture(); await f.stop.abort(); assert.equal(f.calls.length, 0);
  f = fixture(); f.stop.begin('thread');
  var pending = f.stop.abort(); f.stop.startSettled(null);
  await pending; assert.equal(f.calls.length, 0);
});

test('interrupt rejection and disconnection are reported', async function () {
  var f = fixture();
  f.server.send = function () { return Promise.reject(new Error('rejected')); };
  f.stop.begin('thread'); f.stop.started('turn'); await f.stop.abort();
  assert.equal(f.errors[0].message, 'rejected');
  f = fixture(); f.server.started = false;
  f.stop.begin('thread'); f.stop.started('turn'); await f.stop.abort();
  assert.equal(f.errors.length, 1); assert.equal(f.calls.length, 0);
});

test('adapter Stop during start interrupts the returned turn and finishes the iterator', async function () {
  var calls = [];
  var resolveStart;
  var controller = new AbortController();
  var server = { started: true, addHandler: function () { return {}; }, removeHandler: function () {}, send: function (method, params) {
    if (method === 'thread/start') return Promise.resolve({ thread: { id: 'thread' } });
    if (method === 'turn/start') return new Promise(function (resolve) { resolveStart = resolve; });
    if (method === 'turn/interrupt') { calls.push(params); return Promise.resolve({}); }
    return Promise.resolve({});
  } };
  var handle = codex.contractTestKit.createQueryHandle(server, { cwd: process.cwd(), skipSkills: true, abortController: controller });
  handle.pushMessage('test');
  while (!resolveStart) await new Promise(function (resolve) { setImmediate(resolve); });
  controller.abort();
  resolveStart({ turn: { id: 'turn' } });
  for await (var event of handle) { /* Drain until Stop ends the iterator. */ }
  assert.deepEqual(calls, [{ threadId: 'thread', turnId: 'turn' }]);
});

// Minimal fake DOM node supporting the handful of APIs generation-stop.js
// actually calls: addEventListener, classList.toggle/contains, and
// reparenting via appendChild / insertBefore (both update parentNode, as the
// real DOM does, so a node is never left registered in two containers).
function makeNode(id) {
  var classes = {};
  var listeners = {};
  var node = {
    id: id,
    parentNode: null,
    children: [],
    classList: { toggle: function (name, on) { if (on) classes[name] = true; else delete classes[name]; }, contains: function (name) { return !!classes[name]; } },
    addEventListener: function (name, fn) { listeners[name] = fn; },
    dispatch: function (name) { if (listeners[name]) listeners[name](); },
    appendChild: function (child) {
      if (child.parentNode) child.parentNode.children.splice(child.parentNode.children.indexOf(child), 1);
      node.children.push(child);
      child.parentNode = node;
      return child;
    },
    insertBefore: function (child, before) {
      if (child.parentNode) child.parentNode.children.splice(child.parentNode.children.indexOf(child), 1);
      var at = before ? node.children.indexOf(before) : -1;
      if (at === -1) node.children.push(child); else node.children.splice(at, 0, child);
      child.parentNode = node;
      return child;
    },
  };
  return node;
}

function loadGenerationStop(overrides) {
  var state = Object.assign({ connected: true, processing: true }, overrides && overrides.state);
  var listener;
  var sent = [];
  var locked = !!(overrides && overrides.locked);
  var els = {
    'generation-stop': makeNode('generation-stop'),
    'input-area': makeNode('input-area'),
    'input-bottom-right': makeNode('input-bottom-right'),
    'send-btn': makeNode('send-btn'),
  };
  // The unlocked default: Stop already sits beside Send, as index.html wires it.
  els['input-bottom-right'].appendChild(els['generation-stop']);
  els['input-bottom-right'].appendChild(els['send-btn']);
  var ws = { readyState: 1, send: function (message) { sent.push(JSON.parse(message)); } };
  var source = fs.readFileSync('lib/public/modules/generation-stop.js', 'utf8').replace(/^import .*;\n/gm, '').replace(/export function/g, 'function');
  var context = {
    store: {
      get: function (key) { return state[key]; },
      subscribe: function (fn) { listener = fn; },
    },
    getWs: function () { return ws; },
    isDriverOperatedView: function () { return locked; },
    document: { getElementById: function (id) { return els[id] || null; } },
  };
  vm.runInNewContext(source + '\ninitGenerationStop();', context);
  return {
    els: els, sent: sent,
    button: els['generation-stop'],
    click: function () { els['generation-stop'].dispatch('click'); },
    setLocked: function (value) { locked = value; },
    setState: function (next) { var prev = state; state = Object.assign({}, state, next); listener(state, prev); },
  };
}

test('emergency Stop only sends on a connected processing session', function () {
  var f = loadGenerationStop();
  f.click(); assert.deepEqual(f.sent, [{ type: 'stop' }]);
  assert.equal(f.button.classList.contains('hidden'), false);
  assert.equal(f.button.disabled, false);
  f.setState({ connected: false, processing: true });
  f.click(); assert.equal(f.sent.length, 1); assert.equal(f.button.disabled, true);
  f.setState({ connected: true, processing: false });
  f.click(); assert.equal(f.sent.length, 1); assert.equal(f.button.classList.contains('hidden'), true);
});

test('Stop lives beside Send in the ordinary composer action row', function () {
  var f = loadGenerationStop({ locked: false });
  assert.equal(f.button.parentNode, f.els['input-bottom-right']);
  assert.ok(f.els['input-bottom-right'].children.indexOf(f.button) < f.els['input-bottom-right'].children.indexOf(f.els['send-btn']),
    'Stop sits before Send, never after it');
});

test('a locked configured Worker pane keeps Stop reachable outside the hidden composer', function () {
  var f = loadGenerationStop({ locked: true });
  assert.equal(f.button.parentNode, f.els['input-area'],
    'Stop is relocated to a direct child of #input-area, never left inside #input-bottom-right');
  assert.equal(f.els['input-bottom-right'].children.indexOf(f.button), -1,
    'no duplicate Stop is left behind in the composer action row');
});

test('Stop follows the pane across a lock/unlock transition, never duplicated', function () {
  var f = loadGenerationStop({ locked: false });
  assert.equal(f.button.parentNode, f.els['input-bottom-right']);
  f.setLocked(true);
  f.setState({ splitGroups: [{ id: 'sg1' }] });
  assert.equal(f.button.parentNode, f.els['input-area']);
  assert.equal(f.els['input-bottom-right'].children.indexOf(f.button), -1);
  f.setLocked(false);
  f.setState({ activeSessionId: 9 });
  assert.equal(f.button.parentNode, f.els['input-bottom-right']);
  assert.equal(f.els['input-area'].children.indexOf(f.button), -1);
  // At no point does either container hold the node twice.
  assert.equal(f.els['input-bottom-right'].children.filter(function (c) { return c === f.button; }).length, 1);
});

test('adapter surfaces interrupt rejection before ending generation', async function () {
  var handler;
  var began;
  var server = { started: true, addHandler: function (fn) { handler = fn; return {}; }, removeHandler: function () {}, send: function (method) {
    if (method === 'thread/start') return Promise.resolve({ thread: { id: 'thread' } });
    if (method === 'turn/start') {
      handler({ method: 'turn/started', params: { threadId: 'thread', turn: { id: 'turn' } } });
      began = true; return Promise.resolve({ turn: { id: 'turn' } });
    }
    if (method === 'turn/interrupt') return Promise.reject(new Error('test rejection'));
    return Promise.resolve({});
  } };
  var handle = codex.contractTestKit.createQueryHandle(server, { cwd: process.cwd(), skipSkills: true });
  handle.pushMessage('test');
  while (!began) await new Promise(function (resolve) { setImmediate(resolve); });
  await handle.abort();
  var errors = [];
  for await (var event of handle) if (event.yokeType === 'error') errors.push(event.text);
  assert.equal(errors.length, 1);
  assert.match(errors[0], /could not be confirmed/);
});

test('Stop between turns releases the query handler and discards queued input', async function () {
  var handler;
  var removed = false;
  var server = { started: true, addHandler: function (fn) { handler = fn; return {}; }, removeHandler: function () { removed = true; }, send: function (method) {
    if (method === 'thread/start') return Promise.resolve({ thread: { id: 'thread' } });
    if (method === 'turn/start') {
      setImmediate(function () { handler({ method: 'turn/completed', params: { threadId: 'thread' } }); });
      return Promise.resolve({ turn: { id: 'turn' } });
    }
    assert.notEqual(method, 'turn/interrupt');
    return Promise.resolve({});
  } };
  var handle = codex.contractTestKit.createQueryHandle(server, { cwd: process.cwd(), skipSkills: true });
  handle.pushMessage('test');
  for await (var event of handle) if (event.yokeType === 'result') break;
  await new Promise(function (resolve) { setImmediate(resolve); });
  await handle.abort();
  await new Promise(function (resolve) { setImmediate(resolve); });
  assert.equal(removed, true);
  assert.equal(handle.pushMessage('should not run'), false);
});

test('Codex pre-aborted controller rejects input before starting a thread', async function () {
  var controller = new AbortController(); controller.abort();
  var calls = [];
  var server = { started: true, send: function (method) { calls.push(method); return Promise.resolve({}); } };
  var handle = codex.contractTestKit.createQueryHandle(server, { cwd: process.cwd(), skipSkills: true, abortController: controller });
  assert.equal(handle.pushMessage('late'), false);
  for await (var event of handle) { /* Drain cancellation. */ }
  assert.deepEqual(calls, []);
});

test('resumed Codex rejects old output and completion before and after the new turn starts', async function () {
  var handler, resume, start;
  var server = { started: true, addHandler: function (fn) { handler = fn; return {}; }, removeHandler: function () {}, send: function (method) {
    if (method === 'thread/resume') return new Promise(function (resolve) { resume = resolve; });
    if (method === 'turn/start') return new Promise(function (resolve) { start = resolve; });
    return Promise.resolve({});
  } };
  var handle = codex.contractTestKit.createQueryHandle(server, { cwd: process.cwd(), skipSkills: true, resumeSessionId: 'thread' });
  function delta(turn, text) { handler({ method: 'item/agentMessage/delta', params: { threadId: 'thread', turnId: turn, itemId: turn, delta: text } }); }
  function complete(turn) { handler({ method: 'turn/completed', params: { threadId: 'thread', turn: { id: turn, status: 'completed' } } }); }
  handle.pushMessage('new prompt');
  delta('old', 'stale replay during resume');
  resume({ thread: { id: 'thread' } });
  while (!start) await new Promise(function (resolve) { setImmediate(resolve); });
  delta('old', 'stale before start response'); complete('old');
  handler({ method: 'turn/started', params: { threadId: 'thread', turn: { id: 'new' } } });
  delta('new', 'fresh before start response');
  start({ turn: { id: 'new' } });
  await new Promise(function (resolve) { setImmediate(resolve); });
  delta('old', 'stale after start response'); complete('old');
  delta('new', 'fresh after stale completion');
  handle.endInput(); complete('new');
  var texts = [];
  for await (var event of handle) if (event.yokeType === 'text_delta') texts.push(event.text);
  assert.deepEqual(texts, ['fresh before start response', 'fresh after stale completion']);
});

test('a locked Worker keeps Stop available even with a stale idle presentation', function () {
  var f = loadGenerationStop({ locked: true, state: { processing: false } });
  assert.equal(f.button.classList.contains('hidden'), false);
  f.click(); assert.deepEqual(f.sent, [{ type: 'stop' }]);
});