From 2f83075719bbca0eb3682c61dbf4fe397894baff Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Mon, 6 Jul 2026 09:54:23 +0000 Subject: feat(web): add ensureTaskLogStream and taskLogStreams for task card log streaming (task 6) Co-Authored-By: Claude Sonnet 4.6 --- web/test/tasks-board.test.mjs | 77 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) (limited to 'web/test/tasks-board.test.mjs') 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'); + }); +}); -- cgit v1.2.3