bytelyst-devops-tools/agent-queue/lib/fleet-dash.test.mjs
Saravanakumar D 66c91233da feat(agent-queue): re-point TUI dashboard at /fleet API (parity)
Add an opt-in fleet mode to the dashboard so an operator can drive the
coordinator fleet from the same TUI used for the local folder queue.

- lib/fleet-dash.mjs: dependency-injectable read/act adapter over the
  platform-service /fleet REST surface (jobs, metrics, factories, events,
  ship/requeue/reject). Pure-ish + fully unit-testable without a live service.
- dashboard.mjs: render + act in fleet mode when AQ_FLEET_DASH=1 — board with
  counts, factories (per-factory rows or metrics aggregate), alerts, running
  (by lease/factory), actionable JOBS with manifest tags, recent, and a
  per-job events log. Single-flight async refresh keeps the last good board on
  failure; ship re-GETs a fresh leaseEpoch before PATCH; run/stop/promote are
  disabled (no safe server contract). Local mode is byte-for-byte unchanged.
- lib/fleet-dash.test.mjs: 22 node:assert assertions (config, stage mapping,
  toBoard, fetch headers/timeout/errors, board assembly + graceful degradation,
  events, job actions) wired into selftest.sh.
- docs: tick the Phase 3 "TUI re-pointed at /fleet" roadmap boxes.

Verified: selftest.sh green (incl. new fleet-dash checks); live non-TTY render
smoke against a stub /fleet server (both factories and metrics-aggregate paths);
local mode unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-30 19:47:56 -07:00

288 lines
12 KiB
JavaScript

// fleet-dash.test.mjs — dependency-light unit tests for the fleet-mode dashboard
// adapter. Uses node:assert only (no test framework), matching the repo style.
// Run: `node fleet-dash.test.mjs` (also wired into selftest.sh).
//
// These tests prove the dashboard's CONTRACT ASSUMPTIONS against the /fleet API
// (request shaping, response mapping, graceful degradation, action semantics)
// via an injected fetch stub. They do NOT prove live server compatibility.
import assert from 'node:assert/strict';
import {
fleetConfig,
mapStage,
toBoard,
fleetFetch,
fetchBoard,
fetchEvents,
formatEvent,
jobAction,
} from './fleet-dash.mjs';
let passed = 0;
const t = (name, fn) => {
try {
const r = fn();
if (r && typeof r.then === 'function') {
return r.then(
() => { passed += 1; },
(e) => { console.error(`${name}\n ${e && e.message}`); process.exitCode = 1; },
);
}
passed += 1;
} catch (e) {
console.error(`${name}\n ${e && e.message}`);
process.exitCode = 1;
}
return undefined;
};
// A recording fetch stub. `routes` maps a matcher → {status, body} (or a fn).
function makeFetch(routes) {
const calls = [];
const fetchImpl = async (url, opts = {}) => {
calls.push({ url, opts, headers: opts.headers || {}, method: opts.method || 'GET' });
let entry = routes[url];
if (!entry) {
// try suffix match (path only)
const key = Object.keys(routes).find((k) => url.endsWith(k));
entry = key ? routes[key] : undefined;
}
if (typeof entry === 'function') entry = entry({ url, opts });
if (entry === undefined) return mkRes(404, '{}');
if (entry.throw) throw Object.assign(new Error(entry.throw), { name: entry.name || 'Error' });
return mkRes(entry.status ?? 200, entry.body ?? '');
};
fetchImpl.calls = calls;
return fetchImpl;
}
const mkRes = (status, body) => ({
ok: status >= 200 && status < 300,
status,
text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
});
const CFG = { enabled: true, ok: true, api: 'http://svc/api', token: 'tok', productId: 'prodX', missing: [] };
await (async () => {
// ── fleetConfig ──
t('fleetConfig: AQ_FLEET_DASH unset ⇒ disabled', () => {
const c = fleetConfig({});
assert.equal(c.enabled, false);
assert.equal(c.ok, false);
});
t('fleetConfig: enabled but missing config ⇒ not ok, lists missing', () => {
const c = fleetConfig({ AQ_FLEET_DASH: '1' });
assert.equal(c.enabled, true);
assert.equal(c.ok, false);
assert.deepEqual(c.missing.sort(), ['AQ_FLEET_API', 'AQ_FLEET_TOKEN', 'AQ_PRODUCT_ID'].sort());
});
t('fleetConfig: enabled + complete ⇒ ok, trims trailing slash', () => {
const c = fleetConfig({ AQ_FLEET_DASH: '1', AQ_FLEET_API: 'http://svc/api/', AQ_FLEET_TOKEN: 'k', AQ_PRODUCT_ID: 'p' });
assert.equal(c.ok, true);
assert.equal(c.api, 'http://svc/api');
});
// ── mapStage ──
t('mapStage: fleet stages collapse to board buckets', () => {
assert.equal(mapStage('queued'), 'inbox');
assert.equal(mapStage('assigned'), 'building');
assert.equal(mapStage('building'), 'building');
assert.equal(mapStage('review'), 'review');
assert.equal(mapStage('testing'), 'testing');
assert.equal(mapStage('shipped'), 'shipped');
assert.equal(mapStage('failed'), 'failed');
assert.equal(mapStage('dead_letter'), 'failed');
assert.equal(mapStage('weird'), 'inbox');
});
// ── toBoard ──
t('toBoard: counts, actionable items, running, recent', () => {
const jobs = [
{ id: 'a', stage: 'queued', priority: 'high', capabilities: ['os:any'] },
{ id: 'b', stage: 'building', priority: 'critical', factoryId: 'mac-1', leaseEpoch: 3 },
{ id: 'c', stage: 'review', updatedAt: '2026-01-01T00:00:02Z' },
{ id: 'd', stage: 'testing' },
{ id: 'e', stage: 'shipped', updatedAt: '2026-01-01T00:00:09Z' },
{ id: 'f', stage: 'failed', updatedAt: '2026-01-01T00:00:05Z' },
{ id: 'g', stage: 'dead_letter', updatedAt: '2026-01-01T00:00:01Z' },
];
const b = toBoard({ jobs });
assert.equal(b.counts.inbox, 1);
assert.equal(b.counts.building, 1);
assert.equal(b.counts.review, 1);
assert.equal(b.counts.testing, 1);
assert.equal(b.counts.shipped, 1);
assert.equal(b.counts.failed, 2); // failed + dead_letter
// running = assigned/building only
assert.deepEqual(b.running.map((x) => x.id), ['b']);
assert.equal(b.running[0].fleetStage, 'building');
assert.equal(b.running[0].factoryId, 'mac-1');
// actionable items exclude building/shipped, ordered review<testing<failed<inbox
assert.deepEqual(b.items.map((x) => x.id), ['c', 'd', 'f', 'g', 'a']);
// item.stage is the bucket (so dashboard gate()/STAGE_TAG reuse works)
assert.equal(b.items[0].stage, 'review');
assert.equal(b.items[4].stage, 'inbox');
// recent = shipped+failed, newest first, capped at 5
assert.deepEqual(b.recent.map((x) => x.id), ['e', 'f', 'g']);
});
// ── fleetFetch: headers + product scoping on every request ──
await t('fleetFetch: sends bearer + X-Product-Id; parses JSON', async () => {
const f = makeFetch({ '/fleet/jobs': { status: 200, body: { jobs: [] } } });
const r = await fleetFetch(CFG, '/fleet/jobs', {}, f);
assert.equal(r.ok, true);
assert.deepEqual(r.json, { jobs: [] });
const h = f.calls[0].headers;
assert.equal(h.Authorization, 'Bearer tok');
assert.equal(h['X-Product-Id'], 'prodX');
assert.equal(f.calls[0].url, 'http://svc/api/fleet/jobs');
});
await t('fleetFetch: network error ⇒ ok:false with message (no throw)', async () => {
const f = makeFetch({ '/fleet/jobs': { throw: 'boom' } });
const r = await fleetFetch(CFG, '/fleet/jobs', {}, f);
assert.equal(r.ok, false);
assert.equal(r.status, 0);
assert.match(r.error, /boom/);
});
await t('fleetFetch: abort ⇒ timeout error', async () => {
const f = makeFetch({ '/fleet/jobs': { throw: 'aborted', name: 'AbortError' } });
const r = await fleetFetch(CFG, '/fleet/jobs', {}, f);
assert.equal(r.ok, false);
assert.equal(r.error, 'timeout');
});
await t('fleetFetch: non-JSON 500 body ⇒ ok:false, json null', async () => {
const f = makeFetch({ '/fleet/jobs': { status: 500, body: '<html>err</html>' } });
const r = await fleetFetch(CFG, '/fleet/jobs', {}, f);
assert.equal(r.ok, false);
assert.equal(r.status, 500);
assert.equal(r.json, null);
});
// ── fetchBoard: assembly + degradation ──
await t('fetchBoard: jobs + metrics + factories assembled', async () => {
const f = makeFetch({
'/fleet/jobs': { body: { jobs: [{ id: 'a', stage: 'review' }] } },
'/fleet/metrics': { body: { utilizationPct: 50, alerts: [] } },
'/fleet/factories': { body: { factories: [{ factoryId: 'mac-1', health: 'ok' }] } },
});
const r = await fetchBoard(CFG, f);
assert.equal(r.ok, true);
assert.equal(r.board.items.length, 1);
assert.equal(r.board.metrics.utilizationPct, 50);
assert.equal(r.board.factories.length, 1);
});
await t('fetchBoard: factories 404 ⇒ degrades to []', async () => {
const f = makeFetch({
'/fleet/jobs': { body: { jobs: [] } },
'/fleet/metrics': { body: {} },
'/fleet/factories': { status: 404, body: {} },
});
const r = await fetchBoard(CFG, f);
assert.equal(r.ok, true);
assert.deepEqual(r.board.factories, []);
});
await t('fetchBoard: factories 501 ⇒ degrades to []', async () => {
const f = makeFetch({
'/fleet/jobs': { body: { jobs: [] } },
'/fleet/metrics': { body: {} },
'/fleet/factories': { status: 501, body: {} },
});
const r = await fetchBoard(CFG, f);
assert.equal(r.ok, true);
assert.deepEqual(r.board.factories, []);
});
await t('fetchBoard: metrics failure ⇒ board still ok, metrics null', async () => {
const f = makeFetch({
'/fleet/jobs': { body: { jobs: [] } },
'/fleet/metrics': { status: 500, body: 'oops' },
'/fleet/factories': { body: { factories: [] } },
});
const r = await fetchBoard(CFG, f);
assert.equal(r.ok, true);
assert.equal(r.board.metrics, null);
});
await t('fetchBoard: jobs failure ⇒ board fails with error', async () => {
const f = makeFetch({ '/fleet/jobs': { status: 503, body: '{}' } });
const r = await fetchBoard(CFG, f);
assert.equal(r.ok, false);
assert.match(r.error, /503/);
});
// ── events ──
t('formatEvent: renders type + actor + data', () => {
const line = formatEvent({ type: 'claimed', actor: 'mac-1', at: '2026-01-01T00:00:00Z', data: { leaseEpoch: 2 } });
assert.match(line, /claimed/);
assert.match(line, /mac-1/);
assert.match(line, /leaseEpoch/);
});
await t('fetchEvents: maps events to lines', async () => {
const f = makeFetch({
'/events': { body: { events: [{ type: 'queued', at: '2026-01-01T00:00:00Z', data: {} }, { type: 'claimed', data: {} }] } },
});
const r = await fetchEvents(CFG, 'job-1', f);
assert.equal(r.ok, true);
assert.equal(r.lines.length, 2);
assert.match(r.lines[1], /claimed/);
assert.match(f.calls[0].url, /\/fleet\/jobs\/job-1\/events$/);
});
await t('fetchEvents: failure ⇒ ok:false, empty lines', async () => {
const f = makeFetch({ '/events': { status: 500, body: 'x' } });
const r = await fetchEvents(CFG, 'job-1', f);
assert.equal(r.ok, false);
assert.deepEqual(r.lines, []);
});
// ── jobAction ──
await t('jobAction: ship re-GETs fresh leaseEpoch then PATCHes shipped', async () => {
let patchBody = null;
const f = makeFetch({
'http://svc/api/fleet/jobs/j1': ({ opts }) => {
if ((opts.method || 'GET') === 'PATCH') { patchBody = JSON.parse(opts.body); return { status: 200, body: { id: 'j1', stage: 'shipped' } }; }
return { status: 200, body: { id: 'j1', stage: 'testing', leaseEpoch: 7 } }; // fresh epoch
},
});
const r = await jobAction(CFG, { id: 'j1', leaseEpoch: 2 /* stale */ }, 'ship', f);
assert.equal(r.ok, true);
assert.equal(patchBody.stage, 'shipped');
assert.equal(patchBody.leaseEpoch, 7); // used the freshly-fetched epoch, not the stale 2
});
await t('jobAction: ship 409 ⇒ actionable fenced message', async () => {
const f = makeFetch({
'http://svc/api/fleet/jobs/j1': ({ opts }) => (opts.method === 'PATCH'
? { status: 409, body: '{}' }
: { status: 200, body: { id: 'j1', leaseEpoch: 7 } }),
});
const r = await jobAction(CFG, { id: 'j1' }, 'ship', f);
assert.equal(r.ok, false);
assert.match(r.message, /fenced|refresh/i);
});
await t('jobAction: requeue ⇒ POST /actions/requeue', async () => {
const f = makeFetch({ '/fleet/jobs/j1/actions/requeue': { status: 200, body: { id: 'j1', stage: 'queued' } } });
const r = await jobAction(CFG, { id: 'j1' }, 'requeue', f);
assert.equal(r.ok, true);
assert.equal(f.calls[0].method, 'POST');
assert.match(f.calls[0].url, /\/actions\/requeue$/);
});
await t('jobAction: reject 409 ⇒ conflict message', async () => {
const f = makeFetch({ '/fleet/jobs/j1/actions/reject': { status: 409, body: '{}' } });
const r = await jobAction(CFG, { id: 'j1' }, 'reject', f);
assert.equal(r.ok, false);
assert.match(r.message, /conflict|terminal|refresh/i);
});
t('jobAction: promote ⇒ explicitly unavailable in fleet mode', async () => {
return jobAction(CFG, { id: 'j1' }, 'promote', makeFetch({})).then((r) => {
assert.equal(r.ok, false);
assert.match(r.message, /promote/i);
});
});
})();
// Summary line (selftest greps for PASS).
process.on('exit', () => {
if (process.exitCode && process.exitCode !== 0) {
console.error('fleet-dash.test FAIL');
} else {
console.log(`fleet-dash.test PASS (${passed} assertions)`);
}
});