Ctrl K
ComunidadGuías…

test/agent-behavior-settings.test.js

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

Archivo original ↗Preguntar a KiBot
// Personal "Agent behavior" client logic: registry entry, catalog-driven state,
// save acknowledgement, failure/retry and stale-reply handling. Runs the real
// modules in Node with a fake fetch; the browser behaviour is covered in
// agent-behavior-browser.test.js.
var test = require('node:test');
var assert = require('node:assert/strict');
var path = require('node:path');
var pathToFileURL = require('node:url').pathToFileURL;
var schema = require('../lib/agent-behavior-schema');

var MODULES = path.join(__dirname, '../lib/public/modules');
function load(name) { return import(pathToFileURL(path.join(MODULES, name)).href); }

function payload(policy, extra) {
  var full = Object.assign(schema.defaults(), policy || {});
  return Object.assign({ ok: true, policy: full, effective: schema.effective(full), catalog: schema.catalog() }, extra || {});
}
function reply(status, body) { return { ok: status >= 200 && status < 300, status: status, json: function () { return Promise.resolve(body); } }; }
function deferredFetch() {
  var calls = [];
  global.fetch = function (url, init) {
    return new Promise(function (resolve, reject) { calls.push({ url: url, init: init || {}, resolve: resolve, reject: reject, body: init && init.body ? JSON.parse(init.body) : null }); });
  };
  return calls;
}
function tick() { return new Promise(function (resolve) { setImmediate(resolve); }); }
async function ready(m, policy) {
  var calls = deferredFetch();
  var loading = m.loadAgentBehavior();
  calls[0].resolve(reply(200, payload(policy)));
  await loading;
  assert.equal(m.getAgentBehavior().status, 'ready');
  return calls;
}
function put(calls, index, body) { calls[index].resolve(reply(200, body)); }

test('the Personal registry lists Agent behavior right after AI providers', async function () {
  var registry = await load('settings-shell-registry.js');
  var ids = registry.PERSONAL_PAGES.map(function (page) { return page.id; });
  assert.equal(ids[ids.indexOf('us-providers') + 1], 'us-agent');
  assert.equal(registry.PERSONAL_PAGES.filter(function (page) { return page.id === 'us-agent'; })[0].label, 'Agent behavior');
  assert.equal(registry.resolvePage('personal', 'us-agent'), 'us-agent');
});

test('loading fetches the catalog and policy, and a failed load is an error state with a message', async function () {
  var m = await load('agent-behavior.js');
  var calls = deferredFetch();
  var loading = m.loadAgentBehavior();
  assert.equal(m.getAgentBehavior().status, 'loading');
  assert.equal(calls[0].url, '/api/user/agent-behavior');
  calls[0].resolve(reply(500, { error: 'Disk unavailable' }));
  await loading;
  assert.equal(m.getAgentBehavior().status, 'error');
  assert.equal(m.getAgentBehavior().error, 'Disk unavailable');
  loading = m.loadAgentBehavior();
  calls[1].resolve(reply(401, { error: 'unauthorized' }));
  await loading;
  assert.match(m.getAgentBehavior().error, /signed out/);
  loading = m.loadAgentBehavior();
  calls[2].resolve(reply(200, { ok: true, policy: {} }));
  await loading;
  assert.equal(m.getAgentBehavior().status, 'error', 'a malformed answer is not an empty page');
  loading = m.loadAgentBehavior();
  calls[3].reject(new Error('offline'));
  await loading;
  assert.match(m.getAgentBehavior().error, /Could not reach/);
  assert.equal(m.getAgentBehavior().catalog, null);
});

test('a choice shows Saving, sends only the changed key with a request id and is Saved only after the 200', async function () {
  var m = await load('agent-behavior.js');
  var calls = await ready(m);
  assert.equal(m.chooseValue('notes', 'request'), true);
  var state = m.getAgentBehavior();
  assert.equal(m.displayedValue(state, 'notes'), 'request');
  assert.deepEqual(m.statusFor(state, 'notes'), { kind: 'saving', text: 'Saving...' });
  assert.equal(state.policy.notes, 'auto', 'the acknowledged value does not move before the reply');
  var request = calls[1];
  assert.equal(request.init.method, 'PUT');
  assert.deepEqual(Object.keys(request.body).sort(), ['changes', 'requestId']);
  assert.deepEqual(request.body.changes, { notes: 'request' });
  assert.match(request.body.requestId, /^ab-/);
  put(calls, 1, payload({ notes: 'request' }, { requestId: request.body.requestId }));
  await tick();
  state = m.getAgentBehavior();
  assert.equal(state.policy.notes, 'request');
  assert.deepEqual(m.statusFor(state, 'notes'), { kind: 'saved', text: 'Saved' });
  assert.equal(m.chooseValue('notes', 'request'), false, 'selecting the saved value again sends nothing');
  assert.equal(calls.length, 2);
});

test('a failed save reverts to the acknowledged value, keeps the failed choice and Retry resends it', async function () {
  var m = await load('agent-behavior.js');
  var calls = await ready(m);
  m.chooseValue('issues', 'off');
  calls[1].resolve(reply(500, { error: 'Could not save agent behavior' }));
  await tick();
  var state = m.getAgentBehavior();
  assert.equal(m.displayedValue(state, 'issues'), 'auto', 'the display reverts');
  assert.deepEqual(m.statusFor(state, 'issues'), { kind: 'error', text: 'Could not save agent behavior' });
  assert.equal(state.edits.issues.value, 'off');
  assert.equal(m.retryValue('issues'), true);
  assert.equal(m.statusFor(m.getAgentBehavior(), 'issues').kind, 'saving');
  assert.deepEqual(calls[2].body.changes, { issues: 'off' });
  assert.notEqual(calls[2].body.requestId, calls[1].body.requestId);
  put(calls, 2, payload({ issues: 'off' }, { requestId: calls[2].body.requestId }));
  await tick();
  assert.equal(m.getAgentBehavior().policy.issues, 'off');
  assert.equal(m.statusFor(m.getAgentBehavior(), 'issues').kind, 'saved');
  // A network failure is handled the same way.
  m.chooseValue('logs', 'request');
  calls[3].reject(new Error('offline'));
  await tick();
  assert.equal(m.statusFor(m.getAgentBehavior(), 'logs').kind, 'error');
  assert.equal(m.displayedValue(m.getAgentBehavior(), 'logs'), 'auto');
  // Choosing the acknowledged value discards the failed choice.
  m.chooseValue('logs', 'auto');
  assert.equal(m.statusFor(m.getAgentBehavior(), 'logs').kind, '');
});

test('requests for one setting are serialized and the newest choice wins; a mismatched reply is a failure', async function () {
  var m = await load('agent-behavior.js');
  var calls = await ready(m);
  m.chooseValue('sketch', 'request');
  m.chooseValue('sketch', 'off');
  assert.equal(calls.length, 2, 'the second choice waits for the first reply');
  assert.equal(m.displayedValue(m.getAgentBehavior(), 'sketch'), 'off');
  put(calls, 1, payload({ sketch: 'request' }, { requestId: calls[1].body.requestId }));
  await tick();
  assert.equal(calls.length, 3);
  assert.deepEqual(calls[2].body.changes, { sketch: 'off' });
  assert.equal(m.displayedValue(m.getAgentBehavior(), 'sketch'), 'off', 'the older reply never moves the display');
  assert.equal(m.statusFor(m.getAgentBehavior(), 'sketch').kind, 'saving');
  put(calls, 2, payload({ sketch: 'off' }, { requestId: calls[2].body.requestId }));
  await tick();
  assert.equal(m.getAgentBehavior().policy.sketch, 'off');
  assert.equal(m.statusFor(m.getAgentBehavior(), 'sketch').kind, 'saved');
  m.chooseValue('continuation', 'off');
  put(calls, 3, payload({ continuation: 'off' }, { requestId: 'someone-else' }));
  await tick();
  assert.equal(m.statusFor(m.getAgentBehavior(), 'continuation').kind, 'error');
});

test('a reply only merges its own setting and cannot overwrite another setting or a newer effective value', async function () {
  var m = await load('agent-behavior.js');
  var calls = await ready(m);
  m.chooseValue('notes', 'request');   // call 1
  m.chooseValue('meUse', 'off');       // call 2
  put(calls, 2, payload({ meUse: 'off' }, { requestId: calls[2].body.requestId }));
  await tick();
  // The older notes reply was computed before meUse changed and carries meUse auto.
  put(calls, 1, payload({ notes: 'request', meUse: 'auto' }, { requestId: calls[1].body.requestId }));
  await tick();
  var state = m.getAgentBehavior();
  assert.equal(state.policy.meUse, 'off');
  assert.equal(state.policy.notes, 'request');
  assert.equal(state.effective, null, 'an effective flag from overlapping replies is unknown, never stale');
  // A lone newest reply with nothing else in flight is trusted again.
  m.chooseValue('notes', 'auto');
  put(calls, 3, payload({ notes: 'auto' }, { requestId: calls[3].body.requestId }));
  await tick();
  assert.equal(m.getAgentBehavior().effective.issueLinkedNotes, true);
});

test('leaving the page invalidates in-flight work so a late reply cannot revive it', async function () {
  var m = await load('agent-behavior.js');
  var calls = await ready(m);
  m.chooseValue('logs', 'off');
  m.resetAgentBehavior();
  assert.equal(m.getAgentBehavior().status, 'idle');
  put(calls, 1, payload({ logs: 'off' }, { requestId: calls[1].body.requestId }));
  await tick();
  assert.equal(m.getAgentBehavior().status, 'idle');
  assert.equal(m.getAgentBehavior().policy, null);
  // A late answer from an older load is ignored as well.
  var next = deferredFetch();
  var first = m.loadAgentBehavior();
  var second = m.loadAgentBehavior();
  next[1].resolve(reply(200, payload({ logs: 'request' })));
  await second;
  next[0].resolve(reply(200, payload({ logs: 'off' })));
  await first;
  assert.equal(m.getAgentBehavior().policy.logs, 'request');
});

test('the linked-note checkbox is inactive unless Sticky Notes is Auto and Issues is not Off, and keeps its saved choice', async function () {
  var m = await load('agent-behavior.js');
  var calls = await ready(m);
  var box = schema.SETTINGS.filter(function (s) { return s.type === 'checkbox'; })[0];
  var info = m.checkboxStatus(m.getAgentBehavior(), box);
  assert.deepEqual([info.checked, info.inactive, info.reason], [true, false, '']);
  m.chooseValue('notes', 'request');
  info = m.checkboxStatus(m.getAgentBehavior(), box);
  assert.equal(info.inactive, true);
  assert.equal(info.checked, true, 'the saved choice stays displayed');
  assert.match(info.reason, /Sticky Notes is On request/);
  put(calls, 1, payload({ notes: 'request' }, { requestId: calls[1].body.requestId }));
  await tick();
  m.chooseValue('notes', 'auto');
  put(calls, 2, payload({}, { requestId: calls[2].body.requestId }));
  await tick();
  assert.equal(m.checkboxStatus(m.getAgentBehavior(), box).inactive, false);
  m.chooseValue('issues', 'off');
  assert.match(m.checkboxStatus(m.getAgentBehavior(), box).reason, /Issues is Off/);
  // The server's effective flag also counts once nothing is pending.
  var other = await ready(m, { issueLinkedNotes: true });
  var state = Object.assign({}, m.getAgentBehavior(), { effective: { issueLinkedNotes: false } });
  assert.match(m.checkboxStatus(state, box).reason, /not currently apply/);
  assert.ok(other.length);
});