summaryrefslogtreecommitdiff
path: root/web/test/tasks-board.test.mjs
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator.local>2026-07-06 09:54:23 +0000
committerClaudomator Agent <agent@claudomator.local>2026-07-06 09:54:23 +0000
commit2f83075719bbca0eb3682c61dbf4fe397894baff (patch)
tree4bd4a903eb1529af951818a5a5f4eb4046687f5f /web/test/tasks-board.test.mjs
parent4344e4d49a94d5d636f4667812b556566ef71849 (diff)
feat(web): add ensureTaskLogStream and taskLogStreams for task card log streaming (task 6)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/test/tasks-board.test.mjs')
-rw-r--r--web/test/tasks-board.test.mjs77
1 files changed, 77 insertions, 0 deletions
diff --git a/web/test/tasks-board.test.mjs b/web/test/tasks-board.test.mjs
index 1e35e18..2e1f276 100644
--- a/web/test/tasks-board.test.mjs
+++ b/web/test/tasks-board.test.mjs
@@ -249,3 +249,80 @@ describe('cardContentSignature', () => {
assert.notEqual(cardContentSignature(cardA), cardContentSignature(cardB));
});
});
+
+// ── ensureTaskLogStream: stream lifecycle (reuse vs. reopen vs. no-op) ─────
+
+import { ensureTaskLogStream, taskLogStreams } from '../app.js';
+
+function makeFakeEventSource() {
+ const instances = [];
+ function FakeEventSource(url) {
+ this.url = url;
+ this.closed = false;
+ this._listeners = {};
+ instances.push(this);
+ }
+ FakeEventSource.prototype.close = function () { this.closed = true; };
+ FakeEventSource.prototype.addEventListener = function (type, fn) { this._listeners[type] = fn; };
+ Object.defineProperty(FakeEventSource.prototype, 'onmessage', { writable: true, value: null });
+ Object.defineProperty(FakeEventSource.prototype, 'onerror', { writable: true, value: null });
+ FakeEventSource.instances = instances;
+ return FakeEventSource;
+}
+
+function makeFakeLogArea() {
+ return {
+ children: [],
+ appendChild(c) { this.children.push(c); },
+ removeChild(c) { this.children = this.children.filter(x => x !== c); },
+ get childElementCount() { return this.children.length; },
+ get firstElementChild() { return this.children[0]; },
+ get innerHTML() { return ''; },
+ set innerHTML(_) { this.children = []; }, // mirrors real DOM: assigning innerHTML clears children
+ scrollTop: 0, scrollHeight: 0, clientHeight: 0, addEventListener() {},
+ };
+}
+
+describe('ensureTaskLogStream', () => {
+ it('does nothing when the task has no executions yet (Queue)', async () => {
+ const fetchFn = async () => ({ ok: true, json: async () => [] });
+ const FakeES = makeFakeEventSource();
+ const logArea = makeFakeLogArea();
+ await ensureTaskLogStream('task-queue-1', logArea, { fetchFn, EventSourceImpl: FakeES, apiBase: '' });
+ assert.equal(FakeES.instances.length, 0);
+ });
+
+ it('opens a stream for a task with an execution', async () => {
+ const fetchFn = async () => ({ ok: true, json: async () => [{ id: 'exec-1' }] });
+ const FakeES = makeFakeEventSource();
+ const logArea = makeFakeLogArea();
+ await ensureTaskLogStream('task-1', logArea, { fetchFn, EventSourceImpl: FakeES, apiBase: '' });
+ assert.equal(FakeES.instances.length, 1);
+ assert.match(FakeES.instances[0].url, /\/api\/executions\/exec-1\/logs\/stream/);
+ assert.equal(taskLogStreams['task-1'].execId, 'exec-1');
+ });
+
+ it('does not reopen a stream already attached to the same execution', async () => {
+ const fetchFn = async () => ({ ok: true, json: async () => [{ id: 'exec-2' }] });
+ const FakeES = makeFakeEventSource();
+ const logArea = makeFakeLogArea();
+ await ensureTaskLogStream('task-2', logArea, { fetchFn, EventSourceImpl: FakeES, apiBase: '' });
+ await ensureTaskLogStream('task-2', logArea, { fetchFn, EventSourceImpl: FakeES, apiBase: '' });
+ assert.equal(FakeES.instances.length, 1, 'should not open a second stream for the same execution');
+ });
+
+ it('closes the old stream and opens a new one when the execution id changes', async () => {
+ let call = 0;
+ const fetchFn = async () => {
+ call++;
+ return { ok: true, json: async () => [{ id: call === 1 ? 'exec-a' : 'exec-b' }] };
+ };
+ const FakeES = makeFakeEventSource();
+ const logArea = makeFakeLogArea();
+ await ensureTaskLogStream('task-3', logArea, { fetchFn, EventSourceImpl: FakeES, apiBase: '' });
+ await ensureTaskLogStream('task-3', logArea, { fetchFn, EventSourceImpl: FakeES, apiBase: '' });
+ assert.equal(FakeES.instances.length, 2);
+ assert.ok(FakeES.instances[0].closed, 'old stream should be closed');
+ assert.equal(taskLogStreams['task-3'].execId, 'exec-b');
+ });
+});