test/background-task-timing.test.js
Rama main · réplica de lectura del repositorio original.
var test = require("node:test");
var assert = require("node:assert");
var fs = require("node:fs");
var path = require("node:path");
var timing = require("../lib/background-task-timing");
var backgroundTasks = require("../lib/yoke/codex-background-tasks");
function task(id, extra) {
return Object.assign({ task_id: id, task_type: "shell", description: id }, extra || {});
}
// Realistic epoch milliseconds. Small integers would be read as epoch seconds
// by normalizeTimestamp's vendor heuristic and rescaled.
var T0 = 1767225600000; // 2026-01-01T00:00:00Z
test("a newly seen task is stamped with the current time", function () {
var merged = timing.mergeStartTimes([], [task("a")], T0);
assert.strictEqual(merged.length, 1);
assert.strictEqual(merged[0].started_at, T0);
assert.strictEqual(merged[0].task_id, "a");
assert.strictEqual(merged[0].description, "a");
});
test("an already-running task keeps its original stamp when the list is re-emitted", function () {
var first = timing.mergeStartTimes([], [task("a")], T0);
// A second task starting re-emits the whole list; "a" must not restart.
var second = timing.mergeStartTimes(first, [task("a"), task("b")], T0 + 8000);
assert.strictEqual(second[0].started_at, T0, "existing task keeps its first-seen time");
assert.strictEqual(second[1].started_at, T0 + 8000, "new task is stamped now");
var third = timing.mergeStartTimes(second, [task("a")], T0 + 19000);
assert.strictEqual(third[0].started_at, T0, "stamp survives a sibling finishing");
});
test("a task id reused after disappearing gets a fresh stamp", function () {
var first = timing.mergeStartTimes([], [task("a")], T0);
var cleared = timing.mergeStartTimes(first, [], T0 + 1000);
assert.deepStrictEqual(cleared, []);
var restarted = timing.mergeStartTimes(cleared, [task("a")], T0 + 4000);
assert.strictEqual(restarted[0].started_at, T0 + 4000);
});
test("a vendor-supplied start time wins over first-seen stamping", function () {
var merged = timing.mergeStartTimes([], [task("a", { started_at: T0 - 500 })], T0 + 8000);
assert.strictEqual(merged[0].started_at, T0 - 500);
});
test("a vendor timestamp still wins over a previously carried stamp", function () {
var first = timing.mergeStartTimes([], [task("a")], T0);
var second = timing.mergeStartTimes(first, [task("a", { started_at: T0 - 250 })], T0 + 8000);
assert.strictEqual(second[0].started_at, T0 - 250);
});
test("epoch seconds, ISO strings, and junk timestamps are normalized", function () {
assert.strictEqual(timing.normalizeTimestamp(1700000000), 1700000000000, "epoch seconds scale to ms");
assert.strictEqual(timing.normalizeTimestamp(1700000000000), 1700000000000, "epoch ms pass through");
assert.strictEqual(timing.normalizeTimestamp("2026-01-02T03:04:05Z"), Date.parse("2026-01-02T03:04:05Z"));
assert.strictEqual(timing.normalizeTimestamp("not a date"), null);
assert.strictEqual(timing.normalizeTimestamp(0), null);
assert.strictEqual(timing.normalizeTimestamp(-5), null);
assert.strictEqual(timing.normalizeTimestamp(null), null);
assert.strictEqual(timing.normalizeTimestamp(undefined), null);
});
test("an unusable vendor timestamp falls back to first-seen stamping, not 1970", function () {
var merged = timing.mergeStartTimes([], [task("a", { started_at: "garbage" })], T0 + 7000);
assert.strictEqual(merged[0].started_at, T0 + 7000);
});
test("merge tolerates non-array input and skips empty entries", function () {
assert.deepStrictEqual(timing.mergeStartTimes([], null, T0), []);
assert.deepStrictEqual(timing.mergeStartTimes(null, undefined, T0), []);
var merged = timing.mergeStartTimes(null, [null, task("a")], T0);
assert.strictEqual(merged.length, 1);
assert.strictEqual(merged[0].task_id, "a");
});
test("merge does not mutate the incoming task objects", function () {
var incoming = task("a");
timing.mergeStartTimes([], [incoming], T0);
assert.strictEqual(incoming.started_at, undefined);
});
test("Codex forwards a background terminal's own start time when present", function () {
var tasks = backgroundTasks.mapTerminals({ terminals: [
{ id: "term-1", command: "npm test", startedAt: 1700000000000 },
{ id: "term-2", command: "npm run dev" },
] });
assert.strictEqual(tasks[0].started_at, 1700000000000);
assert.strictEqual(tasks[1].started_at, undefined, "absent vendor time is left for the session to stamp");
});
// The client formatter is an ES module, so assert against its source rather
// than importing it into this CommonJS test.
function loadFormatElapsed() {
var source = fs.readFileSync(
path.join(__dirname, "..", "lib", "public", "modules", "background-tasks-ui.js"),
"utf8"
);
var start = source.indexOf("export function formatElapsed");
var end = source.indexOf("// Ambient tasks");
assert.ok(start !== -1 && end > start, "formatElapsed and its pad helper must be present");
var body = source.slice(start, end).replace("export function", "function");
return new Function(body + "\nreturn formatElapsed;")();
}
test("elapsed time renders as short, tabular-friendly labels", function () {
var formatElapsed = loadFormatElapsed();
var base = 1000000;
assert.strictEqual(formatElapsed(base, base), "0s");
assert.strictEqual(formatElapsed(base, base + 45 * 1000), "45s");
assert.strictEqual(formatElapsed(base, base + 59 * 1000), "59s");
assert.strictEqual(formatElapsed(base, base + 60 * 1000), "1m 00s");
assert.strictEqual(formatElapsed(base, base + 134 * 1000), "2m 14s");
assert.strictEqual(formatElapsed(base, base + 3599 * 1000), "59m 59s");
assert.strictEqual(formatElapsed(base, base + 3600 * 1000), "1h 00m");
assert.strictEqual(formatElapsed(base, base + 3780 * 1000), "1h 03m");
});
test("elapsed time is blank for a missing stamp and never negative", function () {
var formatElapsed = loadFormatElapsed();
assert.strictEqual(formatElapsed(0, 5000), "");
assert.strictEqual(formatElapsed(undefined, 5000), "");
assert.strictEqual(formatElapsed(NaN, 5000), "");
// Clock skew between server stamp and client render must not show "-3s".
assert.strictEqual(formatElapsed(10000, 7000), "0s");
});
test("the composer bar ships no emoji and no accent-filled count chip", function () {
var source = fs.readFileSync(
path.join(__dirname, "..", "lib", "public", "modules", "background-tasks-ui.js"),
"utf8"
);
assert.ok(source.indexOf("⏳") === -1, "the hourglass emoji is gone");
assert.ok(!/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(source), "no emoji in the rendered markup");
assert.ok(source.indexOf("background-tasks-chip") === -1, "the numeric accent chip is gone");
assert.ok(source.indexOf("iconHtml") !== -1, "icons come from the shared icon system");
});
test("the sidebar task-count badge keeps its own style rule", function () {
var css = fs.readFileSync(path.join(__dirname, "..", "lib", "public", "css", "input.css"), "utf8");
assert.ok(css.indexOf(".session-background-task-count") !== -1, "sidebar badge rule still exists");
assert.ok(css.indexOf(".background-tasks-chip") === -1, "composer chip rule is removed");
assert.ok(
/@media \(prefers-reduced-motion: reduce\)[^}]*\{[\s\S]*?\.background-tasks-activity i \{ animation: none/.test(css),
"the activity indicator has a reduced-motion fallback"
);
});
// --- ambient (housekeeping) tasks ------------------------------------------
test("the Claude adapter forwards the ambient flag and defaults it to false", function () {
var normalize = require("../lib/yoke/adapters/claude").contractTestKit.normalizeEvent;
var events = normalize({
type: "system",
subtype: "background_tasks_changed",
tasks: [
{ task_id: "t1", task_type: "local_bash", description: "npm test" },
{ task_id: "t2", task_type: "local_agent", description: "watcher", ambient: true },
{ task_id: "t3", task_type: "local_bash", description: "explicit", ambient: false },
],
});
var result = Array.isArray(events) ? events[0] : events;
assert.strictEqual(result.tasks[0].ambient, false, "absent normalizes to false, not undefined");
assert.strictEqual(result.tasks[1].ambient, true);
assert.strictEqual(result.tasks[2].ambient, false);
// The existing normalization must survive alongside the new field.
assert.strictEqual(result.tasks[0].task_type, "shell");
assert.strictEqual(result.tasks[1].task_type, "agent");
});
test("the start-time merge preserves the ambient flag", function () {
var merged = timing.mergeStartTimes([], [
task("a", { ambient: true }),
task("b", { ambient: false }),
], T0);
assert.strictEqual(merged[0].ambient, true);
assert.strictEqual(merged[1].ambient, false);
// And across a re-emission, alongside the carried-forward stamp.
var again = timing.mergeStartTimes(merged, [task("a", { ambient: true })], T0 + 5000);
assert.strictEqual(again[0].ambient, true);
assert.strictEqual(again[0].started_at, T0, "ambient tasks keep their elapsed clock too");
});
test("Codex tasks carry no ambient flag and count as user work", function () {
var tasks = backgroundTasks.mapTerminals({ terminals: [{ id: "term-1", command: "npm test" }] });
assert.strictEqual(tasks[0].ambient, undefined, "Codex has no ambient concept");
assert.strictEqual(timing.countUserTasks(tasks), 1, "absent must not be read as ambient");
});
test("counting and splitting ignore ambient housekeeping", function () {
var list = [
task("user-1"),
task("amb-1", { ambient: true }),
task("user-2", { ambient: false }),
task("amb-2", { ambient: true }),
];
assert.strictEqual(timing.countUserTasks(list), 2);
assert.deepStrictEqual(timing.userTasks(list).map(function (t) { return t.task_id; }), ["user-1", "user-2"]);
assert.deepStrictEqual(timing.ambientTasks(list).map(function (t) { return t.task_id; }), ["amb-1", "amb-2"]);
// Ambient-only reads as "no user work running".
assert.strictEqual(timing.countUserTasks([task("amb", { ambient: true })]), 0);
assert.strictEqual(timing.countUserTasks([]), 0);
assert.strictEqual(timing.countUserTasks(null), 0);
});
test("the sidebar badge and change notifications count user work only", function () {
// sessions.js feeds every sidebar badge site from this one field, and
// sdk-message-processor decides whether to rebroadcast the session list from
// the same count. Both must ignore ambient watchers.
var sessions = fs.readFileSync(path.join(__dirname, "..", "lib", "sessions.js"), "utf8");
assert.ok(
/backgroundTaskCount: backgroundTaskTiming\.countUserTasks\(s\.activeBackgroundTasks\)/.test(sessions),
"the sidebar badge counts non-ambient tasks"
);
var processor = fs.readFileSync(path.join(__dirname, "..", "lib", "sdk-message-processor.js"), "utf8");
assert.strictEqual(
(processor.match(/backgroundTaskTiming\.countUserTasks\(/g) || []).length, 3,
"both the init reset and the changed handler's before/after counts use it"
);
assert.ok(
!/previousBackgroundTaskCount !== activeBackgroundTasks\.length/.test(processor),
"the raw length comparison is gone"
);
});
// --- client rendering ------------------------------------------------------
function loadClientHelpers() {
var source = fs.readFileSync(
path.join(__dirname, "..", "lib", "public", "modules", "background-tasks-ui.js"),
"utf8"
);
var start = source.indexOf("export function isAmbientTask");
var end = source.indexOf("function renderBackgroundTasks");
assert.ok(start !== -1 && end > start, "isAmbientTask and splitTasks must be present");
var body = source.slice(start, end).replace(/export function/g, "function");
return new Function(body + "\nreturn { isAmbientTask: isAmbientTask, splitTasks: splitTasks };")();
}
test("the composer splits user work from housekeeping", function () {
var helpers = loadClientHelpers();
var split = helpers.splitTasks([
{ task_id: "u1", description: "build" },
{ task_id: "a1", description: "watcher", ambient: true },
{ task_id: "u2", description: "test", ambient: false },
null,
]);
assert.deepStrictEqual(split.user.map(function (t) { return t.task_id; }), ["u1", "u2"]);
assert.deepStrictEqual(split.ambient.map(function (t) { return t.task_id; }), ["a1"]);
assert.deepStrictEqual(helpers.splitTasks(null), { user: [], ambient: [] });
});
test("native task tabs hide ambient-only work and reuse Worker chips", function () {
var source = fs.readFileSync(path.join(__dirname, "..", "lib/public/modules/background-tasks-ui.js"), "utf8");
assert.match(source, /split.user.length \? split.user.concat\(split.ambient\) : \[\]/);
assert.match(source, /split-view-worker-chip/);
assert.match(source, /background_task_stop/);
assert.match(source, /Housekeeping/);
});