dsh-im-ops/test/workspace-editor.test.mjs

814 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import test from 'node:test';
import * as React from 'react';
import TestRenderer from 'react-test-renderer';
import {
AgentPresetCatalogContext,
AgentPresetEditor,
} from '../plugin-src/client/agent-preset.js';
import {
WorkspaceDirectoryPickerContext,
WorkspaceEditor,
} from '../plugin-src/client/workspace-editor.js';
import { DiscordSettingsTab } from '../plugin-src/client/channels/discord/index.js';
import { en, setImTranslator } from '../plugin-src/client/i18n.js';
const { act, create } = TestRenderer;
function deferred() {
let resolve;
const promise = new Promise((onResolve) => { resolve = onResolve; });
return { promise, resolve };
}
async function flushMicrotasks() {
for (let index = 0; index < 6; index += 1) await Promise.resolve();
}
function directoryListing(path, childNames = [], { home = '/workspace', truncated = false } = {}) {
let cursor = '';
const crumbs = [{ name: '/', path: '/', hidden: false }];
for (const name of path.split('/').filter(Boolean)) {
cursor += `/${name}`;
crumbs.push({ name, path: cursor, hidden: false });
}
return {
path,
home,
crumbs,
entries: childNames.map((name) => ({
name,
path: `${path === '/' ? '' : path}/${name}`,
hidden: name.startsWith('.'),
})),
truncated,
};
}
function nativeUnavailable() {
const error = new Error('Directory browsing is unavailable');
error.rpcError = {
code: 'directory-picker-unavailable',
message: error.message,
details: { capability: 'native' },
};
return error;
}
function nativeDirectoryPicker(selected) {
const calls = { list: 0, pick: 0 };
return {
calls,
async listDirectory() {
calls.list += 1;
throw nativeUnavailable();
},
async pickDirectory() {
calls.pick += 1;
return selected;
},
};
}
function withDirectoryPicker(element, picker) {
return React.createElement(
WorkspaceDirectoryPickerContext.Provider,
{ value: picker },
element,
);
}
function textOf(node) {
if (typeof node === 'string' || typeof node === 'number') return String(node);
return node?.children?.map(textOf).join('') ?? '';
}
function buttonNamed(root, name) {
return root.findAllByType('button').find((button) => textOf(button) === name);
}
function discordSnapshot(workspace) {
return {
revision: 1,
bots: [{
botId: 'discord_test',
connected: true,
state: 'connected',
workspace,
bot: { name: 'Harness Bot', username: 'HarnessBot', idMasked: '123•••' },
health: { summary: 'Discord Gateway 长连接运行正常', lastCheckedAt: Date.now() },
error: null,
}],
};
}
function twoBotDiscordSnapshot(firstWorkspace) {
const first = discordSnapshot(firstWorkspace).bots[0];
return {
revision: 1,
bots: [
{ ...first, botId: 'discord_first', bot: { ...first.bot, name: 'First Bot' } },
{
...first,
botId: 'discord_second',
workspace: '/workspace/second',
bot: { ...first.bot, name: 'Second Bot' },
},
],
};
}
test('WorkspaceEditor browses from the current path and saves the selected directory', async () => {
const saved = [];
const listed = [];
const picker = {
async listDirectory(path) {
listed.push(path);
if (path === '/workspace/current') {
return directoryListing(path, ['next project', '.hidden']);
}
if (path === '/workspace/current/next project') return directoryListing(path);
throw new Error(`Unexpected directory: ${path}`);
},
async pickDirectory() { throw new Error('native picker should not run'); },
};
function Fixture() {
const [workspace, setWorkspace] = React.useState('/workspace/current');
return React.createElement(WorkspaceEditor, {
workspace,
directoryPicker: picker,
async onSave(value) {
saved.push(value);
setWorkspace(value);
},
});
}
const bodyNode = { scrollTop: 0 };
let renderer;
await act(async () => {
renderer = create(React.createElement(Fixture), {
createNodeMock(element) {
if (element.props?.className === 'dim-directoryPickerBody') return bodyNode;
return {};
},
});
});
assert.equal(renderer.root.findByType('code').props.title, '/workspace/current');
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
const dialog = renderer.root.findByProps({ role: 'dialog' });
assert.equal(dialog.props['aria-modal'], 'true');
assert.equal(
textOf(renderer.root.findByProps({ id: dialog.props['aria-describedby'] })),
'切换后会清除这个机器人的旧会话映射。',
);
assert.equal(renderer.root.findAllByProps({ title: '/workspace/current/.hidden' }).length, 0);
bodyNode.scrollTop = 240;
await act(async () => {
renderer.root.findByProps({ title: '/workspace/current/next project' }).props.onClick();
await flushMicrotasks();
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.onClick();
await flushMicrotasks();
});
assert.deepEqual(listed, ['/workspace/current', '/workspace/current/next project']);
assert.equal(bodyNode.scrollTop, 0);
assert.deepEqual(saved, ['/workspace/current/next project']);
assert.equal(renderer.root.findByType('code').props.title, '/workspace/current/next project');
});
test('WorkspaceEditor keeps the picker open and presents a rejected workspace error', async () => {
const picker = {
async listDirectory(path) {
return path === '/workspace/current'
? directoryListing(path, ['missing'])
: directoryListing(path);
},
};
let renderer;
await act(async () => {
renderer = create(React.createElement(WorkspaceEditor, {
workspace: '/workspace/current',
directoryPicker: picker,
async onSave() {
const error = new Error('工作区路径不存在。');
error.code = 'workspace-not-found';
throw error;
},
}));
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
await act(async () => {
renderer.root.findByProps({ title: '/workspace/current/missing' }).props.onClick();
await flushMicrotasks();
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.onClick();
await flushMicrotasks();
});
assert.equal(textOf(renderer.root.findByProps({ role: 'alert' })), '工作区路径不存在。');
assert.equal(renderer.root.findByProps({ role: 'dialog' }).props['aria-modal'], 'true');
assert.equal(renderer.root.findByType('code').props.title, '/workspace/current');
});
test('WorkspaceEditor closes without saving when the current directory is selected', async () => {
let saves = 0;
const picker = { async listDirectory(path) { return directoryListing(path); } };
let renderer;
await act(async () => {
renderer = create(React.createElement(WorkspaceEditor, {
workspace: '/workspace/current',
directoryPicker: picker,
async onSave() { saves += 1; },
}));
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.onClick();
await flushMicrotasks();
});
assert.equal(saves, 0);
assert.equal(renderer.root.findAllByProps({ role: 'dialog' }).length, 0);
});
test('WorkspaceEditor falls back to one native picker without restarting after save', async () => {
const saved = [];
const picker = nativeDirectoryPicker('/workspace/native');
function Fixture() {
const [workspace, setWorkspace] = React.useState('/workspace/current');
return React.createElement(WorkspaceEditor, {
workspace,
directoryPicker: picker,
async onSave(value) {
saved.push(value);
setWorkspace(value);
},
});
}
let renderer;
await act(async () => { renderer = create(React.createElement(Fixture)); });
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
assert.deepEqual(saved, ['/workspace/native']);
assert.deepEqual(picker.calls, { list: 1, pick: 1 });
assert.equal(renderer.root.findByType('code').props.title, '/workspace/native');
assert.equal(renderer.root.findAllByProps({ role: 'dialog' }).length, 0);
});
test('WorkspaceEditor treats native picker cancellation as cancellation, not an error', async () => {
let saves = 0;
const picker = nativeDirectoryPicker(null);
let renderer;
await act(async () => {
renderer = create(React.createElement(WorkspaceEditor, {
workspace: '/workspace/current',
directoryPicker: picker,
async onSave() { saves += 1; },
}));
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
assert.equal(saves, 0);
assert.deepEqual(picker.calls, { list: 1, pick: 1 });
assert.equal(renderer.root.findAllByProps({ role: 'alert' }).length, 0);
assert.equal(renderer.root.findAllByProps({ role: 'dialog' }).length, 0);
});
test('WorkspaceEditor falls back to the Host home when the saved path is unreadable', async () => {
const listed = [];
const saved = [];
const picker = {
async listDirectory(path) {
listed.push(path);
if (path === '/workspace/gone') {
const error = new Error('missing');
error.rpcError = { code: 'directory-unreadable', message: 'missing', details: { path } };
throw error;
}
return directoryListing('/workspace', ['projects']);
},
};
let renderer;
await act(async () => {
renderer = create(React.createElement(WorkspaceEditor, {
workspace: '/workspace/gone',
directoryPicker: picker,
async onSave(value) { saved.push(value); },
}));
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
assert.deepEqual(listed, ['/workspace/gone', undefined]);
await act(async () => {
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.onClick();
await flushMicrotasks();
});
assert.deepEqual(saved, ['/workspace']);
});
test('WorkspaceEditor moves keyboard focus into and back out of the picker', async () => {
let dialogFocus = 0;
let editFocus = 0;
const picker = { async listDirectory(path) { return directoryListing(path); } };
let renderer;
await act(async () => {
renderer = create(React.createElement(WorkspaceEditor, {
workspace: '/workspace/current',
directoryPicker: picker,
async onSave() {},
}), {
createNodeMock(element) {
if (element.props?.className === 'dim-directoryPicker') {
return { focus() { dialogFocus += 1; } };
}
if (element.props?.className === 'dim-workspaceEdit') {
return { focus() { editFocus += 1; } };
}
return {};
},
});
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
assert.equal(dialogFocus, 1);
await act(async () => {
buttonNamed(renderer.root, '取消').props.onClick();
await flushMicrotasks();
});
assert.equal(editFocus, 1);
});
test('WorkspaceEditor never translates Host filesystem names in the English UI', async (t) => {
setImTranslator((key) => en[key] ?? key);
t.after(() => setImTranslator(null));
const picker = {
async listDirectory(path) { return directoryListing(path, ['微信']); },
};
let renderer;
await act(async () => {
renderer = create(React.createElement(WorkspaceEditor, {
workspace: '/workspace/current',
directoryPicker: picker,
async onSave() {},
}));
await flushMicrotasks();
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
await flushMicrotasks();
});
const directory = renderer.root.findByProps({ title: '/workspace/current/微信' });
assert.equal(textOf(directory), '微信');
assert.doesNotMatch(textOf(directory), /WeChat/);
await act(async () => { renderer.unmount(); });
});
test('a status response started before saving cannot restore the old workspace', async (t) => {
const previousWindow = globalThis.window;
let intervalCallback;
globalThis.window = {
setInterval(callback) { intervalCallback = callback; return 1; },
clearInterval() {},
};
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const staleStatus = deferred();
let statusCalls = 0;
const rpcCall = async (endpoint) => {
if (endpoint === 'connection.status') {
statusCalls += 1;
if (statusCalls === 1) return { ok: true, value: discordSnapshot('/workspace/current') };
if (statusCalls === 2) return staleStatus.promise;
return { ok: true, value: discordSnapshot('/workspace/new') };
}
if (endpoint === 'bot.workspace.set') {
return { ok: true, value: discordSnapshot('/workspace/new') };
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
const picker = nativeDirectoryPicker('/workspace/new');
await act(async () => {
renderer = create(withDirectoryPicker(
React.createElement(DiscordSettingsTab, { rpcCall }),
picker,
));
await flushMicrotasks();
});
await act(async () => {
intervalCallback();
await flushMicrotasks();
});
await act(async () => {
buttonNamed(renderer.root, '选择目录').props.onClick();
await flushMicrotasks();
});
assert.equal(renderer.root.findByType('code').props.title, '/workspace/new');
await act(async () => {
staleStatus.resolve({ ok: true, value: discordSnapshot('/workspace/old') });
await flushMicrotasks();
});
assert.equal(renderer.root.findByType('code').props.title, '/workspace/new');
await act(async () => { renderer.unmount(); });
});
test('an older reconnect snapshot from another bot cannot restore a saved workspace', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const staleReconnect = deferred();
let statusCalls = 0;
const rpcCall = async (endpoint) => {
if (endpoint === 'connection.status') {
statusCalls += 1;
return {
ok: true,
value: twoBotDiscordSnapshot(statusCalls === 1 ? '/workspace/current' : '/workspace/new'),
};
}
if (endpoint === 'bot.reconnect') return staleReconnect.promise;
if (endpoint === 'bot.workspace.set') {
return { ok: true, value: twoBotDiscordSnapshot('/workspace/new') };
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
const picker = nativeDirectoryPicker('/workspace/new');
await act(async () => {
renderer = create(withDirectoryPicker(
React.createElement(DiscordSettingsTab, { rpcCall }),
picker,
));
await flushMicrotasks();
});
const firstCard = renderer.root.findByProps({ 'data-bot-id': 'discord_first' });
const secondCard = renderer.root.findByProps({ 'data-bot-id': 'discord_second' });
await act(async () => {
secondCard.findAllByType('button')
.find((button) => button.children.join('') === '检查连接').props.onClick();
await flushMicrotasks();
});
await act(async () => {
buttonNamed(firstCard, '选择目录').props.onClick();
await flushMicrotasks();
});
staleReconnect.resolve({ ok: true, value: twoBotDiscordSnapshot('/workspace/old') });
await act(async () => { await flushMicrotasks(); });
assert.equal(
renderer.root.findByProps({ 'data-bot-id': 'discord_first' }).findByType('code').props.title,
'/workspace/new',
);
await act(async () => { renderer.unmount(); });
});
test('connection check requests a test message and shows its delivery result', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const calls = [];
const snapshot = discordSnapshot('/workspace/current');
const rpcCall = async (endpoint, payload) => {
calls.push({ endpoint, payload });
if (endpoint === 'connection.status') return { ok: true, value: snapshot };
if (endpoint === 'bot.reconnect') {
return { ok: true, value: { ...snapshot, testMessage: { sent: true } } };
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(DiscordSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const card = renderer.root.findByProps({ 'data-bot-id': 'discord_test' });
await act(async () => {
buttonNamed(card, '检查连接').props.onClick();
await flushMicrotasks();
});
assert.deepEqual(calls.find((call) => call.endpoint === 'bot.reconnect')?.payload, {
botId: 'discord_test',
sendTest: true,
});
assert.equal(
textOf(renderer.root.findByProps({ role: 'status' })),
'测试消息已发送,请到对应机器人会话中确认。',
);
await act(async () => { renderer.unmount(); });
});
test('shared token target-unavailable feedback asks for any direct message', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
setImTranslator((key) => en[key] ?? key);
t.after(() => {
setImTranslator(null);
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const snapshot = discordSnapshot('/workspace/current');
const rpcCall = async (endpoint) => {
if (endpoint === 'connection.status') return { ok: true, value: snapshot };
if (endpoint === 'bot.reconnect') {
return {
ok: true,
value: {
...snapshot,
testMessage: { sent: false, code: 'test-target-unavailable' },
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(DiscordSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const card = renderer.root.findByProps({ 'data-bot-id': 'discord_test' });
await act(async () => {
buttonNamed(card, 'Check connection').props.onClick();
await flushMicrotasks();
});
const notice = textOf(renderer.root.findByProps({ role: 'status' }));
assert.equal(
notice,
'Connection check completed. The bot has not received a direct message it can use for testing.',
);
assert.doesNotMatch(notice, /\/status|[\p{Script=Han}]/u);
await act(async () => { renderer.unmount(); });
});
test('shared token connection failures render a fixed English-safe notice', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
setImTranslator((key) => en[key] ?? key);
t.after(() => {
setImTranslator(null);
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const snapshot = discordSnapshot('/workspace/current');
const rpcCall = async (endpoint) => {
if (endpoint === 'connection.status') return { ok: true, value: snapshot };
if (endpoint === 'bot.reconnect') {
return {
ok: false,
error: { code: 'discord-operation-failed', message: 'Discord 操作失败,请稍后重试。' },
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(DiscordSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const card = renderer.root.findByProps({ 'data-bot-id': 'discord_test' });
await act(async () => {
buttonNamed(card, 'Check connection').props.onClick();
await flushMicrotasks();
});
const notice = textOf(renderer.root.findByProps({ role: 'status' }));
assert.equal(notice, 'Connection check failed. Try again later.');
assert.doesNotMatch(notice, /[\p{Script=Han}]/u);
await act(async () => { renderer.unmount(); });
});
test('an older reconnect snapshot cannot resurrect a bot deleted by a newer mutation', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const staleReconnect = deferred();
const initialSnapshot = twoBotDiscordSnapshot('/workspace/first');
const deletedSnapshot = { ...initialSnapshot, bots: initialSnapshot.bots.slice(1) };
let statusCalls = 0;
const rpcCall = async (endpoint) => {
if (endpoint === 'connection.status') {
statusCalls += 1;
return { ok: true, value: statusCalls === 1 ? initialSnapshot : deletedSnapshot };
}
if (endpoint === 'bot.reconnect') return staleReconnect.promise;
if (endpoint === 'bot.delete') return { ok: true, value: deletedSnapshot };
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(DiscordSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const firstCard = renderer.root.findByProps({ 'data-bot-id': 'discord_first' });
const secondCard = renderer.root.findByProps({ 'data-bot-id': 'discord_second' });
await act(async () => {
secondCard.findAllByType('button')
.find((button) => button.children.join('') === '检查连接').props.onClick();
await flushMicrotasks();
});
await act(async () => {
firstCard.findAllByType('button')
.find((button) => button.children.join('') === '移除接入').props.onClick();
});
await act(async () => {
await firstCard.findAllByType('button')
.find((button) => button.children.join('') === '确认移除接入').props.onClick();
await flushMicrotasks();
});
assert.equal(renderer.root.findAllByProps({ 'data-bot-id': 'discord_first' }).length, 0);
staleReconnect.resolve({ ok: true, value: initialSnapshot });
await act(async () => { await flushMicrotasks(); });
assert.equal(renderer.root.findAllByProps({ 'data-bot-id': 'discord_first' }).length, 0);
await act(async () => { renderer.unmount(); });
});
const PRESET_CATALOG = {
defaultId: 'default',
items: [
{ id: 'coding', label: 'Coding' },
{ id: 'default', label: 'Default' },
],
};
function optionValues(select) {
return select.children.map((option) => option.props.value);
}
test('AgentPresetEditor lists Host presets and moves its session guidance into accessible help', () => {
const renderer = create(React.createElement(
AgentPresetCatalogContext.Provider,
{ value: PRESET_CATALOG },
React.createElement(AgentPresetEditor, {
agentPreset: 'coding',
onSave() {},
}),
));
const select = renderer.root.findByProps({ className: 'dim-presetSelect' });
assert.equal(select.props.value, 'coding');
assert.deepEqual(optionValues(select), ['', 'coding', 'default']);
assert.equal(textOf(select.children[0]), '跟随 Host 默认');
assert.equal(textOf(select.children[1]), 'Coding(coding)');
const helpButton = renderer.root.findByProps({
'aria-label': '查看 Agent Preset 说明',
});
const tooltip = renderer.root.findByProps({ role: 'tooltip' });
assert.equal(helpButton.props.type, 'button');
assert.ok(tooltip.props.id);
assert.equal(helpButton.props['aria-describedby'], tooltip.props.id);
assert.equal(
textOf(tooltip),
'只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。',
);
assert.equal(renderer.root.findAllByType('small').length, 0);
renderer.unmount();
});
test('AgentPresetEditor marks a removed current preset and still allows clearing it', async () => {
const saved = [];
const renderer = create(React.createElement(
AgentPresetCatalogContext.Provider,
{ value: PRESET_CATALOG },
React.createElement(AgentPresetEditor, {
agentPreset: 'removed-preset',
onSave(value) { saved.push(value); },
}),
));
const select = renderer.root.findByProps({ className: 'dim-presetSelect' });
assert.equal(select.props.value, 'removed-preset');
assert.deepEqual(optionValues(select), ['', 'coding', 'default', 'removed-preset']);
assert.equal(textOf(select.children[3]), 'removed-preset(已不可用)');
assert.equal(
textOf(renderer.root.findByProps({ role: 'status' })),
'当前 Agent Preset 已不可用,请选择其他 Preset 或跟随 Host 默认。',
);
await act(async () => {
select.props.onChange({ target: { value: '' } });
await flushMicrotasks();
});
assert.deepEqual(saved, [null]);
renderer.unmount();
});
test('AgentPresetEditor saves a selected preset and can follow the Host default', async () => {
const saved = [];
function Harness() {
const [agentPreset, setAgentPreset] = React.useState('');
return React.createElement(AgentPresetEditor, {
agentPreset,
onSave(value) {
saved.push(value);
setAgentPreset(value ?? '');
},
});
}
const renderer = create(React.createElement(
AgentPresetCatalogContext.Provider,
{ value: PRESET_CATALOG },
React.createElement(Harness),
));
const select = renderer.root.findByProps({ className: 'dim-presetSelect' });
await act(async () => {
select.props.onChange({ target: { value: 'coding' } });
await flushMicrotasks();
});
await act(async () => {
renderer.root.findByProps({ className: 'dim-presetSelect' })
.props.onChange({ target: { value: '' } });
await flushMicrotasks();
});
assert.deepEqual(saved, ['coding', null]);
renderer.unmount();
});
test('Discord settings save an Agent Preset through bot.preset.set', async (t) => {
const previousWindow = globalThis.window;
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
const snapshot = {
...discordSnapshot('/workspace/current'),
agentPresetCatalog: PRESET_CATALOG,
};
const calls = [];
const rpcCall = async (endpoint, payload) => {
calls.push({ endpoint, payload });
if (endpoint === 'connection.status') return { ok: true, value: snapshot };
if (endpoint === 'bot.preset.set') {
return {
ok: true,
value: {
...snapshot,
bots: [{ ...snapshot.bots[0], agentPreset: payload.agentPreset }],
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
};
let renderer;
await act(async () => {
renderer = create(React.createElement(DiscordSettingsTab, { rpcCall }));
await flushMicrotasks();
});
const card = renderer.root.findByProps({ 'data-bot-id': 'discord_test' });
await act(async () => {
card.findByProps({ className: 'dim-presetSelect' })
.props.onChange({ target: { value: 'coding' } });
await flushMicrotasks();
});
assert.deepEqual(calls.find((call) => call.endpoint === 'bot.preset.set')?.payload, {
botId: 'discord_test',
agentPreset: 'coding',
});
await act(async () => { renderer.unmount(); });
});