diff --git a/changelog.d/2450-archived-retired-refresh.fixed.md b/changelog.d/2450-archived-retired-refresh.fixed.md new file mode 100644 index 000000000..2683c9b4c --- /dev/null +++ b/changelog.d/2450-archived-retired-refresh.fixed.md @@ -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)). diff --git a/plugins/tree/client.js b/plugins/tree/client.js index 08c2def5e..0c4694e03 100644 --- a/plugins/tree/client.js +++ b/plugins/tree/client.js @@ -533,7 +533,7 @@ window.__ModuleLoader__.load({ 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]) diff --git a/plugins/tree/test/archived-section.test.js b/plugins/tree/test/archived-section.test.js index a59ac394c..302e2c066 100644 --- a/plugins/tree/test/archived-section.test.js +++ b/plugins/tree/test/archived-section.test.js @@ -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 } @@ -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') +})