@steerable/agent-shell-web 0.6.19 → 0.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -8
- package/vite.base.d.ts +7 -0
- package/vite.base.js +83 -0
- package/src/brand.test.ts +0 -22
- package/src/components/AgentSidebar.test.tsx +0 -791
- package/src/components/ChatHeader.test.tsx +0 -230
- package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
- package/src/components/chat/ApprovalModal.test.tsx +0 -83
- package/src/components/chat/AskUserModalHost.test.tsx +0 -176
- package/src/components/chat/AskUserQuestionMenu.test.tsx +0 -190
- package/src/components/chat/AssistantMessage.test.tsx +0 -68
- package/src/components/chat/ChatInput.test.tsx +0 -609
- package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
- package/src/components/chat/Markdown.test.tsx +0 -170
- package/src/components/chat/ModelPicker.test.tsx +0 -149
- package/src/components/chat/SessionTreeModal.test.tsx +0 -161
- package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
- package/src/components/chat/TaskPanelModal.test.tsx +0 -198
- package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
- package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
- package/src/components/chat/elapsed.test.ts +0 -42
- package/src/components/chat/executed-actions-model.test.ts +0 -134
- package/src/components/chat/orchestration-children-model.test.ts +0 -115
- package/src/components/chat/turn-timeline.test.ts +0 -97
- package/src/components/chat/useChatTasks.test.ts +0 -136
- package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
- package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
- package/src/components/settings/LlmSettingsPanel.test.tsx +0 -461
- package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
- package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
- package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
- package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
- package/src/components/settings/compat-flags-model.test.ts +0 -71
- package/src/components/settings/llm-vendors.test.ts +0 -171
- package/src/components/settings/model-capabilities.test.ts +0 -88
- package/src/components/settings/preset-choice-model.test.ts +0 -96
- package/src/hooks/useChatsAndAgents.test.ts +0 -165
- package/src/lib/attachments.test.ts +0 -127
- package/src/lib/chat-title.test.ts +0 -53
- package/src/lib/chat-transport.test.ts +0 -282
- package/src/lib/date-groups.test.ts +0 -62
- package/src/lib/demo-flag.test.ts +0 -17
- package/src/lib/electron-bridge.test.ts +0 -54
- package/src/lib/exec-policy.test.ts +0 -71
- package/src/lib/http-bridge.test.ts +0 -283
- package/src/lib/insights.test.ts +0 -56
- package/src/lib/local-api.test.ts +0 -353
- package/src/lib/pending-first-message.test.ts +0 -17
- package/src/lib/slash-sources.test.ts +0 -161
- package/src/pages/AgentPage.test.tsx +0 -627
- package/src/pages/SettingsPage.test.tsx +0 -102
- package/vite.base.ts +0 -94
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* chat-transport:Electron IPC SSE → 框架 ChatStreamTransport 的适配。
|
|
3
|
-
* 分两层锁定:
|
|
4
|
-
* - LocalBackendSseAdapter(经 __test__ 导出):legacy 帧归一化、
|
|
5
|
-
* budget_exhausted 降级护栏(已有内容流过时降级为普通结束,避免
|
|
6
|
-
* 框架 hook 把已渲染文本冲掉)、done 恰好一次、turn_timeline 旁路。
|
|
7
|
-
* - createElectronChatTransport:请求形状(路径编码、metadata 并入
|
|
8
|
-
* body)、错误路径(startStream 拒绝 / error 载荷都会 reject 并
|
|
9
|
-
* 发 error 事件)、cancelActive 双路径(有 streamId 走 cancelStream,
|
|
10
|
-
* 否则按 chatId 打取消端点)、steer 的软失败。
|
|
11
|
-
* 桥走真实的 window.electron 路径,不 mock 模块。
|
|
12
|
-
*/
|
|
13
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
-
import type { SSEEvent } from '@steerable/agent-protocol';
|
|
15
|
-
import {
|
|
16
|
-
createElectronChatTransport,
|
|
17
|
-
regenerateChatMessage,
|
|
18
|
-
__test__,
|
|
19
|
-
} from './chat-transport';
|
|
20
|
-
|
|
21
|
-
const { LocalBackendSseAdapter } = __test__;
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
delete (window as { electron?: unknown }).electron;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
function collectEvents(): { events: SSEEvent[]; onEvent: (e: SSEEvent) => void } {
|
|
28
|
-
const events: SSEEvent[] = [];
|
|
29
|
-
return { events, onEvent: (e) => events.push(e) };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function ofType<T extends SSEEvent['type']>(events: SSEEvent[], type: T) {
|
|
33
|
-
return events.filter((e) => e.type === type);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('LocalBackendSseAdapter 帧归一化', () => {
|
|
37
|
-
it('content 帧按序透出,并附带 turn_timeline 旁路事件', () => {
|
|
38
|
-
const { events, onEvent } = collectEvents();
|
|
39
|
-
const adapter = new LocalBackendSseAdapter(onEvent);
|
|
40
|
-
adapter.feed('data: {"content":"你好"}\n\n');
|
|
41
|
-
adapter.feed('data: {"content":"世界"}\n\n');
|
|
42
|
-
const contents = ofType(events, 'content').map((e) => (e as { content: string }).content);
|
|
43
|
-
expect(contents).toEqual(['你好', '世界']);
|
|
44
|
-
const timelines = events.filter(
|
|
45
|
-
(e) => e.type === 'agent' && (e as { event?: string }).event === 'turn_timeline',
|
|
46
|
-
);
|
|
47
|
-
expect(timelines.length).toBe(2);
|
|
48
|
-
const last = timelines[1] as unknown as {
|
|
49
|
-
payload: { blocks: Array<{ type: string; content: string }> };
|
|
50
|
-
};
|
|
51
|
-
expect(last.payload.blocks).toEqual([{ type: 'text', content: '你好世界' }]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('[DONE] 帧触发一次 done;之后再 end 不重复', () => {
|
|
55
|
-
const { events, onEvent } = collectEvents();
|
|
56
|
-
const adapter = new LocalBackendSseAdapter(onEvent);
|
|
57
|
-
adapter.feed('data: {"content":"x"}\n\ndata: [DONE]\n\n');
|
|
58
|
-
adapter.end();
|
|
59
|
-
expect(ofType(events, 'done')).toHaveLength(1);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('没有 [DONE] 时 end() 补发一次 done', () => {
|
|
63
|
-
const { events, onEvent } = collectEvents();
|
|
64
|
-
const adapter = new LocalBackendSseAdapter(onEvent);
|
|
65
|
-
adapter.feed('data: {"content":"x"}\n\n');
|
|
66
|
-
adapter.end();
|
|
67
|
-
expect(ofType(events, 'done')).toHaveLength(1);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// fixture 与服务端 router.ts onNotice 的真实发射形状一致(budget.kind + message)。
|
|
71
|
-
it('budget_exhausted 在没有任何内容流过时原样透出、不结束流', () => {
|
|
72
|
-
const { events, onEvent } = collectEvents();
|
|
73
|
-
const adapter = new LocalBackendSseAdapter(onEvent);
|
|
74
|
-
adapter.feed(
|
|
75
|
-
'data: {"type":"budget_exhausted","budget":{"kind":"tokens"},"message":"budget_exhausted: tokens"}\n\n',
|
|
76
|
-
);
|
|
77
|
-
expect(ofType(events, 'budget_exhausted')).toHaveLength(1);
|
|
78
|
-
expect(ofType(events, 'done')).toHaveLength(0);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('budget_exhausted 在已有内容流过时降级为普通结束(护栏)', () => {
|
|
82
|
-
const { events, onEvent } = collectEvents();
|
|
83
|
-
const adapter = new LocalBackendSseAdapter(onEvent);
|
|
84
|
-
adapter.feed('data: {"content":"半截回答"}\n\n');
|
|
85
|
-
adapter.feed(
|
|
86
|
-
'data: {"type":"budget_exhausted","budget":{"kind":"tokens"},"message":"budget_exhausted: tokens"}\n\n',
|
|
87
|
-
);
|
|
88
|
-
// 原事件被吞,替换为 suppression 标记 + done。
|
|
89
|
-
expect(ofType(events, 'budget_exhausted')).toHaveLength(0);
|
|
90
|
-
const suppressed = events.filter(
|
|
91
|
-
(e) =>
|
|
92
|
-
e.type === 'agent' &&
|
|
93
|
-
(e as { event?: string }).event === 'budget_exhausted_suppressed',
|
|
94
|
-
);
|
|
95
|
-
expect(suppressed).toHaveLength(1);
|
|
96
|
-
expect(ofType(events, 'done')).toHaveLength(1);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('executed_actions 帧透出并同步进时间线', () => {
|
|
100
|
-
const { events, onEvent } = collectEvents();
|
|
101
|
-
const adapter = new LocalBackendSseAdapter(onEvent);
|
|
102
|
-
adapter.feed(
|
|
103
|
-
'data: {"type":"executed_actions","actions":[{"id":"t1","toolName":"bash","status":"done"}]}\n\n',
|
|
104
|
-
);
|
|
105
|
-
const executed = events.filter(
|
|
106
|
-
(e) => e.type === 'agent' && (e as { event?: string }).event === 'executed_actions',
|
|
107
|
-
);
|
|
108
|
-
expect(executed).toHaveLength(1);
|
|
109
|
-
const timelines = events.filter(
|
|
110
|
-
(e) => e.type === 'agent' && (e as { event?: string }).event === 'turn_timeline',
|
|
111
|
-
);
|
|
112
|
-
expect(timelines).toHaveLength(1);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('createElectronChatTransport', () => {
|
|
117
|
-
function installStreamBridge() {
|
|
118
|
-
const captured: {
|
|
119
|
-
input?: { method: string; path: string; body?: unknown };
|
|
120
|
-
cb?: (payload: { type: string; chunk?: string; status?: number; error?: string }) => void;
|
|
121
|
-
} = {};
|
|
122
|
-
const cancelStream = vi.fn();
|
|
123
|
-
const request = vi.fn().mockResolvedValue({});
|
|
124
|
-
const startStream = vi.fn(
|
|
125
|
-
(
|
|
126
|
-
input: { method: string; path: string; body?: unknown },
|
|
127
|
-
cb: (payload: { type: string; chunk?: string; status?: number; error?: string }) => void,
|
|
128
|
-
) => {
|
|
129
|
-
captured.input = input;
|
|
130
|
-
captured.cb = cb;
|
|
131
|
-
return Promise.resolve('stream-1');
|
|
132
|
-
},
|
|
133
|
-
);
|
|
134
|
-
(window as { electron?: unknown }).electron = {
|
|
135
|
-
localBackend: { request, startStream, cancelStream },
|
|
136
|
-
};
|
|
137
|
-
return { captured, cancelStream, request, startStream };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
it('桥缺失时 stream 直接拒绝', async () => {
|
|
141
|
-
const transport = createElectronChatTransport('chat-1');
|
|
142
|
-
await expect(
|
|
143
|
-
transport.stream({ content: 'hi' }, () => {}),
|
|
144
|
-
).rejects.toThrow(/Electron bridge unavailable/);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('请求形状:路径编码 chatId,metadata 并入 body', async () => {
|
|
148
|
-
const { captured } = installStreamBridge();
|
|
149
|
-
const transport = createElectronChatTransport('chat 1');
|
|
150
|
-
const { onEvent } = collectEvents();
|
|
151
|
-
const p = transport.stream({ content: 'hi', metadata: { foo: 1 } }, onEvent);
|
|
152
|
-
await vi.waitFor(() => expect(captured.cb).toBeDefined());
|
|
153
|
-
expect(captured.input).toEqual({
|
|
154
|
-
method: 'POST',
|
|
155
|
-
path: '/api/v2/chats/chat%201/run',
|
|
156
|
-
body: { message: 'hi', foo: 1 },
|
|
157
|
-
});
|
|
158
|
-
captured.cb!({ type: 'end', status: 200 });
|
|
159
|
-
await p;
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('data → end 载荷驱动适配器,end 后 stream resolve 且返回取消句柄', async () => {
|
|
163
|
-
const { captured, cancelStream } = installStreamBridge();
|
|
164
|
-
const transport = createElectronChatTransport('chat-1');
|
|
165
|
-
const { events, onEvent } = collectEvents();
|
|
166
|
-
const p = transport.stream({ content: 'hi' }, onEvent);
|
|
167
|
-
await vi.waitFor(() => expect(captured.cb).toBeDefined());
|
|
168
|
-
captured.cb!({ type: 'data', chunk: 'data: {"content":"x"}\n\n' });
|
|
169
|
-
captured.cb!({ type: 'end', status: 200 });
|
|
170
|
-
const cancel = await p;
|
|
171
|
-
expect(ofType(events, 'content')).toHaveLength(1);
|
|
172
|
-
expect(ofType(events, 'done')).toHaveLength(1);
|
|
173
|
-
// 流已结束,契约句柄此时取消仍指向记忆中的 streamId(空操作语义见文件头)。
|
|
174
|
-
if (typeof cancel !== 'function') throw new Error('expected a cancel handle');
|
|
175
|
-
cancel();
|
|
176
|
-
expect(cancelStream).toHaveBeenCalledWith('stream-1');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('startStream 拒绝时发 error 事件并 reject', async () => {
|
|
180
|
-
const { startStream } = installStreamBridge();
|
|
181
|
-
startStream.mockRejectedValueOnce(new Error('ipc dead'));
|
|
182
|
-
const transport = createElectronChatTransport('chat-1');
|
|
183
|
-
const { events, onEvent } = collectEvents();
|
|
184
|
-
await expect(transport.stream({ content: 'hi' }, onEvent)).rejects.toThrow('ipc dead');
|
|
185
|
-
expect(ofType(events, 'error')).toHaveLength(1);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('error 载荷发 error 事件并 reject', async () => {
|
|
189
|
-
const { captured } = installStreamBridge();
|
|
190
|
-
const transport = createElectronChatTransport('chat-1');
|
|
191
|
-
const { events, onEvent } = collectEvents();
|
|
192
|
-
const p = transport.stream({ content: 'hi' }, onEvent);
|
|
193
|
-
await vi.waitFor(() => expect(captured.cb).toBeDefined());
|
|
194
|
-
captured.cb!({ type: 'error', error: 'agent crashed' });
|
|
195
|
-
await expect(p).rejects.toThrow('agent crashed');
|
|
196
|
-
expect(ofType(events, 'error')).toEqual([{ type: 'error', message: 'agent crashed' }]);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('cancelActive 有进行中流时走 cancelStream,否则按 chatId 打取消端点', async () => {
|
|
200
|
-
const { captured, cancelStream, request } = installStreamBridge();
|
|
201
|
-
const transport = createElectronChatTransport('chat-1');
|
|
202
|
-
const { onEvent } = collectEvents();
|
|
203
|
-
const p = transport.stream({ content: 'hi' }, onEvent);
|
|
204
|
-
await vi.waitFor(() => expect(captured.cb).toBeDefined());
|
|
205
|
-
// startStream 已 resolve,streamId 已登记——取消走 IPC。
|
|
206
|
-
transport.cancelActive();
|
|
207
|
-
expect(cancelStream).toHaveBeenCalledWith('stream-1');
|
|
208
|
-
captured.cb!({ type: 'end', status: 200 });
|
|
209
|
-
await p;
|
|
210
|
-
// 流已结束后再取消:改打 HTTP 取消端点(fire-and-forget)。
|
|
211
|
-
transport.cancelActive();
|
|
212
|
-
expect(request).toHaveBeenCalledWith({
|
|
213
|
-
method: 'POST',
|
|
214
|
-
path: '/api/v2/chats/chat-1/cancel',
|
|
215
|
-
body: {},
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('steer 在桥无 steerChat 能力时软失败为 false', async () => {
|
|
220
|
-
installStreamBridge();
|
|
221
|
-
const transport = createElectronChatTransport('chat-1');
|
|
222
|
-
await expect(transport.steer!('换个方向')).resolves.toBe(false);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('steer 透传桥的返回值', async () => {
|
|
226
|
-
const steerChat = vi.fn().mockResolvedValue(true);
|
|
227
|
-
(window as { electron?: unknown }).electron = { localBackend: { steerChat } };
|
|
228
|
-
const transport = createElectronChatTransport('chat-1');
|
|
229
|
-
await expect(transport.steer!('换个方向')).resolves.toBe(true);
|
|
230
|
-
expect(steerChat).toHaveBeenCalledWith('chat-1', '换个方向');
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('regenerateChatMessage', () => {
|
|
235
|
-
function installRegenerateBridge() {
|
|
236
|
-
const captured: {
|
|
237
|
-
cb?: (payload: { type: string; chunk?: string; status?: number; error?: string }) => void;
|
|
238
|
-
} = {};
|
|
239
|
-
const startStream = vi.fn(
|
|
240
|
-
(
|
|
241
|
-
_input: unknown,
|
|
242
|
-
cb: (payload: { type: string; chunk?: string; status?: number; error?: string }) => void,
|
|
243
|
-
) => {
|
|
244
|
-
captured.cb = cb;
|
|
245
|
-
return Promise.resolve('stream-r');
|
|
246
|
-
},
|
|
247
|
-
);
|
|
248
|
-
(window as { electron?: unknown }).electron = { localBackend: { startStream } };
|
|
249
|
-
return captured;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
it('2xx 结束 → resolve', async () => {
|
|
253
|
-
const captured = installRegenerateBridge();
|
|
254
|
-
const p = regenerateChatMessage('chat-1', 'msg-1');
|
|
255
|
-
await vi.waitFor(() => expect(captured.cb).toBeDefined());
|
|
256
|
-
captured.cb!({ type: 'end', status: 200 });
|
|
257
|
-
await expect(p).resolves.toBeUndefined();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('error 帧 + 非 2xx 结束 → 以后端的拒绝文案 reject', async () => {
|
|
261
|
-
const captured = installRegenerateBridge();
|
|
262
|
-
const p = regenerateChatMessage('chat-1', 'msg-1');
|
|
263
|
-
await vi.waitFor(() => expect(captured.cb).toBeDefined());
|
|
264
|
-
captured.cb!({ type: 'data', chunk: 'event: error\ndata: {"message":"该回复不可重新生成"}\n\n' });
|
|
265
|
-
captured.cb!({ type: 'end', status: 400 });
|
|
266
|
-
await expect(p).rejects.toThrow('该回复不可重新生成');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('非 2xx 且无 error 帧 → 带状态码的兜底文案', async () => {
|
|
270
|
-
const captured = installRegenerateBridge();
|
|
271
|
-
const p = regenerateChatMessage('chat-1', 'msg-1');
|
|
272
|
-
await vi.waitFor(() => expect(captured.cb).toBeDefined());
|
|
273
|
-
captured.cb!({ type: 'end', status: 500 });
|
|
274
|
-
await expect(p).rejects.toThrow('重新生成失败(HTTP 500)');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('桥缺失时拒绝', async () => {
|
|
278
|
-
await expect(regenerateChatMessage('chat-1', 'msg-1')).rejects.toThrow(
|
|
279
|
-
/Electron bridge unavailable/,
|
|
280
|
-
);
|
|
281
|
-
});
|
|
282
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* date-groups:侧栏会话分组的标签与排序权重。
|
|
3
|
-
* 锁定分档边界(今天 / 昨天 / 2–3 天前 / 同年 M月D日 / 跨年 YYYY年M月D日)
|
|
4
|
-
* 与 priority 的单调性(越小越新)。测试日期一律相对今天构造(正午锚定),
|
|
5
|
-
* 避免硬编码日期随时间腐化或在午夜前后抖动。
|
|
6
|
-
*/
|
|
7
|
-
import { describe, expect, it } from 'vitest';
|
|
8
|
-
import { getDateGroupLabel, getDateGroupPriority } from './date-groups';
|
|
9
|
-
|
|
10
|
-
/** 构造 n 天前的日期(锚在正午,避开跨午夜的边界抖动)。 */
|
|
11
|
-
function daysAgo(n: number): Date {
|
|
12
|
-
const d = new Date();
|
|
13
|
-
d.setHours(12, 0, 0, 0);
|
|
14
|
-
d.setDate(d.getDate() - n);
|
|
15
|
-
return d;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('getDateGroupLabel 分档', () => {
|
|
19
|
-
it('今天与昨天有专用标签', () => {
|
|
20
|
-
expect(getDateGroupLabel(daysAgo(0))).toBe('今天');
|
|
21
|
-
expect(getDateGroupLabel(daysAgo(1))).toBe('昨天');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('2–3 天前折叠为「N天前」', () => {
|
|
25
|
-
expect(getDateGroupLabel(daysAgo(2))).toBe('2天前');
|
|
26
|
-
expect(getDateGroupLabel(daysAgo(3))).toBe('3天前');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('4 天起按年份分档:同年 M月D日,跨年 YYYY年M月D日', () => {
|
|
30
|
-
const d = daysAgo(10);
|
|
31
|
-
const today = new Date();
|
|
32
|
-
if (d.getFullYear() === today.getFullYear()) {
|
|
33
|
-
expect(getDateGroupLabel(d)).toBe(`${d.getMonth() + 1}月${d.getDate()}日`);
|
|
34
|
-
} else {
|
|
35
|
-
expect(getDateGroupLabel(d)).toBe(
|
|
36
|
-
`${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('确定的过去年份一定走跨年格式', () => {
|
|
42
|
-
expect(getDateGroupLabel(new Date(2000, 0, 5))).toBe('2000年1月5日');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('getDateGroupPriority 排序权重', () => {
|
|
47
|
-
it('各档位的权重值', () => {
|
|
48
|
-
expect(getDateGroupPriority('今天')).toBe(1);
|
|
49
|
-
expect(getDateGroupPriority('昨天')).toBe(2);
|
|
50
|
-
expect(getDateGroupPriority('3天前')).toBe(5);
|
|
51
|
-
expect(getDateGroupPriority('9月1日')).toBe(100);
|
|
52
|
-
expect(getDateGroupPriority('2025年9月1日')).toBe(1000);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('按权重排序后组序为 今天 → 昨天 → N天前 → 月日 → 跨年', () => {
|
|
56
|
-
const labels = ['2024年3月2日', '9月1日', '3天前', '昨天', '2天前', '今天'];
|
|
57
|
-
const sorted = [...labels].sort(
|
|
58
|
-
(a, b) => getDateGroupPriority(a) - getDateGroupPriority(b),
|
|
59
|
-
);
|
|
60
|
-
expect(sorted).toEqual(['今天', '昨天', '2天前', '3天前', '9月1日', '2024年3月2日']);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* demo-flag:静态演示构建(steerableframework.com/demo/)的模块级开关。
|
|
3
|
-
* 正常应用构建从不调用 markDemoMode(),所以缺省必须为 false;
|
|
4
|
-
* 标记一旦置位不可逆(没有 unmark——页面生命周期内不需要)。
|
|
5
|
-
*/
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
7
|
-
import { isDemoMode, markDemoMode } from './demo-flag';
|
|
8
|
-
|
|
9
|
-
describe('demo-flag', () => {
|
|
10
|
-
it('缺省 false,markDemoMode 后置 true 且保持', () => {
|
|
11
|
-
expect(isDemoMode()).toBe(false);
|
|
12
|
-
markDemoMode();
|
|
13
|
-
expect(isDemoMode()).toBe(true);
|
|
14
|
-
markDemoMode();
|
|
15
|
-
expect(isDemoMode()).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* electron-bridge 的桥选择逻辑:window.electron(Electron preload)优先,
|
|
3
|
-
* 否则 window.__DEEPPATH_BS__(BS 服务器托管)落到 HTTP 实现,
|
|
4
|
-
* 两者皆无则是纯浏览器演示态(null)。SPA 必须在三种形态下都不崩。
|
|
5
|
-
*/
|
|
6
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
7
|
-
import { getElectronBridge, isElectron } from './electron-bridge';
|
|
8
|
-
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
delete (window as { electron?: unknown }).electron;
|
|
11
|
-
delete (window as { __DEEPPATH_BS__?: unknown }).__DEEPPATH_BS__;
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe('getElectronBridge / isElectron', () => {
|
|
15
|
-
it('两种桥都不存在时返回 null(纯浏览器演示态)', () => {
|
|
16
|
-
expect(getElectronBridge()).toBeNull();
|
|
17
|
-
expect(isElectron()).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('window.electron 存在时原样返回', () => {
|
|
21
|
-
const fake = { runtime: 'local', platform: 'darwin' };
|
|
22
|
-
(window as { electron?: unknown }).electron = fake;
|
|
23
|
-
expect(getElectronBridge()).toBe(fake);
|
|
24
|
-
expect(isElectron()).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('仅 __DEEPPATH_BS__ 时落到 HTTP 桥,平台取自注入的引导信息', () => {
|
|
28
|
-
(window as { __DEEPPATH_BS__?: unknown }).__DEEPPATH_BS__ = {
|
|
29
|
-
platform: 'linux',
|
|
30
|
-
flavor: 'generic',
|
|
31
|
-
brandName: 'Test',
|
|
32
|
-
};
|
|
33
|
-
const bridge = getElectronBridge();
|
|
34
|
-
expect(bridge).not.toBeNull();
|
|
35
|
-
expect(bridge!.runtime).toBe('local');
|
|
36
|
-
expect(bridge!.platform).toBe('linux');
|
|
37
|
-
// HTTP 桥的传输面齐全(fetch 实现,无需 Electron)。
|
|
38
|
-
expect(typeof bridge!.localBackend.request).toBe('function');
|
|
39
|
-
expect(isElectron()).toBe(true);
|
|
40
|
-
// 单例:再次取桥是同一个对象。
|
|
41
|
-
expect(getElectronBridge()).toBe(bridge);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('window.electron 优先于 __DEEPPATH_BS__', () => {
|
|
45
|
-
const fake = { runtime: 'local', platform: 'darwin' };
|
|
46
|
-
(window as { electron?: unknown }).electron = fake;
|
|
47
|
-
(window as { __DEEPPATH_BS__?: unknown }).__DEEPPATH_BS__ = {
|
|
48
|
-
platform: 'linux',
|
|
49
|
-
flavor: 'generic',
|
|
50
|
-
brandName: 'Test',
|
|
51
|
-
};
|
|
52
|
-
expect(getElectronBridge()).toBe(fake);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* exec-policy:会话级命令执行沙箱档位的解析与持久化。
|
|
3
|
-
* 锁定三点:只有精确字符串 'full' 才放开沙箱(其余一律落回 workspace);
|
|
4
|
-
* localStorage 缺失时(SSR / 无存储环境)读回缺省、写入静默不抛;
|
|
5
|
-
* 有存储时读写往返不漂移。持久化路径用 vi.stubGlobal 装内存实现来测,
|
|
6
|
-
* 缺失路径用 stubGlobal(..., undefined) 模拟。
|
|
7
|
-
*/
|
|
8
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
-
import {
|
|
10
|
-
EXEC_POLICY_STORAGE_KEY,
|
|
11
|
-
parseExecPolicy,
|
|
12
|
-
persistExecPolicy,
|
|
13
|
-
readStoredExecPolicy,
|
|
14
|
-
} from './exec-policy';
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
vi.unstubAllGlobals();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
/** 装一个最小内存 localStorage,返回底层 Map 便于断言。 */
|
|
21
|
-
function stubLocalStorage() {
|
|
22
|
-
const store = new Map<string, string>();
|
|
23
|
-
vi.stubGlobal('localStorage', {
|
|
24
|
-
getItem: (k: string) => (store.has(k) ? store.get(k)! : null),
|
|
25
|
-
setItem: (k: string, v: string) => void store.set(k, String(v)),
|
|
26
|
-
removeItem: (k: string) => void store.delete(k),
|
|
27
|
-
clear: () => store.clear(),
|
|
28
|
-
});
|
|
29
|
-
return store;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('parseExecPolicy', () => {
|
|
33
|
-
it("只有精确的 'full' 解析为 full", () => {
|
|
34
|
-
expect(parseExecPolicy('full')).toBe('full');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('其余一律落回 workspace(含大小写变体与 falsy)', () => {
|
|
38
|
-
for (const value of ['workspace', 'FULL', 'Full', '', null, undefined, 0, 1, {}]) {
|
|
39
|
-
expect(parseExecPolicy(value)).toBe('workspace');
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('readStoredExecPolicy / persistExecPolicy', () => {
|
|
45
|
-
it('localStorage 缺失时读回缺省 workspace,写入静默不抛', () => {
|
|
46
|
-
// vitest.setup 会装内存 localStorage;stub 成 undefined 模拟 SSR 环境。
|
|
47
|
-
vi.stubGlobal('localStorage', undefined);
|
|
48
|
-
expect(readStoredExecPolicy()).toBe('workspace');
|
|
49
|
-
expect(() => persistExecPolicy('full')).not.toThrow();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('无存储值时默认 workspace', () => {
|
|
53
|
-
stubLocalStorage();
|
|
54
|
-
expect(readStoredExecPolicy()).toBe('workspace');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('持久化后可读回,且写在约定的 key 上', () => {
|
|
58
|
-
const store = stubLocalStorage();
|
|
59
|
-
persistExecPolicy('full');
|
|
60
|
-
expect(store.get(EXEC_POLICY_STORAGE_KEY)).toBe('full');
|
|
61
|
-
expect(readStoredExecPolicy()).toBe('full');
|
|
62
|
-
persistExecPolicy('workspace');
|
|
63
|
-
expect(readStoredExecPolicy()).toBe('workspace');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('存储里是非法值时读回 workspace', () => {
|
|
67
|
-
const store = stubLocalStorage();
|
|
68
|
-
store.set(EXEC_POLICY_STORAGE_KEY, 'everything');
|
|
69
|
-
expect(readStoredExecPolicy()).toBe('workspace');
|
|
70
|
-
});
|
|
71
|
-
});
|