|
1 | 1 | import assert from 'node:assert/strict' |
2 | 2 | import test from 'node:test' |
3 | | -import { ROOM_CONTEXT_BUDGET, boardCoversCue, compactStatus, fitRoomLines, formatRoomBoard, prepareRoomLine, statusShaped } from '../src/host/room-context.js' |
| 3 | +import { ROOM_CONTEXT_BUDGET, ROOM_PRESENCE_BUDGET, boardCoversCue, compactStatus, fitRoomLines, formatRoomBoard, formatRoomPresence, prepareRoomLine, statusShaped } from '../src/host/room-context.js' |
4 | 4 |
|
5 | 5 | const render = line => `${line.who}: ${line.text}` |
6 | 6 | const essay = (tail) => `CI on tip abcdef12 is green. tests-ok passed. enqueue when mergeable. merge_group is next. PASS at abcdef12. @Chief ${'detail '.repeat(80)}${tail}` |
@@ -103,3 +103,85 @@ test('an open card covers a status @ and not a question or a new ask', () => { |
103 | 103 | assert.equal(boardCoversCue(tasks, { roomId: 'room', botId: 'other', text: '@Other Writer implements' }), false) |
104 | 104 | assert.equal(boardCoversCue({ only: { ...tasks.waiting } }, { ...cue, text: '@Writer done' }), false) |
105 | 105 | }) |
| 106 | + |
| 107 | +// #1213: the room shows who is on what, read from the board, so a member that claimed |
| 108 | +// and stayed quiet is not read as idle. These are the fixtures the issue asks for: two |
| 109 | +// holders and one idle member, and a holder whose card is removed. |
| 110 | +const presenceAt = Date.parse('2026-10-26T02:00:00Z') |
| 111 | +const presenceMembers = [{ id: 'dev2', name: 'Dev2' }, { id: 'dev3', name: 'Dev3' }, { id: 'chief', name: 'Chief' }] |
| 112 | +const presenceTasks = () => ({ |
| 113 | + race: { id: 'race', status: 'working', progress: 15, title: 'Connectors test race', ownerBotId: 'dev2', origin: { roomId: 'room' }, target: { number: 1081 }, createdAt: presenceAt - 27 * 60000, updatedAt: presenceAt - 27 * 60000 }, |
| 114 | + recursion: { id: 'recursion', status: 'claimed', progress: 0, title: 'Face room recursion', ownerBotId: 'dev3', origin: { roomId: 'room' }, target: { number: 979 }, createdAt: presenceAt - 3 * 60000, updatedAt: presenceAt - 3 * 60000 }, |
| 115 | + done: { id: 'done', status: 'accepted', title: 'FINISHED_MARKER', ownerBotId: 'dev3', origin: { roomId: 'room' }, updatedAt: presenceAt }, |
| 116 | + elsewhere: { id: 'elsewhere', status: 'working', progress: 50, title: 'OTHER_ROOM', ownerBotId: 'dev2', origin: { roomId: 'elsewhere' }, updatedAt: presenceAt }, |
| 117 | +}) |
| 118 | + |
| 119 | +test('presence: two holders and an idle member each get one line, with column, progress and age', () => { |
| 120 | + const text = formatRoomPresence(presenceTasks(), { roomId: 'room', members: presenceMembers, now: presenceAt }) |
| 121 | + assert.match(text, /^- Dev2 — #1081 Connectors test race \(in progress 15%, open 27 min\)$/m) |
| 122 | + assert.match(text, /^- Dev3 — #979 Face room recursion \(not started, just claimed, open 3 min\)$/m) |
| 123 | + assert.match(text, /^- Chief — idle \(no open card\)$/m) |
| 124 | + assert.doesNotMatch(text, /FINISHED_MARKER|OTHER_ROOM/) |
| 125 | + assert.ok(text.startsWith('Who is working')) |
| 126 | +}) |
| 127 | + |
| 128 | +test('presence: a card removed turns its holder into idle (the strip follows the board)', () => { |
| 129 | + const tasks = presenceTasks() |
| 130 | + delete tasks.recursion |
| 131 | + const text = formatRoomPresence(tasks, { roomId: 'room', members: presenceMembers, now: presenceAt }) |
| 132 | + assert.doesNotMatch(text, /Dev3 — #979/) |
| 133 | + assert.match(text, /^- Dev3 — idle \(no open card\)$/m) |
| 134 | + assert.match(text, /^- Dev2 — #1081/m) |
| 135 | +}) |
| 136 | + |
| 137 | +test('presence: a waiting card says so, and a moved card adds when it moved', () => { |
| 138 | + const tasks = { |
| 139 | + ask: { id: 'ask', status: 'blocked', waitingFor: 'input', progress: 40, title: 'Need a name', ownerBotId: 'dev2', origin: { roomId: 'room' }, createdAt: presenceAt - 12 * 60000, updatedAt: presenceAt - 2 * 60000 }, |
| 140 | + } |
| 141 | + const text = formatRoomPresence(tasks, { roomId: 'room', members: [{ id: 'dev2', name: 'Dev2' }], now: presenceAt }) |
| 142 | + assert.match(text, /- Dev2 — Need a name \(waiting on an answer, open 12 min, moved 2 min ago\)/) |
| 143 | +}) |
| 144 | + |
| 145 | +test('presence: a holder with three cards names the first and counts the rest', () => { |
| 146 | + const tasks = { |
| 147 | + one: { id: 'one', status: 'working', progress: 10, title: 'First card', ownerBotId: 'dev2', origin: { roomId: 'room' }, createdAt: presenceAt, updatedAt: presenceAt }, |
| 148 | + two: { id: 'two', status: 'open', progress: 0, title: 'Second card', ownerBotId: 'dev2', origin: { roomId: 'room' }, createdAt: presenceAt, updatedAt: presenceAt }, |
| 149 | + three: { id: 'three', status: 'open', progress: 0, title: 'Third card', ownerBotId: 'dev2', origin: { roomId: 'room' }, createdAt: presenceAt, updatedAt: presenceAt }, |
| 150 | + } |
| 151 | + const text = formatRoomPresence(tasks, { roomId: 'room', members: [{ id: 'dev2', name: 'Dev2' }], now: presenceAt }) |
| 152 | + assert.match(text, /^- Dev2 — First card \(in progress 10%, open 0 min\) \+2 more$/m) |
| 153 | +}) |
| 154 | + |
| 155 | +test('presence: nobody holding a card is one readable line, never an empty block', () => { |
| 156 | + assert.equal(formatRoomPresence({}, { roomId: 'room', members: presenceMembers, now: presenceAt }), 'Who is working: nobody (no open card).') |
| 157 | + assert.equal(formatRoomPresence(presenceTasks(), { roomId: 'no-such-room', members: presenceMembers, now: presenceAt }), 'Who is working: nobody (no open card).') |
| 158 | +}) |
| 159 | + |
| 160 | +test('presence: at most eight members, the rest counted, and the whole block stays under the budget', () => { |
| 161 | + const tasks = {} |
| 162 | + const members = [] |
| 163 | + for (let at = 0; at < 12; at += 1) { |
| 164 | + const id = `bot${at}` |
| 165 | + members.push({ id, name: `Bot${at}` }) |
| 166 | + tasks[id] = { id, status: 'working', progress: 50, title: `Card ${at}`, ownerBotId: id, origin: { roomId: 'room' }, createdAt: presenceAt, updatedAt: presenceAt } |
| 167 | + } |
| 168 | + const text = formatRoomPresence(tasks, { roomId: 'room', members, now: presenceAt }) |
| 169 | + const memberLines = text.split('\n').filter(line => line.startsWith('- ') && line.includes(' — ')) |
| 170 | + assert.ok(memberLines.length <= 8, `at most eight member lines, got ${memberLines.length}`) |
| 171 | + assert.match(text, /^- \+\d+ more member\(s\)$/m) |
| 172 | + assert.ok(text.length <= ROOM_PRESENCE_BUDGET, `block is ${text.length} chars`) |
| 173 | +}) |
| 174 | + |
| 175 | +test('presence: a long title is flattened and cut, and no note prose rides along', () => { |
| 176 | + const tasks = { |
| 177 | + long: { id: 'long', status: 'working', progress: 5, title: `${'x'.repeat(300)}<b>`, note: 'NOTE_PROSE '.repeat(40), ownerBotId: 'dev2', origin: { roomId: 'room' }, createdAt: presenceAt, updatedAt: presenceAt }, |
| 178 | + } |
| 179 | + const text = formatRoomPresence(tasks, { roomId: 'room', members: [{ id: 'dev2', name: 'Dev2' }], now: presenceAt }) |
| 180 | + assert.doesNotMatch(text, /NOTE_PROSE|<b>/) |
| 181 | + assert.ok(text.length <= ROOM_PRESENCE_BUDGET) |
| 182 | +}) |
| 183 | + |
| 184 | +test('presence: no members or no room renders nothing at all', () => { |
| 185 | + assert.equal(formatRoomPresence(presenceTasks(), { roomId: 'room', members: [], now: presenceAt }), '') |
| 186 | + assert.equal(formatRoomPresence(presenceTasks(), { members: presenceMembers, now: presenceAt }), '') |
| 187 | +}) |
0 commit comments