From b429d7054329cd4553087afab96e5a21157c7ade Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:59:44 +0800 Subject: [PATCH] fix(tree): refresh the Archived section when a Bot is retired archive_bot hides a retired Bot from the default list, so the sidebar's Archived section is the only way back to its chat. That section's field-change subscription covered archived/parent/treeRole/removed but not retired, while the structure entry already reacts to it and docs/subsystems/tree.md states both consumers refresh on it. A retired Bot now appears in the Archived section on the change itself rather than on the next 10 s roster poll. The archived-section test harness takes the react stub it renders with, so the new test can run the section's effects and fire the change. Docs-not-needed: the behaviour is already stated in docs/subsystems/tree.md (both consumers refresh on `retired`); this commit makes client.js match it. Fixes #2450 --- .../2450-archived-retired-refresh.fixed.md | 1 + plugins/tree/client.js | 2 +- plugins/tree/test/archived-section.test.js | 33 +++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 changelog.d/2450-archived-retired-refresh.fixed.md 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') +})