Refresh config pages in realtime
This commit is contained in:
133
docs/superpowers/plans/2026-04-07-config-pages-realtime.md
Normal file
133
docs/superpowers/plans/2026-04-07-config-pages-realtime.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Config Pages Realtime Refresh Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 让 Boss Web 的配置页在配置状态发生变化后自动刷新。
|
||||
|
||||
**Architecture:** 为配置类状态引入专用 SSE 事件,状态写入函数统一发布事件,页面通过 `RealtimeRefresh` 订阅对应事件完成无侵入刷新。实现保持在现有 `boss-events -> /api/v1/events -> RealtimeRefresh` 链路内,不新增新的推送通道。
|
||||
|
||||
**Tech Stack:** Next.js App Router, TypeScript, Node test runner, SSE
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 写红灯测试
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/config-pages-realtime-refresh.test.ts`
|
||||
- Create: `tests/config-state-events.test.ts`
|
||||
|
||||
- [ ] **Step 1: 写页面接线失败测试**
|
||||
|
||||
```ts
|
||||
assert.match(source, /<RealtimeRefresh/, "expected page to render RealtimeRefresh");
|
||||
assert.match(source, /events=\{\["ai_accounts\.updated"\]\}/, "expected ai accounts page to refresh on ai account updates");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行页面测试确认失败**
|
||||
|
||||
Run: `npx tsx --test tests/config-pages-realtime-refresh.test.ts`
|
||||
Expected: FAIL,提示目标页面还没有接 `RealtimeRefresh` 或事件名不存在。
|
||||
|
||||
- [ ] **Step 3: 写事件发布失败测试**
|
||||
|
||||
```ts
|
||||
await saveAiAccount({ label: "主 GPT", role: "primary", provider: "openai_api", displayName: "OpenAI", apiKey: "sk-test", setActive: true });
|
||||
assert.equal(events.at(-1)?.event, "ai_accounts.updated");
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行事件测试确认失败**
|
||||
|
||||
Run: `npx tsx --test tests/config-state-events.test.ts`
|
||||
Expected: FAIL,提示没有发布新事件。
|
||||
|
||||
### Task 2: 补状态事件
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/boss-events.ts`
|
||||
- Modify: `src/lib/boss-data.ts`
|
||||
|
||||
- [ ] **Step 1: 扩展事件类型定义**
|
||||
|
||||
```ts
|
||||
| "ai_accounts.updated"
|
||||
| "storage.updated"
|
||||
| "master_agent.settings.updated";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 AI 账号写入路径发布事件**
|
||||
|
||||
```ts
|
||||
publishBossEvent("ai_accounts.updated");
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 在附件存储写入路径发布事件**
|
||||
|
||||
```ts
|
||||
publishBossEvent("storage.updated");
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 在主 Agent 设置写入路径发布事件**
|
||||
|
||||
```ts
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑事件测试确认转绿**
|
||||
|
||||
Run: `npx tsx --test tests/config-state-events.test.ts`
|
||||
Expected: PASS
|
||||
|
||||
### Task 3: 补页面实时刷新
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/me/ai-accounts/page.tsx`
|
||||
- Modify: `src/app/me/storage/page.tsx`
|
||||
- Modify: `src/app/me/master-agent/page.tsx`
|
||||
- Modify: `src/app/me/master-agent/takeover/page.tsx`
|
||||
|
||||
- [ ] **Step 1: 给四个页面引入 `RealtimeRefresh`**
|
||||
|
||||
```ts
|
||||
import { RealtimeRefresh } from "@/components/app-runtime";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 给每个页面接上最小事件集合**
|
||||
|
||||
```tsx
|
||||
<RealtimeRefresh events={["ai_accounts.updated"]} />
|
||||
<RealtimeRefresh events={["storage.updated"]} />
|
||||
<RealtimeRefresh events={["master_agent.settings.updated"]} />
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑页面测试确认转绿**
|
||||
|
||||
Run: `npx tsx --test tests/config-pages-realtime-refresh.test.ts`
|
||||
Expected: PASS
|
||||
|
||||
### Task 4: 全量验证
|
||||
|
||||
**Files:**
|
||||
- Test: `tests/config-pages-realtime-refresh.test.ts`
|
||||
- Test: `tests/config-state-events.test.ts`
|
||||
|
||||
- [ ] **Step 1: 跑本轮相关测试**
|
||||
|
||||
Run: `npx tsx --test tests/config-pages-realtime-refresh.test.ts tests/config-state-events.test.ts tests/realtime-refresh-utils.test.ts tests/project-scoped-realtime-refresh.test.ts tests/status-pages-realtime-refresh.test.ts`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 2: 跑 lint**
|
||||
|
||||
Run: `npm run lint`
|
||||
Expected: exit code 0
|
||||
|
||||
- [ ] **Step 3: 跑 build**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: exit code 0
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-04-07-config-pages-realtime-design.md docs/superpowers/plans/2026-04-07-config-pages-realtime.md tests/config-pages-realtime-refresh.test.ts tests/config-state-events.test.ts src/lib/boss-events.ts src/lib/boss-data.ts src/app/me/ai-accounts/page.tsx src/app/me/storage/page.tsx src/app/me/master-agent/page.tsx src/app/me/master-agent/takeover/page.tsx
|
||||
git commit -m "Refresh config pages in realtime"
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# 配置页实时刷新 Design
|
||||
|
||||
## 目标
|
||||
|
||||
让 Boss Web 的配置类页面在后台状态变更后自动刷新,不再要求用户手动返回或重开页面才能看到最新配置。
|
||||
|
||||
## 本轮范围
|
||||
|
||||
- AI 账号页:`/me/ai-accounts`
|
||||
- 附件与存储页:`/me/storage`
|
||||
- 主 Agent 提示词 / 记忆页:`/me/master-agent`
|
||||
- 全局接管页:`/me/master-agent/takeover`
|
||||
|
||||
本轮不改 Android,不改聊天页,不引入新的数据存储层。
|
||||
|
||||
## 已批准方案
|
||||
|
||||
采用方案 A:为配置类状态新增专用 SSE 事件,再把对应页面接到 `RealtimeRefresh`。
|
||||
|
||||
新增事件:
|
||||
|
||||
- `ai_accounts.updated`
|
||||
- `storage.updated`
|
||||
- `master_agent.settings.updated`
|
||||
|
||||
## 事件分配
|
||||
|
||||
### AI 账号
|
||||
|
||||
以下写入动作统一发布 `ai_accounts.updated`:
|
||||
|
||||
- 新增或更新 AI 账号
|
||||
- 删除 AI 账号
|
||||
- 切换当前主控 AI 账号
|
||||
- 更新 AI 账号健康状态
|
||||
|
||||
### 附件与存储
|
||||
|
||||
以下写入动作发布 `storage.updated`:
|
||||
|
||||
- 保存附件存储配置
|
||||
|
||||
### 主 Agent 设置
|
||||
|
||||
以下写入动作统一发布 `master_agent.settings.updated`:
|
||||
|
||||
- 更新全局主提示词
|
||||
- 更新或清空用户主提示词
|
||||
- 创建、更新、归档用户记忆
|
||||
- 更新 `master-agent` 项目的 `agentControls`,包括全局接管
|
||||
|
||||
## 页面接线
|
||||
|
||||
页面只监听和自身内容直接相关的事件:
|
||||
|
||||
- `/me/ai-accounts` 监听 `ai_accounts.updated`
|
||||
- `/me/storage` 监听 `storage.updated`
|
||||
- `/me/master-agent` 监听 `master_agent.settings.updated`
|
||||
- `/me/master-agent/takeover` 监听 `master_agent.settings.updated`
|
||||
|
||||
## 验证
|
||||
|
||||
- 新增页面接线测试,确认以上四个页面都渲染了 `RealtimeRefresh`
|
||||
- 新增事件发布测试,确认关键状态写入路径会发布正确的事件
|
||||
- 跑相关测试、`npm run lint`、`npm run build`
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RealtimeRefresh } from "@/components/app-runtime";
|
||||
import { AppShell, HeaderTitle, PageNav, StatusBar } from "@/components/app-ui";
|
||||
import { AiAccountsClient } from "@/components/ai-accounts-client";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
@@ -11,6 +12,7 @@ export default async function AiAccountsPage() {
|
||||
|
||||
return (
|
||||
<AppShell bottomNav={false}>
|
||||
<RealtimeRefresh events={["ai_accounts.updated"]} />
|
||||
<StatusBar />
|
||||
<PageNav title="AI 账号" backHref="/me" />
|
||||
<HeaderTitle title="主控身份与接入入口" />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RealtimeRefresh } from "@/components/app-runtime";
|
||||
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
|
||||
import { MasterAgentPromptMemoryClient } from "@/components/master-agent-prompt-memory-client";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
@@ -26,6 +27,7 @@ export default async function MasterAgentPromptMemoryPage() {
|
||||
|
||||
return (
|
||||
<AppShell bottomNav={false}>
|
||||
<RealtimeRefresh events={["master_agent.settings.updated"]} />
|
||||
<StatusBar />
|
||||
<PageNav title="主 Agent 提示词 / 记忆" backHref="/me" />
|
||||
<div className="px-[18px] pb-3">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RealtimeRefresh } from "@/components/app-runtime";
|
||||
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
|
||||
import { MasterAgentTakeoverClient } from "@/components/master-agent-takeover-client";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
@@ -12,6 +13,7 @@ export default async function MasterAgentTakeoverPage() {
|
||||
|
||||
return (
|
||||
<AppShell bottomNav={false}>
|
||||
<RealtimeRefresh events={["master_agent.settings.updated"]} />
|
||||
<StatusBar />
|
||||
<PageNav title="全局接管" backHref="/conversations/master-agent" />
|
||||
<div className="px-[18px] pb-3">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RealtimeRefresh } from "@/components/app-runtime";
|
||||
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
|
||||
import { AttachmentStorageClient } from "@/components/attachment-storage-client";
|
||||
import { requirePageSession } from "@/lib/boss-auth";
|
||||
@@ -20,6 +21,7 @@ export default async function StoragePage() {
|
||||
|
||||
return (
|
||||
<AppShell bottomNav={false}>
|
||||
<RealtimeRefresh events={["storage.updated"]} />
|
||||
<StatusBar />
|
||||
<PageNav title="附件与存储" backHref="/me" />
|
||||
<AttachmentStorageClient key={`${config.mode}:${config.updatedAt}`} config={config} />
|
||||
|
||||
@@ -4322,7 +4322,7 @@ export async function updateProjectAgentControls(
|
||||
throw new Error("MASTER_AGENT_TAKEOVER_SCOPE_RESTRICTED");
|
||||
}
|
||||
|
||||
return mutateStateIfChanged((state) => {
|
||||
const result = await mutateStateIfChanged((state) => {
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||||
|
||||
@@ -4429,6 +4429,10 @@ export async function updateProjectAgentControls(
|
||||
changed: true,
|
||||
};
|
||||
});
|
||||
if (projectId === "master-agent") {
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function projectOrchestrationRequestedBackendId(project: Project): OrchestrationBackendId {
|
||||
@@ -4529,7 +4533,7 @@ export async function getAttachmentStorageConfig(account: string) {
|
||||
}
|
||||
|
||||
export async function upsertAttachmentStorageConfig(config: UserAttachmentStorageConfig) {
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const index = state.userAttachmentStorageConfigs.findIndex(
|
||||
(item) => item.account === config.account,
|
||||
);
|
||||
@@ -4540,6 +4544,8 @@ export async function upsertAttachmentStorageConfig(config: UserAttachmentStorag
|
||||
}
|
||||
return config;
|
||||
});
|
||||
publishBossEvent("storage.updated");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getMasterAgentPromptPolicy() {
|
||||
@@ -4556,7 +4562,7 @@ export async function updateMasterAgentPromptPolicy(input: {
|
||||
throw new Error("MASTER_AGENT_PROMPT_REQUIRED");
|
||||
}
|
||||
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const policy: MasterAgentPromptPolicy = {
|
||||
globalPrompt,
|
||||
updatedBy: input.updatedBy?.trim() || undefined,
|
||||
@@ -4565,6 +4571,8 @@ export async function updateMasterAgentPromptPolicy(input: {
|
||||
state.masterAgentPromptPolicy = policy;
|
||||
return policy;
|
||||
});
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getUserMasterPrompt(account: string) {
|
||||
@@ -4578,7 +4586,7 @@ export async function updateUserMasterPrompt(account: string, content: string) {
|
||||
throw new Error("USER_MASTER_PROMPT_REQUIRED");
|
||||
}
|
||||
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const next: UserMasterPrompt = {
|
||||
account,
|
||||
content: trimmedContent,
|
||||
@@ -4592,14 +4600,18 @@ export async function updateUserMasterPrompt(account: string, content: string) {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function clearUserMasterPrompt(account: string) {
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const before = state.userMasterPrompts.length;
|
||||
state.userMasterPrompts = state.userMasterPrompts.filter((item) => item.account !== account);
|
||||
return { cleared: before !== state.userMasterPrompts.length };
|
||||
});
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listUserMasterMemories(
|
||||
@@ -4647,7 +4659,7 @@ export async function createUserMasterMemory(input: {
|
||||
throw new Error("USER_MASTER_MEMORY_PROJECT_ID_REQUIRED");
|
||||
}
|
||||
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const now = nowIso();
|
||||
const memory: MasterAgentMemory = {
|
||||
memoryId: randomToken("memory"),
|
||||
@@ -4667,6 +4679,8 @@ export async function createUserMasterMemory(input: {
|
||||
state.masterAgentMemories.unshift(memory);
|
||||
return memory;
|
||||
});
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateUserMasterMemory(
|
||||
@@ -4679,7 +4693,7 @@ export async function updateUserMasterMemory(
|
||||
>
|
||||
>,
|
||||
) {
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const memory = state.masterAgentMemories.find(
|
||||
(item) => item.memoryId === memoryId && item.account === account,
|
||||
);
|
||||
@@ -4721,10 +4735,12 @@ export async function updateUserMasterMemory(
|
||||
memory.updatedAt = nowIso();
|
||||
return memory;
|
||||
});
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function archiveUserMasterMemory(memoryId: string, account: string) {
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const memory = state.masterAgentMemories.find(
|
||||
(item) => item.memoryId === memoryId && item.account === account,
|
||||
);
|
||||
@@ -4736,6 +4752,8 @@ export async function archiveUserMasterMemory(memoryId: string, account: string)
|
||||
memory.updatedAt = nowIso();
|
||||
return memory;
|
||||
});
|
||||
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function touchUserMasterMemories(memoryIds: string[], account: string) {
|
||||
@@ -5509,7 +5527,7 @@ export async function saveAiAccount(payload: {
|
||||
setActive?: boolean;
|
||||
loginStatusNote?: string;
|
||||
}) {
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const existing = payload.accountId
|
||||
? state.aiAccounts.find((item) => item.accountId === payload.accountId)
|
||||
: null;
|
||||
@@ -5580,13 +5598,15 @@ export async function saveAiAccount(payload: {
|
||||
|
||||
return buildAiAccountSummary(next);
|
||||
});
|
||||
publishBossEvent("ai_accounts.updated");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteAiAccount(accountId: string) {
|
||||
if (accountId === ENV_OPENAI_ACCOUNT_ID) {
|
||||
throw new Error("ENV_AI_ACCOUNT_READ_ONLY");
|
||||
}
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const target = state.aiAccounts.find((item) => item.accountId === accountId);
|
||||
if (!target) {
|
||||
throw new Error("AI_ACCOUNT_NOT_FOUND");
|
||||
@@ -5600,12 +5620,14 @@ export async function deleteAiAccount(accountId: string) {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
publishBossEvent("ai_accounts.updated");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function activateAiAccount(accountId: string, reason: string) {
|
||||
if (accountId === ENV_OPENAI_ACCOUNT_ID) {
|
||||
const state = await readState();
|
||||
return {
|
||||
const result = {
|
||||
activeIdentity: {
|
||||
...getMasterIdentitySummaryFromState(state),
|
||||
accountId: ENV_OPENAI_ACCOUNT_ID,
|
||||
@@ -5617,13 +5639,17 @@ export async function activateAiAccount(accountId: string, reason: string) {
|
||||
isEnvironmentFallback: true,
|
||||
},
|
||||
};
|
||||
publishBossEvent("ai_accounts.updated");
|
||||
return result;
|
||||
}
|
||||
return mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
setActiveAiAccountInState(state, accountId, reason);
|
||||
return {
|
||||
activeIdentity: getMasterIdentitySummaryFromState(state),
|
||||
};
|
||||
});
|
||||
publishBossEvent("ai_accounts.updated");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateAiAccountHealth(params: {
|
||||
@@ -5661,6 +5687,7 @@ export async function updateAiAccountHealth(params: {
|
||||
);
|
||||
}
|
||||
});
|
||||
publishBossEvent("ai_accounts.updated");
|
||||
}
|
||||
|
||||
export async function getMasterAgentRuntimeAccount() {
|
||||
|
||||
@@ -6,8 +6,11 @@ export type BossEventName =
|
||||
| "project.context_risk.updated"
|
||||
| "app.logs.updated"
|
||||
| "master_agent.task.updated"
|
||||
| "master_agent.settings.updated"
|
||||
| "ai_accounts.updated"
|
||||
| "devices.updated"
|
||||
| "devices.skills.updated"
|
||||
| "storage.updated"
|
||||
| "ota.updated";
|
||||
|
||||
export interface BossEventPayload {
|
||||
|
||||
47
tests/config-pages-realtime-refresh.test.ts
Normal file
47
tests/config-pages-realtime-refresh.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
async function readSource(relativePath: string) {
|
||||
return readFile(new URL(`../${relativePath}`, import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
test("ai accounts page refreshes when AI account state changes", async () => {
|
||||
const source = await readSource("src/app/me/ai-accounts/page.tsx");
|
||||
|
||||
assert.match(source, /import \{ RealtimeRefresh \}/, "expected ai accounts page to import RealtimeRefresh");
|
||||
assert.match(source, /<RealtimeRefresh/, "expected ai accounts page to render RealtimeRefresh");
|
||||
assert.match(
|
||||
source,
|
||||
/events=\{\["ai_accounts\.updated"\]\}/,
|
||||
"expected ai accounts page to refresh on ai_accounts.updated",
|
||||
);
|
||||
});
|
||||
|
||||
test("storage page refreshes when storage config changes", async () => {
|
||||
const source = await readSource("src/app/me/storage/page.tsx");
|
||||
|
||||
assert.match(source, /import \{ RealtimeRefresh \}/, "expected storage page to import RealtimeRefresh");
|
||||
assert.match(source, /<RealtimeRefresh/, "expected storage page to render RealtimeRefresh");
|
||||
assert.match(
|
||||
source,
|
||||
/events=\{\["storage\.updated"\]\}/,
|
||||
"expected storage page to refresh on storage.updated",
|
||||
);
|
||||
});
|
||||
|
||||
test("master agent settings pages refresh when master agent config changes", async () => {
|
||||
for (const relativePath of [
|
||||
"src/app/me/master-agent/page.tsx",
|
||||
"src/app/me/master-agent/takeover/page.tsx",
|
||||
]) {
|
||||
const source = await readSource(relativePath);
|
||||
assert.match(source, /import \{ RealtimeRefresh \}/, `expected ${relativePath} to import RealtimeRefresh`);
|
||||
assert.match(source, /<RealtimeRefresh/, `expected ${relativePath} to render RealtimeRefresh`);
|
||||
assert.match(
|
||||
source,
|
||||
/events=\{\["master_agent\.settings\.updated"\]\}/,
|
||||
`expected ${relativePath} to refresh on master_agent.settings.updated`,
|
||||
);
|
||||
}
|
||||
});
|
||||
152
tests/config-state-events.test.ts
Normal file
152
tests/config-state-events.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
|
||||
let runtimeRoot = "";
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
|
||||
let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
|
||||
let upsertAttachmentStorageConfig: (typeof import("../src/lib/boss-data"))["upsertAttachmentStorageConfig"];
|
||||
let updateMasterAgentPromptPolicy: (typeof import("../src/lib/boss-data"))["updateMasterAgentPromptPolicy"];
|
||||
let createUserMasterMemory: (typeof import("../src/lib/boss-data"))["createUserMasterMemory"];
|
||||
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
||||
let subscribeBossEvents: (typeof import("../src/lib/boss-events"))["subscribeBossEvents"];
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
|
||||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-config-events-"));
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [data, events] = await Promise.all([
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/lib/boss-events.ts"),
|
||||
]);
|
||||
|
||||
readState = data.readState;
|
||||
writeState = data.writeState;
|
||||
saveAiAccount = data.saveAiAccount;
|
||||
upsertAttachmentStorageConfig = data.upsertAttachmentStorageConfig;
|
||||
updateMasterAgentPromptPolicy = data.updateMasterAgentPromptPolicy;
|
||||
createUserMasterMemory = data.createUserMasterMemory;
|
||||
updateProjectAgentControls = data.updateProjectAgentControls;
|
||||
subscribeBossEvents = events.subscribeBossEvents;
|
||||
}
|
||||
|
||||
async function resetConfigState() {
|
||||
const state = await readState();
|
||||
state.aiAccounts = [];
|
||||
state.aiAccountSwitchHistory = [];
|
||||
state.userAttachmentStorageConfigs = [];
|
||||
state.masterAgentPromptPolicy = undefined;
|
||||
state.userMasterPrompts = [];
|
||||
state.masterAgentMemories = [];
|
||||
const masterAgentProject = state.projects.find((project) => project.id === "master-agent");
|
||||
if (masterAgentProject) {
|
||||
masterAgentProject.agentControls = undefined;
|
||||
}
|
||||
await writeState(state);
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await setup();
|
||||
await resetConfigState();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
if (runtimeRoot) {
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("saveAiAccount publishes ai account refresh event", async () => {
|
||||
const events: Array<{ event: string }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event) => {
|
||||
events.push({ event });
|
||||
});
|
||||
|
||||
await saveAiAccount({
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI 主账号",
|
||||
apiKey: "sk-test-primary",
|
||||
setActive: true,
|
||||
});
|
||||
unsubscribe();
|
||||
|
||||
assert.equal(events.at(-1)?.event, "ai_accounts.updated");
|
||||
});
|
||||
|
||||
test("upsertAttachmentStorageConfig publishes storage refresh event", async () => {
|
||||
const events: Array<{ event: string }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event) => {
|
||||
events.push({ event });
|
||||
});
|
||||
|
||||
await upsertAttachmentStorageConfig({
|
||||
account: "17600003315",
|
||||
mode: "server_file",
|
||||
updatedAt: "2026-04-07T10:20:00.000Z",
|
||||
});
|
||||
unsubscribe();
|
||||
|
||||
assert.equal(events.at(-1)?.event, "storage.updated");
|
||||
});
|
||||
|
||||
test("master agent prompt policy publishes master agent settings refresh event", async () => {
|
||||
const events: Array<{ event: string; payload: { projectId?: string } }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
events.push({ event, payload });
|
||||
});
|
||||
|
||||
await updateMasterAgentPromptPolicy({
|
||||
globalPrompt: "保持简洁并只输出有效内容。",
|
||||
updatedBy: "17600003315",
|
||||
});
|
||||
unsubscribe();
|
||||
|
||||
assert.equal(events.at(-1)?.event, "master_agent.settings.updated");
|
||||
assert.equal(events.at(-1)?.payload.projectId, "master-agent");
|
||||
});
|
||||
|
||||
test("master agent memory writes publish master agent settings refresh event", async () => {
|
||||
const events: Array<{ event: string; payload: { projectId?: string } }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
events.push({ event, payload });
|
||||
});
|
||||
|
||||
await createUserMasterMemory({
|
||||
account: "17600003315",
|
||||
scope: "global",
|
||||
title: "用户偏好",
|
||||
content: "群聊里默认一键通过。",
|
||||
memoryType: "user_preference",
|
||||
});
|
||||
unsubscribe();
|
||||
|
||||
assert.equal(events.at(-1)?.event, "master_agent.settings.updated");
|
||||
assert.equal(events.at(-1)?.payload.projectId, "master-agent");
|
||||
});
|
||||
|
||||
test("master agent takeover changes publish master agent settings refresh event", async () => {
|
||||
const events: Array<{ event: string; payload: { projectId?: string } }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
events.push({ event, payload });
|
||||
});
|
||||
|
||||
await updateProjectAgentControls(
|
||||
"master-agent",
|
||||
{
|
||||
globalTakeoverEnabled: true,
|
||||
},
|
||||
"17600003315",
|
||||
);
|
||||
unsubscribe();
|
||||
|
||||
assert.equal(events.at(-1)?.event, "master_agent.settings.updated");
|
||||
assert.equal(events.at(-1)?.payload.projectId, "master-agent");
|
||||
});
|
||||
Reference in New Issue
Block a user