Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2450-archived-retired-refresh.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A Bot retired with `archive_bot` now appears in the sidebar's Archived section at once, instead of up to one 10 s roster poll later ([#2450](https://github.com/ThinkFlowLab/FlyBot/issues/2450)).
2 changes: 1 addition & 1 deletion plugins/tree/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@
try { next = typeof flybot.bots === 'function' ? await flybot.bots() : [] } catch { reading = false; return }
reading = false
bots = next ?? []
for (const fn of [...subs]) { try { fn(bots) } catch { /* one subscriber must not stop the feed */ } }

Check warning on line 338 in plugins/tree/client.js

View workflow job for this annotation

GitHub Actions / plugins (tree)

unicorn(no-useless-spread)

Using a spread operator here creates a new array unnecessarily.
if (again) { again = false; void read() }
}
return {
Expand Down Expand Up @@ -533,7 +533,7 @@
const applyBots = useCallback(list => setBots(archivedOf(list)), [])
useEffect(() => {
const offs = [feed.subscribe(applyBots)]
try { offs.push(flybot.fields?.on?.('change', c => { if (c && (c.key === 'archived' || c.key === 'parent' || c.key === 'treeRole' || c.removed)) feed.refresh() })) } catch { /* no change feed */ }
try { offs.push(flybot.fields?.on?.('change', c => { if (c && (c.key === 'archived' || c.key === 'parent' || c.key === 'treeRole' || c.key === 'retired' || c.removed)) feed.refresh() })) } catch { /* no change feed */ }
try { if (typeof flybot.on === 'function') offs.push(flybot.on('bot.removed', () => feed.refresh())) } catch { /* no removal event */ }
return () => { for (const off of offs) { try { off?.() } catch { /* already gone */ } } }
}, [applyBots, feed])
Expand Down
33 changes: 31 additions & 2 deletions plugins/tree/test/archived-section.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { readFileSync } from 'node:fs'
import { test } from 'node:test'
import { findAll, reactStub, renderToString } from './render.js'

const loadClient = () => {
const loadClient = (react = reactStub) => {
let mod
globalThis.window = { __ModuleLoader__: { load: ({ factory }) => { mod = factory(() => reactStub) } } }
globalThis.window = { __ModuleLoader__: { load: ({ factory }) => { mod = factory(() => react) } } }
new Function(readFileSync(new URL('../client.js', import.meta.url), 'utf8'))()
return mod
}
Expand Down Expand Up @@ -138,3 +138,32 @@ test('#810: the section is registered on the sidebar slot, and archived Bots sta
mod.apply({ flybot: { ...flybot, slots: { register: (name, entry) => { if (name === 'flybot.sidebar.section') off.push(entry.id); return () => {} } } } }, { archivedSection: false })
assert.ok(!off.includes('flybot-tree-archived'), 'config.archivedSection: false turns the section off')
})

// #2450: `archive_bot` hides the retired Bot from the default list, so this section is the way back
// to its chat. Its field-change subscription must refresh on `retired` itself, like the structure
// entry does (docs/subsystems/tree.md says both consumers refresh on `retired`), not one 10 s poll later.
test('#2450: a retired Bot reaches the Archived section on the change, not the next poll', () => {
const effects = []
const react = {
createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }),
useState: init => [typeof init === 'function' ? init() : init, () => {}],
useEffect: fn => effects.push(fn),
useCallback: fn => fn,
}
const mod = loadClient(react)
const calls = []
const feed = { subscribe: () => () => {}, refresh: () => calls.push('refresh'), get: () => [] }
const flybot = { fields: { on: (event, fn) => { if (event === 'change') calls.push(fn); return () => {} } } }
mod.makeArchivedSection(flybot, feed)({})
for (const fn of effects) fn()
const onChange = calls.find(v => typeof v === 'function')
assert.equal(typeof onChange, 'function', 'the section subscribes to field changes')

calls.length = 0
onChange({ key: 'parent' })
assert.equal(calls.filter(v => v === 'refresh').length, 1, 'control: parent still refreshes')

calls.length = 0
onChange({ key: 'retired', value: true })
assert.equal(calls.filter(v => v === 'refresh').length, 1, 'a retired change refreshes the archived list')
})
Loading