test/agent-behavior-settings.test.js
Rama main · réplica de lectura del repositorio original.
// 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);
});