dsh-activity-pane 0.7.0 → 0.9.0
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/.dsh-plugin/client.js +978 -250
- package/package.json +1 -1
- package/scripts/acceptance.mjs +6 -2
- package/scripts/check.mjs +632 -164
- package/src/client.mjs +572 -150
- package/src/core.mjs +403 -98
- package/src/host.mjs +231 -4
- package/src/navigation.mjs +3 -2
package/scripts/check.mjs
CHANGED
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
cardSignature,
|
|
20
20
|
cleanPreview,
|
|
21
21
|
clampPaneWidth,
|
|
22
|
+
chatLatestAssistantSettled,
|
|
22
23
|
pagedHistoryEvents,
|
|
23
24
|
delegationActive,
|
|
24
25
|
progressAnchor,
|
|
25
|
-
detailLoadPlan,
|
|
26
26
|
conversationWorkItems,
|
|
27
27
|
conversationTimelineFromHistory,
|
|
28
28
|
foldWorkGroups,
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
foldedHistoryTimeline,
|
|
31
31
|
historyInstructionAnchor,
|
|
32
32
|
openTurnStartFromEvents,
|
|
33
|
-
openTurnStartMissing,
|
|
34
33
|
escapeCssString,
|
|
35
34
|
firstPhysicalLine,
|
|
36
35
|
fmtAbsoluteDateTime,
|
|
@@ -54,12 +53,17 @@ import {
|
|
|
54
53
|
movedToRecentIds,
|
|
55
54
|
movedToActiveIds,
|
|
56
55
|
modelMetadata,
|
|
57
|
-
|
|
56
|
+
modelFromHistoryEvents,
|
|
57
|
+
reasoningEffortFromHistoryEvents,
|
|
58
|
+
catalogModelEntries,
|
|
58
59
|
lastTurnEndFromEvents,
|
|
59
60
|
lastTurnEndFromTimings,
|
|
60
|
-
|
|
61
|
-
lastTurnDurationFromTimings,
|
|
61
|
+
lastTurnBusyFromEvents,
|
|
62
62
|
lastTurnDuration,
|
|
63
|
+
applyTurnEventToStats,
|
|
64
|
+
reconcileTurnStats,
|
|
65
|
+
normalizeBusyMs,
|
|
66
|
+
totalBusyDisplayMs,
|
|
63
67
|
pendingText,
|
|
64
68
|
progressHalfLifeSec,
|
|
65
69
|
progressOf,
|
|
@@ -204,6 +208,14 @@ unbindCard();
|
|
|
204
208
|
assert.equal(cardListeners.size, 0, "card 卸载移除 click/keydown 监听");
|
|
205
209
|
|
|
206
210
|
// ---- R-01-005/AC-01 回归:移动端切换会话后抑制原生 composer 自动聚焦(不弹软键盘) ----
|
|
211
|
+
// 宿主 DOM 契约(dsh-client-ui-conversation ComposerContentEditable):composer 输入面是
|
|
212
|
+
// Lexical contenteditable div,显式标记 data-composer-input。旧 textarea[data-phase] 在宿主
|
|
213
|
+
// DOM 上零命中会使抑制整体退化为空操作(软键盘回归根因),故钉住选择器防再漂移。
|
|
214
|
+
assert.equal(
|
|
215
|
+
COMPOSER_SELECTOR,
|
|
216
|
+
"[data-composer-input]",
|
|
217
|
+
"composer 选择器须锚定宿主 contenteditable div 的显式标记 data-composer-input",
|
|
218
|
+
);
|
|
207
219
|
const composerEl = {
|
|
208
220
|
focused: true,
|
|
209
221
|
matches: (selector) => selector === COMPOSER_SELECTOR,
|
|
@@ -330,76 +342,113 @@ assert.deepEqual(
|
|
|
330
342
|
buildEntries(subagentUnderFork, [], {}, null, null, ["source"]).map((entry) => entry.id),
|
|
331
343
|
["fork", "nested-subagent"],
|
|
332
344
|
"归档 fork 来源不影响独立 fork 及其真实子代理子树");
|
|
333
|
-
//
|
|
334
|
-
const
|
|
345
|
+
// ---- R-01-012/AC-17、AC-18 子代理模型溯源:history 尾扫最近一条携带 source.model 的 assistant/message ----
|
|
346
|
+
const subModelEvents = [
|
|
347
|
+
{ event: { type: "turn/start", seq: 1, data: { turn: 1 } } },
|
|
348
|
+
{ event: { type: "assistant/message", seq: 2, data: { turn: 1, step: 1, message: { role: "assistant", source: { kind: "model", provider: "deepseek", model: "model-a" } } } } },
|
|
349
|
+
{ event: { type: "tool/call", seq: 3, data: { turn: 1, step: 1, callId: "c1", name: "bash", arguments: "{}" } } },
|
|
350
|
+
{ event: { type: "assistant/message", seq: 4, data: { turn: 1, step: 2, message: { role: "assistant", source: { kind: "model", provider: "deepseek", model: "model-b" } } } } },
|
|
351
|
+
];
|
|
335
352
|
assert.deepEqual(
|
|
336
|
-
|
|
337
|
-
{
|
|
338
|
-
"
|
|
339
|
-
);
|
|
340
|
-
assert.equal(
|
|
341
|
-
detailLoadPlan({ detail: loadDetail, modelInflight: true }).model,
|
|
342
|
-
false,
|
|
343
|
-
"读取在途时不重复发起",
|
|
353
|
+
modelFromHistoryEvents(subModelEvents),
|
|
354
|
+
{ model: "model-b", reasoning: "" },
|
|
355
|
+
"子代理模型溯源取最近一条携带 source.model 的 assistant/message(R-01-012/AC-17)",
|
|
344
356
|
);
|
|
345
|
-
loadDetail.model = { model: "", reasoning: "" };
|
|
346
|
-
assert.equal(detailLoadPlan({ detail: loadDetail }).model, false, "失败置空后可见期内不热重试");
|
|
347
|
-
assert.equal(detailLoadPlan({ detail: {} }).model, true, "离开可见清理后重回可见允许重试");
|
|
348
357
|
assert.deepEqual(
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
"
|
|
358
|
+
modelFromHistoryEvents([{ type: "assistant/message", seq: 9, data: { message: { source: { provider: "p", model: "bare" } } } }]),
|
|
359
|
+
{ model: "bare", reasoning: "" },
|
|
360
|
+
"裸事件形态与 {event} 包装同样兼容(R-01-012/AC-17)",
|
|
352
361
|
);
|
|
362
|
+
assert.equal(modelFromHistoryEvents([]), null, "空事件流无溯源,保持空白(R-01-012/AC-18)");
|
|
353
363
|
assert.equal(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"
|
|
364
|
+
modelFromHistoryEvents([{ event: { type: "user/message", seq: 1, data: { message: { content: [] } } } }]),
|
|
365
|
+
null,
|
|
366
|
+
"无 assistant/message 时不得以其它事件冒充模型(R-01-012/AC-18)",
|
|
357
367
|
);
|
|
358
368
|
assert.equal(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
"
|
|
369
|
+
modelFromHistoryEvents([{ event: { type: "assistant/message", seq: 2, data: { message: { source: { provider: "p" } } } } }]),
|
|
370
|
+
null,
|
|
371
|
+
"source.model 缺失时不得冒充(R-01-012/AC-18)",
|
|
362
372
|
);
|
|
363
|
-
// R-01-013/AC-03、AC-04:最近卡窗口快照缺用户或 agent 预览时补读一次 history。
|
|
364
373
|
assert.equal(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
"
|
|
374
|
+
modelFromHistoryEvents([null, { event: undefined }, { event: { type: "assistant/message", seq: 3, data: { turn: 1, step: 1, message: { role: "assistant", source: { provider: "p", model: "ok" } } } } }]).model,
|
|
375
|
+
"ok",
|
|
376
|
+
"畸形条目跳过不抛错,命中项照常提取(R-01-012/AC-17)",
|
|
368
377
|
);
|
|
378
|
+
|
|
379
|
+
// ---- R-01-012/AC-17 子代理模型读取触发信号:快照最新助手节点已定案即触发 ----
|
|
380
|
+
const settledAssistantSnapshot = {
|
|
381
|
+
chat: {
|
|
382
|
+
order: ["k1", "k2", "k3", "k4"],
|
|
383
|
+
nodes: new Map([
|
|
384
|
+
["k1", { kind: "user", data: { content: [] } }],
|
|
385
|
+
["k2", { kind: "assistant-step", data: { status: "settled", turn: 1, step: 1, blocks: [] } }],
|
|
386
|
+
["k3", { kind: "tool-call", data: { status: "settled" } }],
|
|
387
|
+
["k4", { kind: "tool-call", data: { status: "running" } }],
|
|
388
|
+
]),
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
assert.equal(chatLatestAssistantSettled(settledAssistantSnapshot), true, "最新助手节点已定案即触发模型读取(R-01-012/AC-17)");
|
|
369
392
|
assert.equal(
|
|
370
|
-
|
|
393
|
+
chatLatestAssistantSettled({
|
|
394
|
+
chat: {
|
|
395
|
+
order: ["k1", "k2"],
|
|
396
|
+
nodes: new Map([
|
|
397
|
+
["k1", { kind: "user", data: { content: [] } }],
|
|
398
|
+
["k2", { kind: "assistant-step", data: { status: "running", turn: 1, step: 2, blocks: [] } }],
|
|
399
|
+
]),
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
371
402
|
false,
|
|
372
|
-
"
|
|
403
|
+
"最新助手节点仍在流式(未定案)时不触发,避免早读扑空(R-01-012/AC-18)",
|
|
373
404
|
);
|
|
374
|
-
|
|
375
|
-
assert.equal(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
405
|
+
assert.equal(chatLatestAssistantSettled(null), false, "无快照不触发(R-01-012/AC-18)");
|
|
406
|
+
assert.equal(chatLatestAssistantSettled({ chat: { order: [], nodes: new Map() } }), false, "空窗口不触发(R-01-012/AC-18)");
|
|
407
|
+
|
|
408
|
+
// ---- R-01-012/AC-17 目录分组解析显示名与 effort:provider 模型 id → {name, reasoning} 索引 ----
|
|
409
|
+
assert.deepEqual(
|
|
410
|
+
catalogModelEntries([
|
|
411
|
+
{ id: "g1", models: [{ id: "glm-5.3-flash-512k", name: "(6000D) GLM-5.3-Flash 512K", reasoning: "high" }, { id: "m2", name: "模型乙" }] },
|
|
412
|
+
{ id: "g2", models: [{ id: "m3", name: "模型丙", reasoning: "low" }] },
|
|
413
|
+
]),
|
|
414
|
+
{ "glm-5.3-flash-512k": { name: "(6000D) GLM-5.3-Flash 512K", reasoning: "high" }, m2: { name: "模型乙", reasoning: "" }, m3: { name: "模型丙", reasoning: "low" } },
|
|
415
|
+
"多分组展平为 id→{name, reasoning} 索引(R-01-012/AC-17)",
|
|
379
416
|
);
|
|
417
|
+
assert.deepEqual(
|
|
418
|
+
catalogModelEntries([null, {}, { models: null }, { models: [null, {}, { id: "", name: "x" }, { id: "ok", name: "" }, { id: "good", name: "好", reasoning: "medium" }] }]),
|
|
419
|
+
{ good: { name: "好", reasoning: "medium" } },
|
|
420
|
+
"畸形分组与畸形条目跳过,不抛错(R-01-012/AC-18)",
|
|
421
|
+
);
|
|
422
|
+
assert.deepEqual(catalogModelEntries(null), {}, "目录缺失返回空索引,调用方回退显示原始溯源 id(R-01-012/AC-18)");
|
|
423
|
+
|
|
424
|
+
// ---- R-01-012/AC-17 reasoning effort 提取:history 尾扫最新一条 request/header 配置 ----
|
|
425
|
+
const headerEvent = (config) => ({ event: { type: "request/header", seq: 1, data: { header: { config } } } });
|
|
380
426
|
assert.equal(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
427
|
+
reasoningEffortFromHistoryEvents([
|
|
428
|
+
{ event: { type: "user/message", seq: 0, data: {} } },
|
|
429
|
+
headerEvent({ provider: "p", model: "m", reasoningEffort: "low" }),
|
|
430
|
+
{ event: { type: "assistant/message", seq: 2, data: { message: { source: { provider: "p", model: "ok" } } } } },
|
|
431
|
+
]),
|
|
432
|
+
"low",
|
|
433
|
+
"请求头事件的 config.reasoningEffort 提取(R-01-012/AC-17)",
|
|
384
434
|
);
|
|
385
|
-
// R-01-009/AC-06、R-01-012/AC-12 冷窗口兜底:快照就绪但窗口缺锚点数据(开放回合起点/用户行在窗口外)时补读 history
|
|
386
435
|
assert.equal(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"
|
|
436
|
+
reasoningEffortFromHistoryEvents([headerEvent({ provider: "p", model: "m", reasoningEffort: "low" }), headerEvent({ provider: "p", model: "m", reasoningEffort: "high" })]),
|
|
437
|
+
"high",
|
|
438
|
+
"多条请求头取最新纪元(R-01-012/AC-17)",
|
|
390
439
|
);
|
|
391
440
|
assert.equal(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
"
|
|
441
|
+
reasoningEffortFromHistoryEvents([headerEvent({ provider: "p", model: "m", reasoningEffort: "low" }), headerEvent({ provider: "p", model: "m" })]),
|
|
442
|
+
null,
|
|
443
|
+
"最新请求头未声明 effort 即停,不回扫已废弃纪元(R-01-012/AC-18)",
|
|
395
444
|
);
|
|
445
|
+
assert.equal(reasoningEffortFromHistoryEvents([]), null, "空事件流无 effort(R-01-012/AC-18)");
|
|
396
446
|
assert.equal(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
"
|
|
447
|
+
reasoningEffortFromHistoryEvents([null, { event: undefined }, { event: { type: "request/header", seq: 3, data: {} } }]),
|
|
448
|
+
null,
|
|
449
|
+
"畸形条目与缺失 header 跳过不抛错(R-01-012/AC-18)",
|
|
400
450
|
);
|
|
401
451
|
|
|
402
|
-
// ---- R-01-012/AC-16 模型选择切换经目录订阅推送更新,一次性读取仅作初值 ----
|
|
403
452
|
// 目录 store 快照形状({current, groups, routable, status, ...})与 RPC value 同形兼容,经同一归一。
|
|
404
453
|
assert.deepEqual(
|
|
405
454
|
modelMetadata({
|
|
@@ -1044,6 +1093,322 @@ assert.equal(fmtElapsedMs(3_599_500), "1h0m0s", "秒数四舍五入进位到小
|
|
|
1044
1093
|
assert.equal(fmtElapsedMs(NaN), "", "NaN 时长归一为空");
|
|
1045
1094
|
assert.equal(fmtElapsedMs(Infinity), "", "Infinity 时长归一为空");
|
|
1046
1095
|
assert.equal(fmtElapsedMs(-1), "", "负时长归一为空");
|
|
1096
|
+
// ---- R-01-020 会话累计运行时长:事件流配对记账与显示合成 ----
|
|
1097
|
+
// R-01-020/AC-02:全部结束原因均计入,回合间空闲不计入。
|
|
1098
|
+
{
|
|
1099
|
+
const events = [
|
|
1100
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1101
|
+
{ type: "turn/end", seq: 2, time: 3_000 }, // completed:+2s
|
|
1102
|
+
{ type: "turn/start", seq: 3, time: 10_000 },
|
|
1103
|
+
{ type: "turn/end", seq: 4, time: 10_500, data: { reason: { kind: "aborted" } } }, // 中断:+0.5s
|
|
1104
|
+
{ type: "turn/start", seq: 5, time: 20_000 },
|
|
1105
|
+
{ type: "turn/end", seq: 6, time: 21_000, data: { reason: { kind: "error" } } }, // 错误:+1s
|
|
1106
|
+
];
|
|
1107
|
+
let stats = { busyMs: null, openTurnStart: null };
|
|
1108
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1109
|
+
assert.equal(stats.busyMs, 3_500, "R-01-020/AC-02 全部结束原因计入、空闲不计入");
|
|
1110
|
+
assert.equal(stats.openTurnStart, null, "R-01-020/AC-02 全部回合闭合后无开放起点");
|
|
1111
|
+
}
|
|
1112
|
+
// R-01-020/AC-02:起点缺失或逆序的回合跳过累加,不制造负值。
|
|
1113
|
+
{
|
|
1114
|
+
let stats = { busyMs: null, openTurnStart: null };
|
|
1115
|
+
stats = applyTurnEventToStats(stats, { type: "turn/end", seq: 1, time: 5_000 });
|
|
1116
|
+
assert.equal(stats.busyMs, null, "R-01-020/AC-02 孤儿 turn/end 不累加");
|
|
1117
|
+
stats = applyTurnEventToStats(stats, { type: "turn/start", seq: 2, time: 8_000 });
|
|
1118
|
+
stats = applyTurnEventToStats(stats, { type: "turn/end", seq: 3, time: 7_000 });
|
|
1119
|
+
assert.equal(stats.busyMs, null, "R-01-020/AC-02 时间逆序回合跳过");
|
|
1120
|
+
assert.equal(stats.openTurnStart, null, "R-01-020/AC-02 逆序回合仍清空起点");
|
|
1121
|
+
}
|
|
1122
|
+
// R-01-020/AC-04:水位幂等——转移函数为纯累加,重复事件必须由调用侧 seq ≤ watermark
|
|
1123
|
+
// 检查跳过(host.mjs 实时登记与回填写入前均做该检查);同批事件重复应用会翻倍。
|
|
1124
|
+
{
|
|
1125
|
+
const replay = [
|
|
1126
|
+
{ type: "turn/start", seq: 7, time: 1_000 },
|
|
1127
|
+
{ type: "turn/end", seq: 8, time: 2_000 },
|
|
1128
|
+
];
|
|
1129
|
+
let stats = { busyMs: null, openTurnStart: null };
|
|
1130
|
+
for (const event of replay) stats = applyTurnEventToStats(stats, event);
|
|
1131
|
+
assert.equal(stats.busyMs, 1_000, "R-01-020/AC-04 单次应用配对累计");
|
|
1132
|
+
// 模拟调用侧水位:seq ≤ watermark 的事件跳过,重复推送不重复计数。
|
|
1133
|
+
for (const event of replay) {
|
|
1134
|
+
if (event.seq <= 8) continue;
|
|
1135
|
+
stats = applyTurnEventToStats(stats, event);
|
|
1136
|
+
}
|
|
1137
|
+
assert.equal(stats.busyMs, 1_000, "R-01-020/AC-04 水位检查下重复推送不重复计数");
|
|
1138
|
+
}
|
|
1139
|
+
// R-01-020/AC-01
|
|
1140
|
+
// R-01-020/AC-03:开放回合实时增量与起点不可得降级。
|
|
1141
|
+
assert.equal(totalBusyDisplayMs({ busyMs: 3_500, openTurnStart: 30_000, now: 32_000 }), 5_500, "累计加开放回合实时已耗时");
|
|
1142
|
+
assert.equal(totalBusyDisplayMs({ busyMs: 3_500, openTurnStart: null, now: 32_000 }), 3_500, "起点不可得只显示已完成累计");
|
|
1143
|
+
assert.equal(totalBusyDisplayMs({ busyMs: null, openTurnStart: 30_000, now: 31_000 }), 1_000, "首回合进行中显示实时增量");
|
|
1144
|
+
// R-01-020/AC-06:无任何有效计时数据返回 null(调用方不显示,不以 0 冒充)。
|
|
1145
|
+
assert.equal(totalBusyDisplayMs({ busyMs: null, openTurnStart: null, now: 32_000 }), null, "无数据显示 null");
|
|
1146
|
+
assert.equal(totalBusyDisplayMs({ busyMs: null, openTurnStart: null, now: null }), null, "now 缺失且无累计为 null");
|
|
1147
|
+
assert.equal(totalBusyDisplayMs({ busyMs: 0, openTurnStart: null, now: null }), 0, "显式 0 累计仍是有效数据");
|
|
1148
|
+
// R-01-020/AC-06:记账字段归一——null/undefined 直通为 null,不得被 Number(null)===0
|
|
1149
|
+
// 误归一为 0(否则开放回合合成出 now−0 的天文数字)。
|
|
1150
|
+
assert.equal(normalizeBusyMs(null), null, "busyMs null 归一为 null");
|
|
1151
|
+
assert.equal(normalizeBusyMs(undefined), null, "busyMs undefined 归一为 null");
|
|
1152
|
+
assert.equal(normalizeBusyMs(null), null, "openTurnStart null 归一为 null");
|
|
1153
|
+
assert.equal(normalizeBusyMs(0), 0, "显式 0 仍是有效数字");
|
|
1154
|
+
assert.equal(normalizeBusyMs("123"), 123, "数字字符串可归一");
|
|
1155
|
+
assert.equal(normalizeBusyMs("abc"), null, "非数字字符串归一为 null");
|
|
1156
|
+
assert.equal(normalizeBusyMs(Number.NaN), null, "NaN 归一为 null");
|
|
1157
|
+
// R-01-020/AC-05:存量回合经全量事件重放补齐(与实时登记同一转移函数)。
|
|
1158
|
+
{
|
|
1159
|
+
const backfillEvents = [
|
|
1160
|
+
{ type: "turn/start", seq: 1, time: 100 },
|
|
1161
|
+
{ type: "turn/end", seq: 2, time: 600 },
|
|
1162
|
+
{ type: "turn/start", seq: 3, time: 900 },
|
|
1163
|
+
];
|
|
1164
|
+
let stats = { busyMs: null, openTurnStart: null };
|
|
1165
|
+
for (const event of backfillEvents) stats = applyTurnEventToStats(stats, event);
|
|
1166
|
+
assert.equal(stats.busyMs, 500, "R-01-020/AC-05 存量回合重放补齐累计");
|
|
1167
|
+
assert.equal(stats.openTurnStart, 900, "R-01-020/AC-05 重放恢复开放回合起点");
|
|
1168
|
+
// 水位缺口增量(宿主 ensureTurnStatsFresh 已有记录路径):从已有记账出发,
|
|
1169
|
+
// 仅应用 seq > watermark 的事件,补停机缺口且不重复计数(R-01-020/AC-04、AC-05)。
|
|
1170
|
+
const gapEvents = [
|
|
1171
|
+
{ type: "turn/start", seq: 1, time: 100 },
|
|
1172
|
+
{ type: "turn/end", seq: 2, time: 600 },
|
|
1173
|
+
{ type: "turn/start", seq: 3, time: 900 },
|
|
1174
|
+
{ type: "turn/end", seq: 4, time: 1_400 },
|
|
1175
|
+
];
|
|
1176
|
+
const gapState = { busyMs: 500, openTurnStart: null };
|
|
1177
|
+
const watermark = 2;
|
|
1178
|
+
let gapResult = gapState;
|
|
1179
|
+
for (const event of gapEvents) {
|
|
1180
|
+
if (event.seq <= watermark) continue;
|
|
1181
|
+
gapResult = applyTurnEventToStats(gapResult, event);
|
|
1182
|
+
}
|
|
1183
|
+
assert.equal(gapResult.busyMs, 1_000, "R-01-020/AC-05 增量重放补齐水位缺口回合");
|
|
1184
|
+
assert.equal(gapResult.openTurnStart, null, "R-01-020/AC-05 增量重放后无开放回合");
|
|
1185
|
+
}
|
|
1186
|
+
// ---- R-01-020/AC-07:回合内阻塞等待配对记账与停表 ----
|
|
1187
|
+
// 审批等待:approval/asked–decided 配对,回合结算扣除等待区间。
|
|
1188
|
+
{
|
|
1189
|
+
const events = [
|
|
1190
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1191
|
+
{ type: "approval/asked", seq: 2, time: 2_000, data: { id: "a1", toolName: "bash" } },
|
|
1192
|
+
{ type: "approval/decided", seq: 3, time: 5_000, data: { id: "a1", outcome: "allowed-once" } },
|
|
1193
|
+
{ type: "turn/end", seq: 4, time: 6_000, data: { reason: { kind: "completed" } } },
|
|
1194
|
+
];
|
|
1195
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null };
|
|
1196
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1197
|
+
assert.equal(stats.busyMs, 2_000, "R-01-020/AC-07 回合结算扣除审批等待 3s");
|
|
1198
|
+
assert.equal(stats.openWaitStart, null, "R-01-020/AC-07 回合结束后无未配对等待");
|
|
1199
|
+
assert.equal(stats.waitedMs, null, "R-01-020/AC-07 回合结束后等待累计清零");
|
|
1200
|
+
}
|
|
1201
|
+
// 提问等待:ask_user_question 的 tool/call–tool/result 配对。
|
|
1202
|
+
{
|
|
1203
|
+
const events = [
|
|
1204
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1205
|
+
{ type: "tool/call", seq: 2, time: 2_000, data: { callId: "c1", name: "ask_user_question" } },
|
|
1206
|
+
{ type: "tool/result", seq: 3, time: 7_000, data: { message: { source: { kind: "tool", callId: "c1" } } } },
|
|
1207
|
+
{ type: "turn/end", seq: 4, time: 8_000, data: { reason: { kind: "completed" } } },
|
|
1208
|
+
];
|
|
1209
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null };
|
|
1210
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1211
|
+
assert.equal(stats.busyMs, 2_000, "R-01-020/AC-07 回合结算扣除提问等待 5s");
|
|
1212
|
+
}
|
|
1213
|
+
// 非提问工具调用不开等待;kind 不一致的结算事件忽略。
|
|
1214
|
+
{
|
|
1215
|
+
const events = [
|
|
1216
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1217
|
+
{ type: "tool/call", seq: 2, time: 2_000, data: { callId: "c2", name: "bash" } },
|
|
1218
|
+
];
|
|
1219
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null };
|
|
1220
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1221
|
+
assert.equal(stats.openWaitStart, null, "R-01-020/AC-07 非提问工具调用不开等待");
|
|
1222
|
+
const mismatch = [
|
|
1223
|
+
{ type: "tool/call", seq: 3, time: 2_000, data: { callId: "c3", name: "ask_user_question" } },
|
|
1224
|
+
{ type: "approval/decided", seq: 4, time: 3_000, data: { id: "a9", outcome: "allowed-once" } },
|
|
1225
|
+
];
|
|
1226
|
+
for (const event of mismatch) stats = applyTurnEventToStats(stats, event);
|
|
1227
|
+
assert.equal(stats.openWaitStart, 2_000, "R-01-020/AC-07 kind 不一致的结算事件不关闭提问等待");
|
|
1228
|
+
assert.equal(stats.openWaitKind, "question", "R-01-020/AC-07 提问等待保持开启");
|
|
1229
|
+
const settle = [{ type: "tool/result", seq: 5, time: 4_000, data: { message: { source: { kind: "tool", callId: "c3" } } } }];
|
|
1230
|
+
for (const event of settle) stats = applyTurnEventToStats(stats, event);
|
|
1231
|
+
assert.equal(stats.waitedMs, 2_000, "R-01-020/AC-07 提问等待按配对边界结算");
|
|
1232
|
+
}
|
|
1233
|
+
// callId/id 不匹配的结算事件忽略,等待保持开启直至配对边界(DESIGN:同 callId 结算)。
|
|
1234
|
+
{
|
|
1235
|
+
const events = [
|
|
1236
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1237
|
+
{ type: "tool/call", seq: 2, time: 2_000, data: { callId: "c1", name: "ask_user_question" } },
|
|
1238
|
+
{ type: "tool/result", seq: 3, time: 3_000, data: { message: { source: { kind: "tool", callId: "cX" } } } },
|
|
1239
|
+
];
|
|
1240
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null };
|
|
1241
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1242
|
+
assert.equal(stats.openWaitStart, 2_000, "R-01-020/AC-07 callId 不匹配的 tool/result 不关闭提问等待");
|
|
1243
|
+
assert.equal(stats.openWaitId, "c1", "R-01-020/AC-07 等待记录配对 callId");
|
|
1244
|
+
const settle = [{ type: "tool/result", seq: 4, time: 4_000, data: { message: { source: { kind: "tool", callId: "c1" } } } }];
|
|
1245
|
+
for (const event of settle) stats = applyTurnEventToStats(stats, event);
|
|
1246
|
+
assert.equal(stats.waitedMs, 2_000, "R-01-020/AC-07 同 callId 的 result 结算等待");
|
|
1247
|
+
}
|
|
1248
|
+
{
|
|
1249
|
+
const events = [
|
|
1250
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1251
|
+
{ type: "approval/asked", seq: 2, time: 2_000, data: { id: "a1", toolName: "bash" } },
|
|
1252
|
+
{ type: "approval/decided", seq: 3, time: 3_000, data: { id: "aX", outcome: "allowed-once" } },
|
|
1253
|
+
];
|
|
1254
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null };
|
|
1255
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1256
|
+
assert.equal(stats.openWaitStart, 2_000, "R-01-020/AC-07 id 不匹配的 approval/decided 不关闭审批等待");
|
|
1257
|
+
const settle = [{ type: "approval/decided", seq: 4, time: 5_000, data: { id: "a1", outcome: "rejected" } }];
|
|
1258
|
+
for (const event of settle) stats = applyTurnEventToStats(stats, event);
|
|
1259
|
+
assert.equal(stats.waitedMs, 3_000, "R-01-020/AC-07 同 id 的 decided 结算审批等待");
|
|
1260
|
+
}
|
|
1261
|
+
// 等待串行不嵌套:未配对等待期间的新等待边界忽略。
|
|
1262
|
+
{
|
|
1263
|
+
const events = [
|
|
1264
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1265
|
+
{ type: "approval/asked", seq: 2, time: 2_000, data: { id: "a1", toolName: "bash" } },
|
|
1266
|
+
{ type: "approval/asked", seq: 3, time: 2_500, data: { id: "a2", toolName: "write" } },
|
|
1267
|
+
{ type: "approval/decided", seq: 4, time: 4_000, data: { id: "a1", outcome: "rejected" } },
|
|
1268
|
+
];
|
|
1269
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null };
|
|
1270
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1271
|
+
assert.equal(stats.waitedMs, 2_000, "R-01-020/AC-07 串行等待自首个边界起算、不重复计时");
|
|
1272
|
+
}
|
|
1273
|
+
// turn/end 强制结算未配对等待:等待尾部不落到运行时长里。
|
|
1274
|
+
{
|
|
1275
|
+
const events = [
|
|
1276
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1277
|
+
{ type: "approval/asked", seq: 2, time: 2_000, data: { id: "a1", toolName: "bash" } },
|
|
1278
|
+
{ type: "turn/end", seq: 3, time: 10_000, data: { reason: { kind: "aborted" } } },
|
|
1279
|
+
];
|
|
1280
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null };
|
|
1281
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1282
|
+
assert.equal(stats.busyMs, 1_000, "R-01-020/AC-07 回合结束强制结算未配对等待");
|
|
1283
|
+
assert.equal(stats.openTurnStart, null, "R-01-020/AC-07 强制结算后回合闭合");
|
|
1284
|
+
}
|
|
1285
|
+
// 换回合重置等待累计。
|
|
1286
|
+
{
|
|
1287
|
+
const events = [
|
|
1288
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1289
|
+
{ type: "tool/call", seq: 2, time: 2_000, data: { callId: "c1", name: "ask_user_question" } },
|
|
1290
|
+
{ type: "tool/result", seq: 3, time: 3_000, data: { message: { source: { kind: "tool", callId: "c1" } } } },
|
|
1291
|
+
{ type: "turn/end", seq: 4, time: 3_500, data: { reason: { kind: "completed" } } },
|
|
1292
|
+
{ type: "turn/start", seq: 5, time: 9_000 },
|
|
1293
|
+
];
|
|
1294
|
+
let stats = { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null };
|
|
1295
|
+
for (const event of events) stats = applyTurnEventToStats(stats, event);
|
|
1296
|
+
assert.equal(stats.busyMs, 1_500, "R-01-020/AC-07 首回合结算扣除提问等待");
|
|
1297
|
+
assert.equal(stats.waitedMs, null, "R-01-020/AC-07 新回合重置等待累计");
|
|
1298
|
+
}
|
|
1299
|
+
// 显示合成(AC-07):等待期间冻结——now 推进而显示值不变。
|
|
1300
|
+
{
|
|
1301
|
+
const state = { busyMs: 1_000, openTurnStart: 5_000, waitedMs: 2_000, openWaitStart: 9_000 };
|
|
1302
|
+
const frozen = totalBusyDisplayMs({ ...state, now: 10_000 });
|
|
1303
|
+
assert.equal(frozen, 3_000, "R-01-020/AC-07 等待开始时刻的运行时长被冻结显示");
|
|
1304
|
+
assert.equal(totalBusyDisplayMs({ ...state, now: 20_000 }), frozen, "R-01-020/AC-07 等待期间显示值不随 now 增长");
|
|
1305
|
+
}
|
|
1306
|
+
// 显示合成(AC-07):等待结算与回合结算处显示值连续,恢复运行后续走。
|
|
1307
|
+
{
|
|
1308
|
+
// 回合 1000 起,等待 2000–4000(waitedMs=2000);now=5000 时显示 2000。
|
|
1309
|
+
const running = { busyMs: 1_000, openTurnStart: 1_000, waitedMs: 2_000, openWaitStart: null };
|
|
1310
|
+
assert.equal(totalBusyDisplayMs({ ...running, now: 5_000 }), 3_000, "R-01-020/AC-07 等待结束后从冻结值继续累计");
|
|
1311
|
+
// turn/end t=6000 结算 busyMs = 1000 + (6000 − 1000 − 2000) = 4000;结算前一刻显示 4000。
|
|
1312
|
+
assert.equal(totalBusyDisplayMs({ ...running, now: 6_000 }), 4_000, "R-01-020/AC-07 回合结算前一刻显示值与结算后一致");
|
|
1313
|
+
assert.equal(totalBusyDisplayMs({ busyMs: 4_000, openTurnStart: null, waitedMs: null, openWaitStart: null, now: 6_000 }), 4_000, "R-01-020/AC-07 回合结算后显示 busyMs 无跳变");
|
|
1314
|
+
}
|
|
1315
|
+
// ---- R-01-020/AC-04、AC-05:reconcileTurnStats 统一收敛 ----
|
|
1316
|
+
// 无记录:全量重放。
|
|
1317
|
+
{
|
|
1318
|
+
const next = reconcileTurnStats(null, [
|
|
1319
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1320
|
+
{ type: "turn/end", seq: 2, time: 3_000 },
|
|
1321
|
+
]);
|
|
1322
|
+
assert.equal(next.busyMs, 2_000, "R-01-020/AC-05 无记录全量重放补齐累计");
|
|
1323
|
+
assert.equal(next.watermarkSeq, 2, "R-01-020/AC-05 全量重放推进水位");
|
|
1324
|
+
}
|
|
1325
|
+
// 水位超前于日志最大 seq(seq 空间被重编,如 dsh 0.1.5 V3 迁移):全量重放自愈,
|
|
1326
|
+
// 卡死的开放回合被清除、总耗时不再无限增长(原缺陷回归)。
|
|
1327
|
+
{
|
|
1328
|
+
const next = reconcileTurnStats(
|
|
1329
|
+
{ busyMs: 100, openTurnStart: 5_000, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 208_536 },
|
|
1330
|
+
[
|
|
1331
|
+
{ type: "turn/start", seq: 1, time: 1_000 },
|
|
1332
|
+
{ type: "turn/end", seq: 2, time: 3_000 },
|
|
1333
|
+
],
|
|
1334
|
+
);
|
|
1335
|
+
assert.equal(next.busyMs, 2_000, "R-01-020/AC-04 水位超前时全量重放重算累计");
|
|
1336
|
+
assert.equal(next.openTurnStart, null, "R-01-020/AC-04 水位超前时卡死的开放回合被清除");
|
|
1337
|
+
assert.equal(next.watermarkSeq, 2, "R-01-020/AC-04 自愈后水位回归当前 seq 空间");
|
|
1338
|
+
}
|
|
1339
|
+
// 正常增量:只应用 seq > watermark 的事件,不重复计数。
|
|
1340
|
+
{
|
|
1341
|
+
const next = reconcileTurnStats(
|
|
1342
|
+
{ busyMs: 500, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 2 },
|
|
1343
|
+
[
|
|
1344
|
+
{ type: "turn/start", seq: 1, time: 100 },
|
|
1345
|
+
{ type: "turn/end", seq: 2, time: 600 },
|
|
1346
|
+
{ type: "turn/start", seq: 3, time: 900 },
|
|
1347
|
+
{ type: "turn/end", seq: 4, time: 1_400 },
|
|
1348
|
+
],
|
|
1349
|
+
);
|
|
1350
|
+
assert.equal(next.busyMs, 1_000, "R-01-020/AC-05 增量收敛只补水位缺口");
|
|
1351
|
+
assert.equal(next.watermarkSeq, 4, "R-01-020/AC-05 增量收敛推进水位");
|
|
1352
|
+
}
|
|
1353
|
+
// 启动扫描:closeOpenTurn 把残留开放回合按日志最后事件时刻强制结算,尾部等待不计数。
|
|
1354
|
+
{
|
|
1355
|
+
const next = reconcileTurnStats(
|
|
1356
|
+
{ busyMs: 100, openTurnStart: 5_000, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 1 },
|
|
1357
|
+
[
|
|
1358
|
+
{ type: "turn/start", seq: 1, time: 5_000 },
|
|
1359
|
+
{ type: "tool/call", seq: 2, time: 6_000, data: { callId: "c1", name: "ask_user_question" } },
|
|
1360
|
+
{ type: "tool/result", seq: 3, time: 7_000, data: { message: { source: { kind: "tool", callId: "c1" } } } },
|
|
1361
|
+
],
|
|
1362
|
+
{ closeOpenTurn: true },
|
|
1363
|
+
);
|
|
1364
|
+
assert.equal(next.busyMs, 1_100, "R-01-020/AC-04 启动扫描按日志末事件时刻结算残留开放回合");
|
|
1365
|
+
assert.equal(next.openTurnStart, null, "R-01-020/AC-04 启动扫描后无残留开放回合");
|
|
1366
|
+
assert.equal(next.waitedMs, null, "R-01-020/AC-04 启动扫描后等待累计清零");
|
|
1367
|
+
}
|
|
1368
|
+
// 重编检测(R-01-020/AC-04):水位推进处记录 watermarkTime;增量重放同步更新。
|
|
1369
|
+
{
|
|
1370
|
+
const next = reconcileTurnStats(
|
|
1371
|
+
{ busyMs: 500, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 2, watermarkTime: 600 },
|
|
1372
|
+
[
|
|
1373
|
+
{ type: "turn/start", seq: 3, time: 10_000 },
|
|
1374
|
+
{ type: "turn/end", seq: 4, time: 11_000 },
|
|
1375
|
+
],
|
|
1376
|
+
);
|
|
1377
|
+
assert.equal(next.busyMs, 1_500, "R-01-020/AC-04 增量重放补齐缺口回合");
|
|
1378
|
+
assert.equal(next.watermarkTime, 11_000, "R-01-020/AC-04 水位推进处记录最后应用边界事件的时刻");
|
|
1379
|
+
}
|
|
1380
|
+
// 部分重叠的 seq 空间重编(水位 ≤ 日志最大 seq,既有水位超前检测不可达):forceFresh
|
|
1381
|
+
// 丢弃旧空间记账、从空状态全量重放,实时登记的重编检测据此自愈;存量记录(无
|
|
1382
|
+
// watermarkTime)同走该路径。
|
|
1383
|
+
{
|
|
1384
|
+
const forced = reconcileTurnStats(
|
|
1385
|
+
{ busyMs: 100, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 2, watermarkTime: 600 },
|
|
1386
|
+
[
|
|
1387
|
+
{ type: "turn/start", seq: 1, time: 10_000 },
|
|
1388
|
+
{ type: "turn/end", seq: 2, time: 11_000 },
|
|
1389
|
+
{ type: "turn/start", seq: 3, time: 11_500 },
|
|
1390
|
+
{ type: "turn/end", seq: 4, time: 12_000 },
|
|
1391
|
+
],
|
|
1392
|
+
{ forceFresh: true },
|
|
1393
|
+
);
|
|
1394
|
+
assert.equal(forced.busyMs, 1_500, "R-01-020/AC-04 强制全量重放按新 seq 空间重算累计");
|
|
1395
|
+
assert.equal(forced.watermarkSeq, 4, "R-01-020/AC-04 强制全量重放后水位落在新空间");
|
|
1396
|
+
assert.equal(forced.watermarkTime, 12_000, "R-01-020/AC-04 强制全量重放后水位时刻为最后边界事件");
|
|
1397
|
+
}
|
|
1398
|
+
{
|
|
1399
|
+
const legacy = reconcileTurnStats(
|
|
1400
|
+
{ busyMs: 100, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 2 },
|
|
1401
|
+
[
|
|
1402
|
+
{ type: "turn/start", seq: 1, time: 10_000 },
|
|
1403
|
+
{ type: "turn/end", seq: 2, time: 11_000 },
|
|
1404
|
+
{ type: "tool/call", seq: 3, time: 11_500, data: { callId: "c1", name: "bash" } },
|
|
1405
|
+
],
|
|
1406
|
+
{ forceFresh: true },
|
|
1407
|
+
);
|
|
1408
|
+
assert.equal(legacy.busyMs, 1_000, "R-01-020/AC-04 存量记录(无 watermarkTime)强制重放后按新空间重算");
|
|
1409
|
+
assert.equal(legacy.watermarkSeq, 3, "R-01-020/AC-04 存量记录重放后水位覆盖全部事件(含无效果事件)");
|
|
1410
|
+
assert.equal(legacy.watermarkTime, 11_500, "R-01-020/AC-04 存量记录重放后补上水位时刻");
|
|
1411
|
+
}
|
|
1047
1412
|
// R-01-013/AC-05:历史卡同时显示本地绝对日期时间与相对年龄,异常输入不制造虚假时间。
|
|
1048
1413
|
const relativeMinute = 60_000;
|
|
1049
1414
|
const relativeHour = 60 * relativeMinute;
|
|
@@ -1137,8 +1502,6 @@ const bodyLabelItems = conversationWorkItems({
|
|
|
1137
1502
|
chat: { order: ["bd"], nodes: { get: (key) => ({ key, kind: "assistant-step", data: { status: "settled", turn: 1, step: 0, blocks: [{ kind: "text", text: "纯正文" }] } }) } },
|
|
1138
1503
|
});
|
|
1139
1504
|
assert.equal(bodyLabelItems[0].label, "助手", "正文工作项 label 为中文「助手」(R-01-012/AC-09)");
|
|
1140
|
-
assert.equal(needsHistorySnapshot({ chat: { order: [] } }), true, "空 chat snapshot 需要 history fallback");
|
|
1141
|
-
assert.equal(needsHistorySnapshot({ chat: { order: ["item"] } }), false, "已 hydrate 的 chat snapshot 优先使用 order");
|
|
1142
1505
|
const models = modelMetadata({
|
|
1143
1506
|
current: { provider: "p", model: "m", reasoningEffort: "high" },
|
|
1144
1507
|
groups: [{ id: "p", models: [{ id: "m", name: "Model M", reasoning: { efforts: [{ id: "high", name: "High" }] } }] }],
|
|
@@ -1473,6 +1836,46 @@ assert.equal(coldInterrupted[0].status, "stopped", "interrupted 结果落定 sto
|
|
|
1473
1836
|
const coldOrphan = conversationTimelineFromHistory([hToolResult(1, "hc9")], 10);
|
|
1474
1837
|
assert.equal(coldOrphan.length, 1, "call 在窗口外的孤儿 result 仍成行(信息不丢失)");
|
|
1475
1838
|
assert.equal(coldOrphan[0].status, "done", "孤儿 result 落定 done,不造 running 行");
|
|
1839
|
+
// ---- V3 log 路径行为证据(T-123 dsh 0.1.5 适配):0.1.5 无独立 context 事件,非用户 source 的
|
|
1840
|
+
// user/message 即宿主 ContextMessageNode;流式期间 eventSource 以 transient assistant/live-chunk
|
|
1841
|
+
// 条目携带增量,须累积为 running live 行(否则运行卡无 running 圆点,R-01-009/AC-09 呈现缺失)----
|
|
1842
|
+
const hContext = (seq, source, text = "上下文正文") => ({ event: { type: "user/message", seq, data: { source, content: [{ type: "text", text }] } } });
|
|
1843
|
+
const ctxInject = conversationTimelineFromHistory([
|
|
1844
|
+
hUser(1, "指令"),
|
|
1845
|
+
hContext(2, { kind: "plugin", plugin: "dsh-activity-pane" }),
|
|
1846
|
+
hAgent(3, "回复"),
|
|
1847
|
+
], 10);
|
|
1848
|
+
assert.equal(ctxInject[1].kind, "context", "非用户 source 的 user/message 映射为上下文行(镜像宿主 ContextMessageNode)");
|
|
1849
|
+
assert.equal(ctxInject[1].label, "上下文注入", "注入角色标题为上下文注入");
|
|
1850
|
+
assert.equal(ctxInject[1].summary, "dsh-activity-pane", "插件 source 摘要取 plugin 名(镜像原生 provenance.label)");
|
|
1851
|
+
const ctxRecall = conversationTimelineFromHistory([
|
|
1852
|
+
hContext(1, { kind: "session-reference", references: [{ label: "会话甲" }, { label: "会话乙" }, { label: "会话甲" }] }),
|
|
1853
|
+
], 10);
|
|
1854
|
+
assert.equal(ctxRecall[0].label, "跨会话召回", "session-reference source 映射为召回角色");
|
|
1855
|
+
assert.equal(ctxRecall[0].summary, "会话甲, 会话乙", "召回摘要取引用标题去重逗号拼接(镜像原生 collect/joined)");
|
|
1856
|
+
const hLiveChunk = (seq, attemptId, text, type = "text-delta") => ({
|
|
1857
|
+
type: "transient",
|
|
1858
|
+
event: { type: "assistant/live-chunk", seq, time: 0, data: { attemptId, turn: 1, step: 0, chunk: { type, text } } },
|
|
1859
|
+
});
|
|
1860
|
+
const streaming = conversationTimelineFromHistory([hUser(1, "流式指令"), hLiveChunk(1.9, "a1", "正在"), hLiveChunk(1.95, "a1", "流式输出")], 10);
|
|
1861
|
+
assert.equal(streaming.length, 2, "流式期间用户行与 live 行并存");
|
|
1862
|
+
assert.equal(streaming[1].kind, "assistant", "live-chunk 累积为 assistant 行");
|
|
1863
|
+
assert.equal(streaming[1].status, "running", "流式 live 行为 running(运行卡 running 圆点来源)");
|
|
1864
|
+
assert.equal(streaming[1].live, true, "live 行携带 live 标记");
|
|
1865
|
+
assert.equal(streaming[1].text, "正在流式输出", "text-delta 按到达序累积为流式正文");
|
|
1866
|
+
const streamingReasoning = conversationTimelineFromHistory([hLiveChunk(1.8, "a2", "思考中", "reasoning-delta")], 10);
|
|
1867
|
+
assert.equal(streamingReasoning[0].status, "running", "reasoning-delta 同样呈现 running 行");
|
|
1868
|
+
assert.equal(streamingReasoning[0].detail, "思考中", "reasoning-delta 累积进 detail");
|
|
1869
|
+
assert.equal(streamingReasoning[0].text, "", "reasoning-delta 不混入正文");
|
|
1870
|
+
const settledStream = foldedHistoryTimeline([hUser(1, "流式指令"), hAgent(2, "最终回复")], 10);
|
|
1871
|
+
assert.ok(settledStream.every((row) => row.status !== "running"), "回合落定后 transient 条目出窗,无 running 残留");
|
|
1872
|
+
// 阻塞等待呈现(settleIdle):折叠前落定残留 running 行,组标题/状态由已定案成员派生
|
|
1873
|
+
// (快照路径 settleWhenIdle 前置语义对齐,R-01-009/AC-09「仅执行中行闪烁」)。
|
|
1874
|
+
const blockedAskRunning = foldedHistoryTimeline([hUser(1, "指令"), hToolCall(2, "ha")], 10);
|
|
1875
|
+
assert.equal(blockedAskRunning.at(-1).label, "正在运行", "未落定呈现保留「正在运行」组标题");
|
|
1876
|
+
const blockedAskSettled = foldedHistoryTimeline([hUser(1, "指令"), hToolCall(2, "ha")], 10, "", true);
|
|
1877
|
+
assert.equal(blockedAskSettled.at(-1).status, "done", "阻塞等待呈现下折叠前落定残留 running 行");
|
|
1878
|
+
assert.equal(blockedAskSettled.at(-1).label, "运行了命令", "落定后组标题由已定案成员派生(非「正在运行」)");
|
|
1476
1879
|
// historyInstructionAnchor:尾扫最近一条非空文本真实用户消息(R-01-012/AC-12 快照窗口外兜底)
|
|
1477
1880
|
assert.equal(historyInstructionAnchor([hUser(1, "旧指令"), hAgent(2, "回复"), hUser(3, "新指令")])?.text, "新指令", "锚行取最近一条用户消息");
|
|
1478
1881
|
assert.equal(historyInstructionAnchor([hUser(1, " "), hAgent(2, "回复")]), null, "空文本用户消息不作锚");
|
|
@@ -1674,6 +2077,16 @@ assert.deepEqual(
|
|
|
1674
2077
|
{ outputTokens: 0, inputTokens: 0, cacheHitPct: 0, rateTokS: 8 },
|
|
1675
2078
|
"统计合并保留有效的零值,仅对缺失字段回退(R-01-009/AC-13)",
|
|
1676
2079
|
);
|
|
2080
|
+
// R-01-009/AC-15 暂停子代理卡的冻结口径与等待卡共用同一合并语义:
|
|
2081
|
+
// 当前投影缺失的字段保留暂停前最后已知值,均缺失时回退当前投影,不伪造 0。
|
|
2082
|
+
assert.deepEqual(
|
|
2083
|
+
mergeRuntimeStats(
|
|
2084
|
+
{ outputTokens: 512, inputTokens: 900, cacheHitPct: 62, rateTokS: 32 },
|
|
2085
|
+
{ outputTokens: null, inputTokens: 1, cacheHitPct: null, rateTokS: 5 },
|
|
2086
|
+
),
|
|
2087
|
+
{ outputTokens: 512, inputTokens: 900, cacheHitPct: 62, rateTokS: 32 },
|
|
2088
|
+
"暂停子代理卡冻结暂停前最后已知统计,缺失字段才回退当前投影(R-01-009/AC-15)",
|
|
2089
|
+
);
|
|
1677
2090
|
assert.deepEqual(
|
|
1678
2091
|
usageSummary({ uncachedInputTokens: 100, cacheReadTokens: 700, cacheWriteTokens: 200 }),
|
|
1679
2092
|
{ inputTokens: 1_000, cacheHitPct: 70 },
|
|
@@ -1798,33 +2211,6 @@ assert.equal(
|
|
|
1798
2211
|
assert.equal(openTurnStartFromEvents([turnStartEv(1, 1, Number.NaN)]), null, "turn/start 时刻非法时无可用起点");
|
|
1799
2212
|
assert.equal(openTurnStartFromEvents([]), null, "空事件无开放回合起点");
|
|
1800
2213
|
assert.equal(openTurnStartFromEvents(null), null, "非数组输入归一 null");
|
|
1801
|
-
// 补读触发口径(R-01-009/AC-06):仅「运行中 + 轮内订阅已建立 + 快照无开放回合起点」算缺口
|
|
1802
|
-
assert.equal(
|
|
1803
|
-
openTurnStartMissing({ snapshotReady: true, running: true, hasLiveness: true, liveStartTime: null }),
|
|
1804
|
-
true,
|
|
1805
|
-
"运行中且快照无开放回合起点判定为缺口(超长回合冷窗口)",
|
|
1806
|
-
);
|
|
1807
|
-
assert.equal(
|
|
1808
|
-
openTurnStartMissing({ snapshotReady: true, running: true, hasLiveness: true, liveStartTime: 1000 }),
|
|
1809
|
-
false,
|
|
1810
|
-
"窗口内含开放回合起点时不是缺口",
|
|
1811
|
-
);
|
|
1812
|
-
assert.equal(
|
|
1813
|
-
openTurnStartMissing({ snapshotReady: true, running: false, hasLiveness: false, liveStartTime: null }),
|
|
1814
|
-
false,
|
|
1815
|
-
"等待/空闲会话(非运行、无 liveness 记录)不算缺口、不触发补读",
|
|
1816
|
-
);
|
|
1817
|
-
assert.equal(
|
|
1818
|
-
openTurnStartMissing({ snapshotReady: true, running: true, hasLiveness: false, liveStartTime: null }),
|
|
1819
|
-
false,
|
|
1820
|
-
"轮内订阅尚未建立时不算缺口(下一帧建立后再判定)",
|
|
1821
|
-
);
|
|
1822
|
-
assert.equal(
|
|
1823
|
-
openTurnStartMissing({ snapshotReady: false, running: true, hasLiveness: true, liveStartTime: null }),
|
|
1824
|
-
false,
|
|
1825
|
-
"快照未就绪走 historyNeeded 原路径,不算窗口缺口",
|
|
1826
|
-
);
|
|
1827
|
-
|
|
1828
2214
|
// ---- R-01-009/AC-07 工作项时间线的状态与主会话窗口语义摘要(无行级耗时,C-012)----
|
|
1829
2215
|
const statusTimeline = conversationWorkItems({
|
|
1830
2216
|
chat: {
|
|
@@ -2481,67 +2867,64 @@ assert.equal(
|
|
|
2481
2867
|
2500,
|
|
2482
2868
|
"turnTimings 取最大 endTime,忽略未结束回合",
|
|
2483
2869
|
);
|
|
2484
|
-
// ---- R-01-009/AC-12
|
|
2485
|
-
assert.equal(
|
|
2486
|
-
lastTurnDurationFromTimings(new Map([
|
|
2487
|
-
[1, { startTime: 100, endTime: 900 }],
|
|
2488
|
-
[2, { startTime: 1000 }],
|
|
2489
|
-
[3, { startTime: 2000, endTime: 2500 }],
|
|
2490
|
-
])),
|
|
2491
|
-
500,
|
|
2492
|
-
"turnTimings 取最近已结束回合的起止差值,不取更早回合或开放回合",
|
|
2493
|
-
);
|
|
2494
|
-
assert.equal(
|
|
2495
|
-
lastTurnDurationFromTimings(new Map([[1, { startTime: 900, endTime: 100 }]])),
|
|
2496
|
-
null,
|
|
2497
|
-
"起点晚于终点时不生成虚假回合耗时",
|
|
2498
|
-
);
|
|
2499
|
-
assert.equal(lastTurnDurationFromTimings(new Map([[1, { startTime: 100 }]])), null, "无 endTime 不生成等待耗时");
|
|
2870
|
+
// ---- R-01-009/AC-12 等待卡保留最近完整回合耗时(busy 口径,R-01-020 语义) ----
|
|
2500
2871
|
assert.equal(
|
|
2501
|
-
|
|
2872
|
+
lastTurnBusyFromEvents([
|
|
2502
2873
|
{ event: { type: "turn/start", time: 100, data: { turn: 1 } } },
|
|
2503
2874
|
{ event: { type: "turn/end", time: 900, data: { turn: 1 } } },
|
|
2504
2875
|
{ event: { type: "turn/start", time: 1000, data: { turn: 2 } } },
|
|
2505
2876
|
{ event: { type: "turn/end", time: 2500, data: { turn: 2 } } },
|
|
2506
2877
|
]),
|
|
2507
2878
|
1500,
|
|
2508
|
-
"history
|
|
2879
|
+
"history 取最近已结束回合的起止差值",
|
|
2509
2880
|
);
|
|
2510
2881
|
assert.equal(
|
|
2511
|
-
|
|
2512
|
-
{ event: { type: "turn/start", time:
|
|
2513
|
-
{ event: { type: "
|
|
2882
|
+
lastTurnBusyFromEvents([
|
|
2883
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2884
|
+
{ event: { type: "tool/call", time: 2000, data: { callId: "c1", name: "ask_user_question" } } },
|
|
2885
|
+
{ event: { type: "tool/result", time: 7000, data: { message: { source: { kind: "tool", callId: "c1" } } } } },
|
|
2886
|
+
{ event: { type: "turn/end", time: 8000, data: { turn: 1 } } },
|
|
2514
2887
|
]),
|
|
2515
|
-
|
|
2516
|
-
"
|
|
2888
|
+
2000,
|
|
2889
|
+
"最近回合耗时扣除回合内提问等待(busy 口径,与总耗时一致)",
|
|
2517
2890
|
);
|
|
2518
2891
|
assert.equal(
|
|
2519
|
-
|
|
2520
|
-
{ event: { type: "turn/start", time:
|
|
2521
|
-
{ event: { type: "
|
|
2892
|
+
lastTurnBusyFromEvents([
|
|
2893
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2894
|
+
{ event: { type: "approval/asked", time: 2000, data: { id: "a1" } } },
|
|
2895
|
+
{ event: { type: "approval/decided", time: 6000, data: { id: "a1" } } },
|
|
2896
|
+
{ event: { type: "turn/end", time: 7000, data: { turn: 1 } } },
|
|
2897
|
+
]),
|
|
2898
|
+
2000,
|
|
2899
|
+
"最近回合耗时扣除回合内审批等待",
|
|
2900
|
+
);
|
|
2901
|
+
assert.equal(
|
|
2902
|
+
lastTurnBusyFromEvents([
|
|
2903
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2904
|
+
{ event: { type: "turn/end", time: 900, data: { turn: 1 } } },
|
|
2522
2905
|
]),
|
|
2523
2906
|
null,
|
|
2524
|
-
"
|
|
2907
|
+
"起止逆序时不生成虚假回合耗时",
|
|
2525
2908
|
);
|
|
2526
2909
|
assert.equal(
|
|
2527
|
-
|
|
2910
|
+
lastTurnBusyFromEvents([
|
|
2911
|
+
{ event: { type: "turn/start", time: null, data: { turn: 1 } } },
|
|
2912
|
+
{ event: { type: "turn/end", time: 100, data: { turn: 1 } } },
|
|
2913
|
+
]),
|
|
2528
2914
|
null,
|
|
2529
|
-
"
|
|
2915
|
+
"起点缺失时不把 null 当作时间零点",
|
|
2530
2916
|
);
|
|
2531
2917
|
assert.equal(
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
800,
|
|
2540
|
-
"turnTimings 最新孤立 end 不得与更早完整回合错配",
|
|
2918
|
+
lastTurnBusyFromEvents([
|
|
2919
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2920
|
+
{ event: { type: "tool/call", time: 2000, data: { callId: "c1", name: "ask_user_question" } } },
|
|
2921
|
+
{ event: { type: "turn/end", time: 5000, data: { turn: 1 } } },
|
|
2922
|
+
]),
|
|
2923
|
+
1000,
|
|
2924
|
+
"turn/end 强制结算未配对等待:等待尾部不计入最近回合耗时",
|
|
2541
2925
|
);
|
|
2542
2926
|
assert.equal(
|
|
2543
2927
|
lastTurnDuration({
|
|
2544
|
-
turnTimings: new Map(),
|
|
2545
2928
|
history: [
|
|
2546
2929
|
{ event: { type: "turn/start", time: 100, data: { turn: 1 } } },
|
|
2547
2930
|
{ event: { type: "turn/end", time: 900, data: { turn: 1 } } },
|
|
@@ -2549,24 +2932,10 @@ assert.equal(
|
|
|
2549
2932
|
],
|
|
2550
2933
|
}),
|
|
2551
2934
|
800,
|
|
2552
|
-
"
|
|
2553
|
-
);
|
|
2554
|
-
assert.equal(
|
|
2555
|
-
lastTurnDuration({
|
|
2556
|
-
turnTimings: new Map([[1, { startTime: 100, endTime: 900 }]]),
|
|
2557
|
-
history: [
|
|
2558
|
-
{ event: { type: "turn/start", time: 1000, data: { turn: 2 } } },
|
|
2559
|
-
{ event: { type: "turn/end", time: 1300, data: { turn: 2 } } },
|
|
2560
|
-
],
|
|
2561
|
-
}),
|
|
2562
|
-
300,
|
|
2563
|
-
"多来源时按最近结束时刻选择同一最新回合耗时",
|
|
2564
|
-
);
|
|
2565
|
-
assert.equal(
|
|
2566
|
-
lastTurnDuration({ turnTimings: new Map([[1, { startTime: 100, endTime: 900 }]]), history: [] }),
|
|
2567
|
-
800,
|
|
2568
|
-
"无 history 时从 turnTimings 提取回合耗时",
|
|
2935
|
+
"最新孤立 end 不得与更早完整回合错配",
|
|
2569
2936
|
);
|
|
2937
|
+
assert.equal(lastTurnDuration({ history: [] }), null, "空 history 无回合耗时");
|
|
2938
|
+
assert.equal(lastTurnDuration({ history: null }), null, "history 缺失回退 null");
|
|
2570
2939
|
const refineSnap = {
|
|
2571
2940
|
ids: ["sTurn", "sPrompt", "sNone"],
|
|
2572
2941
|
byId: {
|
|
@@ -2918,9 +3287,9 @@ assert.deepEqual(
|
|
|
2918
3287
|
);
|
|
2919
3288
|
|
|
2920
3289
|
// ---- R-01-014/AC-05 补充数据失败降级为空字段并可重试 ----
|
|
2921
|
-
//
|
|
2922
|
-
//
|
|
2923
|
-
|
|
3290
|
+
// (dsh 0.1.5 起 per-session models RPC 移除,模型提取随日志读取统一进行:失败置空
|
|
3291
|
+
// 语义由 R-01-012/AC-18 的空白归一单测与本文件日志读取门禁锚点共同
|
|
3292
|
+
// 承载——失败不置标记、下一可见期允许重试。)
|
|
2924
3293
|
|
|
2925
3294
|
// ---- R-01-015/AC-02 拖拽宽度夹取 | R-01-015/AC-04 持久化恢复归一 ----
|
|
2926
3295
|
assert.equal(clampPaneWidth(280), 280, "范围内宽度原样保留");
|
|
@@ -3076,8 +3445,8 @@ assert.ok(
|
|
|
3076
3445
|
assert.ok(!bundle.includes("dshcf") && !bundle.includes("autoCollapseActive"), "无 dsh-auto-collapse 探测残留(R-01-017、C-017)");
|
|
3077
3446
|
assert.ok(!bundle.includes("nativeWorkItemRow") && !bundle.includes("cloneNativeIcon") && !bundle.includes("nativeIconsByTraceKey"), "原生行匹配/图标克隆机器无残留(C-017)");
|
|
3078
3447
|
assert.ok(!bundle.includes("mergeTraceStatus") && !bundle.includes("allowNativePresentation"), "行状态直接采用核心派生值,无合并/切换层(C-017)");
|
|
3079
|
-
assert.ok(bundle.includes("
|
|
3080
|
-
assert.ok(bundle.includes("
|
|
3448
|
+
assert.ok(bundle.includes(".page("), "冷会话使用 remote 日志分页一次性补齐(Session V3)");
|
|
3449
|
+
assert.ok(bundle.includes(".modelCatalog("), "模型/reasoning 使用部署级 modelCatalog 数据");
|
|
3081
3450
|
assert.ok(bundle.includes("dap-token-stats"), "token 统计 DOM 位于进度条之后");
|
|
3082
3451
|
assert.ok(
|
|
3083
3452
|
bundle.includes('makeEl("span", "dap-token-main")') && bundle.includes('makeEl("span", "dap-token-time")'),
|
|
@@ -3086,7 +3455,7 @@ assert.ok(
|
|
|
3086
3455
|
assert.ok(bundle.includes("function renderTokenStats"), "运行卡统计行继续复用既有渲染逻辑(R-01-009/AC-05)");
|
|
3087
3456
|
// R-01-009/AC-13 等待卡保留进入等待前最后已知的 token 统计
|
|
3088
3457
|
assert.ok(
|
|
3089
|
-
bundle.includes('return [head, row, makeEl("div", "dap-trace"),
|
|
3458
|
+
bundle.includes('return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];'),
|
|
3090
3459
|
"等待卡骨架在时间线后保留统计行(R-01-009/AC-13)",
|
|
3091
3460
|
);
|
|
3092
3461
|
assert.ok(
|
|
@@ -3112,7 +3481,7 @@ assert.ok(!bundle.includes("≈"), "速率不再携带约等于符号(R-01-009
|
|
|
3112
3481
|
assert.ok(bundle.includes("dap-history-line"), "历史卡包含用户/agent 两条消息预览行");
|
|
3113
3482
|
// R-01-013/AC-12 最近历史卡保留最近回合统计
|
|
3114
3483
|
assert.ok(
|
|
3115
|
-
bundle.includes('return [head, row, userLine, agentLine,
|
|
3484
|
+
bundle.includes('return [head, row, userLine, agentLine, makeStatsRow(), makeEl("div", "dap-note")];'),
|
|
3116
3485
|
"历史卡统计行位于助手预览后、活动时间前",
|
|
3117
3486
|
);
|
|
3118
3487
|
assert.ok(bundle.includes("statsFromProjection"), "历史卡统计复用列表投影派生函数(R-01-013/AC-12)");
|
|
@@ -3183,7 +3552,7 @@ assert.ok(bundle.includes("function activeSessionIds(byId = {})"), "活动子代
|
|
|
3183
3552
|
// ---- R-01-016/AC-01 等待卡保留最近工作项时间线 ----
|
|
3184
3553
|
assert.ok(
|
|
3185
3554
|
bundle.includes("awaitHead.append(capsule);") &&
|
|
3186
|
-
bundle.includes('return [head, row, makeEl("div", "dap-trace"),
|
|
3555
|
+
bundle.includes('return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];'),
|
|
3187
3556
|
"awaiting 骨架在标题行与统计行、末行两段(胶囊+正文)之间含时间线,固定耗时由统计行承载(R-01-016/AC-01、R-01-009/AC-12,C-043)",
|
|
3188
3557
|
);
|
|
3189
3558
|
// ---- R-01-002/AC-10 完成提醒卡「移入历史」按钮 ----
|
|
@@ -3197,7 +3566,7 @@ assert.ok(
|
|
|
3197
3566
|
"按钮点击/键盘激活写回 ack 且阻断卡片跳转(R-01-002/AC-10)",
|
|
3198
3567
|
);
|
|
3199
3568
|
assert.ok(bundle.includes('confirm.hidden = entry.waitClass !== "done"'), "仅完成提醒卡显示「移入历史」按钮,阻塞等待卡不显示(R-01-002/AC-10)");
|
|
3200
|
-
assert.ok(bundle.includes("new window.EventSource(`${
|
|
3569
|
+
assert.ok(bundle.includes("new window.EventSource(`${PANE_API_BASE}/acks/stream`)"), "完成确认状态经 SSE 通道订阅(R-01-002/AC-11、AC-12)");
|
|
3201
3570
|
// R-01-002/AC-12 缺陷回归:移动 PWA 后台恢复后 ack 通道必须自愈(EventSource CLOSED/半开
|
|
3202
3571
|
// 不再自动重连),否则完成等待中的会话被误判入历史区直至整页重载。
|
|
3203
3572
|
assert.ok(
|
|
@@ -3212,7 +3581,7 @@ assert.ok(
|
|
|
3212
3581
|
bundle.includes('document.removeEventListener("visibilitychange", onVisibilityResume)') && bundle.includes('window.removeEventListener("pageshow", onPageShow)'),
|
|
3213
3582
|
"卸载时移除 ack 通道自愈监听(R-01-002/AC-12)",
|
|
3214
3583
|
);
|
|
3215
|
-
assert.ok(bundle.includes("fetch(`${
|
|
3584
|
+
assert.ok(bundle.includes("fetch(`${PANE_API_BASE}/ack`"), "确认写回经宿主侧 ack 路由(R-01-002/AC-10、AC-11)");
|
|
3216
3585
|
assert.ok(
|
|
3217
3586
|
!bundle.includes("updateCompletedHolds") && !bundle.includes("heldCompletedIds") && !bundle.includes("prevActiveMainIds"),
|
|
3218
3587
|
"响应保持易失记账全套移除(C-030)",
|
|
@@ -3225,12 +3594,12 @@ assert.ok(
|
|
|
3225
3594
|
assert.equal(
|
|
3226
3595
|
bundle.split('makeEl("span", "dap-pct")').length - 1,
|
|
3227
3596
|
1,
|
|
3228
|
-
"
|
|
3597
|
+
"百分比文本元素经 makeProgressRow 共享骨架单点创建(R-01-009/AC-06、AC-14)",
|
|
3229
3598
|
);
|
|
3230
3599
|
assert.equal(
|
|
3231
3600
|
bundle.split('querySelector(".dap-pct")').length - 1,
|
|
3232
3601
|
1,
|
|
3233
|
-
"
|
|
3602
|
+
"百分比文本写入经 renderProgressRow 单点承载(R-01-009/AC-06、AC-14)",
|
|
3234
3603
|
);
|
|
3235
3604
|
// ---- R-01-016/AC-04 时间线数据在途时显示加载指示、返回就地填充 ----
|
|
3236
3605
|
assert.ok(
|
|
@@ -3278,8 +3647,24 @@ assert.ok(!bundle.includes('"Think"') && !bundle.includes('"Assistant"'), "bundl
|
|
|
3278
3647
|
assert.ok(bundle.includes('"思考"') && bundle.includes('"助手"'), "bundle 含中文「思考」「助手」标签(R-01-012/AC-09、AC-10)");
|
|
3279
3648
|
assert.ok(bundle.includes("session.subscribe"), "运行卡通过 native session subscribe 接收实时推送");
|
|
3280
3649
|
assert.ok(
|
|
3281
|
-
clientSource.includes('const inject = ["
|
|
3282
|
-
"sessions/workspaces 通过 client inject 注入,不依赖服务发现定时器",
|
|
3650
|
+
clientSource.includes('const inject = ["sessions", "workspaces", "uiSession", "remote.session"];'),
|
|
3651
|
+
"sessions/workspaces/uiSession/remote.session 通过 client inject 注入,不依赖服务发现定时器",
|
|
3652
|
+
);
|
|
3653
|
+
assert.ok(
|
|
3654
|
+
bundle.includes("uiSession?.pendingInteractions") && bundle.includes("pendingInteraction: interaction.kind"),
|
|
3655
|
+
"0.1.5 等待行动经 uiSession.pendingInteractions 快照回填行副本(sessions 行不再承载 pendingInteraction)",
|
|
3656
|
+
);
|
|
3657
|
+
assert.ok(
|
|
3658
|
+
bundle.includes("historyDeepReadDone = true") && bundle.includes("historyDeepReadDone !== true"),
|
|
3659
|
+
"日志深翻每可见期至多一次:完成即置位(失败同样置位不热重试),重入可见由详情记账清理放行重试(R-01-014/AC-05)",
|
|
3660
|
+
);
|
|
3661
|
+
assert.ok(
|
|
3662
|
+
clientSource.includes("deepReadNeeded && !historyLoads.has(id)"),
|
|
3663
|
+
"深翻在途守卫:同一会话深翻在途不重复入队,防 page RPC 风暴与加载指示在途性高频抖动(R-01-014/AC-05)",
|
|
3664
|
+
);
|
|
3665
|
+
assert.ok(
|
|
3666
|
+
clientSource.includes('!detail.log && detail.snapshot?.openState !== "open"'),
|
|
3667
|
+
"open 仅在日志窗口缺席时发起:已水合会话不逐帧重发 open,防在途翻转驱动加载指示抖动",
|
|
3283
3668
|
);
|
|
3284
3669
|
assert.ok(!bundle.includes("serviceTimer"), "服务发现不得保留后台定时器");
|
|
3285
3670
|
assert.ok(!bundle.includes("frameProbeTimer"), "宿主 frame 发现不得保留后台定时器");
|
|
@@ -3311,24 +3696,33 @@ assert.ok(bundle.includes("loadingPreviews"), "预览字段级加载指示并入
|
|
|
3311
3696
|
assert.ok(bundle.includes("renderTraceLoading"), "时间线区数据在途时显示加载行");
|
|
3312
3697
|
assert.ok(clientSource.includes('e2eParams.get("dap-e2e-model-delay")'), "detail 渐进 E2E 接缝由显式 URL fragment 启用");
|
|
3313
3698
|
assert.ok(clientSource.includes("Math.min(requestedModelDelay, 1_000)"), "detail 渐进 E2E 延迟上限为 1 秒");
|
|
3314
|
-
assert.ok(
|
|
3315
|
-
|
|
3699
|
+
assert.ok(
|
|
3700
|
+
clientSource.includes("if (!subagent) {") &&
|
|
3701
|
+
clientSource.includes("subscribeModelDirectory(id, detail);") &&
|
|
3702
|
+
clientSource.includes("loadDirectoryOnce(id);"),
|
|
3703
|
+
"仅主会话建立 model directory 订阅与一次性 load(子代理目录不可用不订阅)",
|
|
3704
|
+
);
|
|
3705
|
+
assert.ok(
|
|
3706
|
+
clientSource.includes('delayedModelCall(() => (typeof directory.load === "function" ? directory.load() : null))'),
|
|
3707
|
+
"detail fixture 延迟目录一次性 load,不伪造 model response",
|
|
3708
|
+
);
|
|
3316
3709
|
assert.ok(clientSource.includes("Promise.resolve().then(call)"), "detail fixture 保留 models RPC 同步异常的 Promise catch 降级语义");
|
|
3317
3710
|
assert.ok(clientSource.includes("e2eModelDelayWaiters.clear()"), "卸载时取消并结清 detail fixture 延迟,不残留 timer/promise");
|
|
3318
3711
|
assert.ok(bundle.includes("promise.then(queueSync, queueSync)"), "补充数据逐个完成即重绘(先就绪先显示)");
|
|
3319
3712
|
assert.ok(bundle.includes("LOAD_CONCURRENCY"), "冷数据读取经并发池限制慢网挤占");
|
|
3320
3713
|
assert.ok(bundle.includes("session.open"), "运行卡通过 native session open hydrate 非当前会话");
|
|
3321
3714
|
assert.ok(bundle.includes("sessionOpenLoads"), "session.open 请求与 cold history fallback 不重复");
|
|
3322
|
-
// R-01-012/AC-16 模型目录订阅:store
|
|
3715
|
+
// R-01-012/AC-16 模型目录订阅:store 推送更新、随可见性/卸载清理;dsh 0.1.5 起目录
|
|
3716
|
+
// store 惰性加载,订阅后补一次一次性 load(C-024 的 generation 竞争以最新操作胜出)
|
|
3323
3717
|
assert.ok(bundle.includes('ctx.get("modelDirectories")'), "模型实时选择来自原生 modelDirectories 服务(可选软依赖)");
|
|
3324
3718
|
assert.ok(bundle.includes("directory.store.subscribe"), "订阅目录 store 推送模型选择变更");
|
|
3325
|
-
assert.ok(
|
|
3719
|
+
assert.ok(bundle.includes("directory.load"), "目录 store 一次性 load:0.1.5 惰性加载下不 load 不产出当前选择");
|
|
3326
3720
|
assert.ok(bundle.includes("pruneSubscriptions(modelDirectorySubs, visibleIds)"), "模型目录订阅随可见性先 unsubscribe 再除名");
|
|
3327
3721
|
assert.ok(bundle.includes("pruneSubscriptions(modelDirectorySubs, new Set())"), "卸载时模型目录订阅整体退订归零");
|
|
3328
3722
|
assert.ok(bundle.includes("detail.modelLive"), "目录订阅已产值时晚到的一次性 RPC 不回写旧值");
|
|
3329
3723
|
assert.ok(!bundle.includes("events.mux"), "不常驻全局 mux,当前会话使用原生 session subscribe");
|
|
3330
3724
|
assert.ok(
|
|
3331
|
-
bundle.includes('return [head, row, makeEl("div", "dap-trace"), progressRow,
|
|
3725
|
+
bundle.includes('return [head, row, makeEl("div", "dap-trace"), progressRow, makeStatsRow()];'),
|
|
3332
3726
|
"running 卡 token 统计骨架位于进度条骨架之后",
|
|
3333
3727
|
);
|
|
3334
3728
|
assert.ok(
|
|
@@ -3353,12 +3747,21 @@ assert.ok(
|
|
|
3353
3747
|
!bundle.includes("ctx.get(\"dsh-answer-pet\")"),
|
|
3354
3748
|
"不得以服务方式依赖第三方宠物插件",
|
|
3355
3749
|
);
|
|
3356
|
-
// R-02-004/AC-02(演进,C-030
|
|
3357
|
-
//
|
|
3358
|
-
//
|
|
3750
|
+
// R-02-004/AC-02(演进,C-030、C-074):HTTP 请求仅两处——完成确认写回(用户操作触发的
|
|
3751
|
+
// 一次性 POST)与累计运行时长懒回填触发(每会话至多一次的一次性 GET,随可见会话触发),
|
|
3752
|
+
// 均非状态轮询;轮内状态与回合统计仍只来自原生订阅推送与 SSE 推送。
|
|
3753
|
+
// fetch/EventSource 数量由下两条断言钉住:轮询需要重复请求,受限的调用面即排除轮询形态。
|
|
3754
|
+
assert.ok(
|
|
3755
|
+
(bundle.match(/fetch\(/g) ?? []).length === 2
|
|
3756
|
+
&& bundle.includes("fetch(`${PANE_API_BASE}/ack`")
|
|
3757
|
+
&& bundle.includes("fetch(`${PANE_API_BASE}/busy?ids="),
|
|
3758
|
+
"HTTP 请求仅完成确认写回与 busy 懒回填触发两处,指向宿主侧自家路由(R-01-002/AC-10、R-01-020、C-030、C-074)",
|
|
3759
|
+
);
|
|
3359
3760
|
assert.ok(
|
|
3360
|
-
(bundle.match(/
|
|
3361
|
-
|
|
3761
|
+
(bundle.match(/new window\.EventSource\(/g) ?? []).length === 2
|
|
3762
|
+
&& bundle.includes("${PANE_API_BASE}/acks/stream")
|
|
3763
|
+
&& bundle.includes("${PANE_API_BASE}/busy/stream"),
|
|
3764
|
+
"SSE 订阅仅 acks 与 busy 两条通道,均连接即收全量快照(C-030、C-074)",
|
|
3362
3765
|
);
|
|
3363
3766
|
|
|
3364
3767
|
// ---- R-02-003/AC-02 卸载时清理注入元素、样式与监听 ----
|
|
@@ -3459,9 +3862,9 @@ assert.ok(bundle.includes("dataset.traceKey"), "同一流程节点复用 DOM,
|
|
|
3459
3862
|
assert.ok(!bundle.includes(".dap-trace-item[data-status=\"running\"]::before {\n animation: none !important;"), "降低动效设置不关闭运行点脉冲");
|
|
3460
3863
|
|
|
3461
3864
|
// R-01-013/AC-09
|
|
3462
|
-
//
|
|
3463
|
-
assert.ok(bundle.includes(
|
|
3464
|
-
assert.ok(bundle.includes("
|
|
3865
|
+
// 全部会话卡标题(活动卡、子代理卡、最近卡)统一常规字重(不加粗),无按卡类覆盖。
|
|
3866
|
+
assert.ok(bundle.includes("white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 400;"), "会话卡标题使用常规字重(不加粗)");
|
|
3867
|
+
assert.ok(!bundle.includes('[data-kind="recent"] .dap-title {'), "最近卡标题不再有独立字重覆盖(与活动卡共用常规字重)");
|
|
3465
3868
|
|
|
3466
3869
|
// R-01-013/AC-10
|
|
3467
3870
|
// 最近历史卡整体不透明度低于活动卡,弱化历史区视觉强调。
|
|
@@ -3872,6 +4275,63 @@ assert.ok(
|
|
|
3872
4275
|
"等待当前卡重声明蓝色描边与光晕(组合选择器压过等待态橙色)",
|
|
3873
4276
|
);
|
|
3874
4277
|
|
|
4278
|
+
// ---- R-01-012/AC-17~AC-18、R-01-009/AC-14~AC-15 子代理卡模型/进度/统计契约 ----
|
|
4279
|
+
// 子代理模型经既有 history 溯源提取(models RPC 对子代理被宿主 agent-busy 拒绝)。
|
|
4280
|
+
assert.ok(bundle.includes("modelFromHistoryEvents") && bundle.includes("chatLatestAssistantSettled"), "子代理模型溯源提取与触发信号进入 bundle(R-01-012/AC-17)");
|
|
4281
|
+
assert.ok(
|
|
4282
|
+
clientSource.includes("if (planSubagentModelRead) {") && clientSource.includes("detail.modelReadDone = true;"),
|
|
4283
|
+
"history 读取落地时为子代理提取模型溯源并记账每次可见期单次尝试(R-01-012/AC-17)",
|
|
4284
|
+
);
|
|
4285
|
+
assert.ok(
|
|
4286
|
+
clientSource.includes("chatLatestAssistantSettled(detailSnapshot)"),
|
|
4287
|
+
"模型读取时机锚定已定案助手行或非运行状态(R-01-012/AC-17)",
|
|
4288
|
+
);
|
|
4289
|
+
assert.ok(
|
|
4290
|
+
clientSource.includes("entry.kind === \"subagent\" && historyLoads.has(entry.id)"),
|
|
4291
|
+
"子代理溯源读取在途时模型区显示加载指示(R-01-014/AC-02、R-01-012/AC-17)",
|
|
4292
|
+
);
|
|
4293
|
+
// 溯源 id → 显示名:目录分组经主会话既有两条到达路径就地收割,渲染时对子代理条目解析。
|
|
4294
|
+
assert.ok(
|
|
4295
|
+
clientSource.includes("const catalogEntries = Object.create(null);"),
|
|
4296
|
+
"目录条目索引为无原型对象,模型 id 恰为继承键名时不得穿透回退(R-01-012/AC-17)",
|
|
4297
|
+
);
|
|
4298
|
+
assert.ok(
|
|
4299
|
+
clientSource.includes("harvestCatalog(snap.groups);") &&
|
|
4300
|
+
clientSource.includes("harvestCatalog(value.groups);"),
|
|
4301
|
+
"目录分组在 store 订阅与一次性 modelCatalog 两条到达路径就地收割(R-01-012/AC-17)",
|
|
4302
|
+
);
|
|
4303
|
+
assert.ok(
|
|
4304
|
+
clientSource.includes("detail.model.reasoning = reasoningEffortFromHistoryEvents(events) ?? \"\";"),
|
|
4305
|
+
"子代理 history 落地时折叠最新请求头 effort(R-01-012/AC-17)",
|
|
4306
|
+
);
|
|
4307
|
+
assert.ok(
|
|
4308
|
+
clientSource.includes("if (catalogEntry?.name) entry.model = catalogEntry.name;") &&
|
|
4309
|
+
clientSource.includes("if (!entry.reasoning && catalogEntry?.reasoning) entry.reasoning = catalogEntry.reasoning;"),
|
|
4310
|
+
"渲染时子代理溯源 id 经目录解析为显示名与 effort 回退,未覆盖保留原始(R-01-012/AC-17、AC-18)",
|
|
4311
|
+
);
|
|
4312
|
+
// 运行中子代理卡与主会话运行卡同构:进度行 + 统计行进骨架,渲染按同一锚点与曲线。
|
|
4313
|
+
assert.ok(
|
|
4314
|
+
clientSource.includes("return [row, makeEl(\"div\", \"dap-subtrace\"), progressRow, makeStatsRow()];") &&
|
|
4315
|
+
clientSource.includes("row.append(makeEl(\"span\", \"dap-dot\"), makeEl(\"span\", \"dap-title\"), model, makeEl(\"span\", \"dap-total-time\"));"),
|
|
4316
|
+
"子代理卡骨架:模型名入标题行右侧,进度行与统计行承载(R-01-009/AC-14、R-01-012/AC-17)",
|
|
4317
|
+
);
|
|
4318
|
+
assert.ok(
|
|
4319
|
+
clientSource.includes("progressOf({ elapsedMs, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) })"),
|
|
4320
|
+
"运行中子代理卡按与运行卡相同的锚点口径现算进度(R-01-009/AC-14)",
|
|
4321
|
+
);
|
|
4322
|
+
assert.ok(
|
|
4323
|
+
clientSource.includes("entry.kind === \"subagent\" && runLikeIds.has(entry.id)"),
|
|
4324
|
+
"运行卡时钟条件纳入运行中子代理卡(R-01-009/AC-14)",
|
|
4325
|
+
);
|
|
4326
|
+
// 非运行子代理卡:进度条整行隐藏、统计与最近回合耗时冻结。
|
|
4327
|
+
assert.ok(
|
|
4328
|
+
clientSource.includes("} else if (!progressRow.hidden) {") && clientSource.includes("progressRow.hidden = true;"),
|
|
4329
|
+
"锚点空闲时子代理卡进度行整行隐藏(R-01-009/AC-15)",
|
|
4330
|
+
);
|
|
4331
|
+
assert.ok(
|
|
4332
|
+
clientSource.includes("entry.progress = null;") && bundle.includes("[data-dsh-activity-pane] .dap-progress[hidden] { display: none; }"),
|
|
4333
|
+
"非运行子代理卡冻结统计并隐藏进度行(R-01-009/AC-15)",
|
|
4334
|
+
);
|
|
3875
4335
|
|
|
3876
4336
|
// ---- R-01-002/AC-10~AC-12 宿主侧完成确认契约(C-030)----
|
|
3877
4337
|
// R-01-002/AC-12 刷新/重连恢复:状态由宿主侧持久化承载,不依赖客户端在线观测。
|
|
@@ -3887,6 +4347,14 @@ assert.ok(hostSource.includes("const API_PATH = '/dsh-activity-pane/api'") && ho
|
|
|
3887
4347
|
assert.ok(hostSource.includes("'/acks/stream'") && hostSource.includes("text/event-stream"), "SSE 推送通道(AC-11、AC-12)");
|
|
3888
4348
|
assert.ok(hostSource.includes("'/ack'") && hostSource.includes("ackedAt: Date.now()"), "ack 写回路由(AC-10~AC-12)");
|
|
3889
4349
|
assert.ok(hostSource.includes("streamClients") && hostSource.includes("for (const res of streamClients)"), "SSE 连接集合随卸载全数关闭");
|
|
4350
|
+
assert.ok(
|
|
4351
|
+
hostSource.includes("ctx.connection?.requestRejection?.(req)") && hostSource.includes("res.end('unauthorized')"),
|
|
4352
|
+
"自定义 webServer 路由经 connection.requestRejection 鉴权门(A1-08:不继承宿主鉴权,acks/busy 读写通道不得裸奔)",
|
|
4353
|
+
);
|
|
4354
|
+
assert.ok(
|
|
4355
|
+
bundle.includes("} else if (nextSeat !== seat) {") && bundle.includes("流式观察者换绑到新节点"),
|
|
4356
|
+
"宿主原地替换槽容器时流式观察者换绑到新节点,不留已分离旧节点(借鉴 -mig 并行实现)",
|
|
4357
|
+
);
|
|
3890
4358
|
|
|
3891
4359
|
|
|
3892
4360
|
// ---- E2E runner 浏览器生命周期契约(C-046、C-047,T-085)----
|