Files
DevFlow/scripts/verify-streaming-guard.mjs
绝尘 bd6a41fe6e 新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端:
- 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展
- 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通
- 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦
- AI planner/plan_hint/intent:aichat B 路线并行多轮基础
- patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环
- 压缩超时兜底(F-15 卡死根治)
- F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本
- 知识注入 DRY/skills/audit 扩展

清理:
- aichat 技术债(误报 allow/死导入/过时注释 30 项)
- URGENT.md 删除(11 项加急全解决/迁 todo)
- 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
2026-06-21 20:51:26 +08:00

175 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
/**
* streamingGuard 单测(对齐 memory [[devflow-generating-statemachine]] 前端落地)。
*
* 背景:前端无 vitest(引入框架超本批白名单),故用零依赖 Node 内置 assert 跑断言。
* 本脚本内联 streamingGuard.ts 的联动规则副本,验证:
* 1. 合法跃迁(false→true / true→false)正确改 streaming + 联动 generatingConvs
* 2. 幂等写(true→true / false→false)不拒绝(放行,记日志)
* 3. 联动规则:value=true && convId → add(convId);value=false && convId → delete(convId);
* 无 convId 不动 generatingConvs(全局兜底场景,避免误删并发会话)
* 4. feature flag 关 → guard 退化为直接赋值(不联动 generatingConvs)
* 5. forceResetStreaming(panic 兜底):复位 streaming + 清 currentText + 清 queue
*
* 同步约定:若 streamingGuard.ts 的联动规则变更,本脚本副本须同步更新(脚本顶部
* 注释已标注对应源码行)。
*
* 运行:node scripts/verify-streaming-guard.mjs
*/
import assert from 'node:assert/strict'
let passed = 0
function test(name, fn) {
try {
fn()
passed++
console.log(`${name}`)
} catch (e) {
console.error(`${name}`)
console.error(` ${e.message}`)
process.exitCode = 1
}
}
// ── mock state + appSettings(对齐 stores/ai.ts state 形态) ──
function makeState() {
return {
streaming: false,
currentText: '',
queue: [],
generatingConvs: new Set(),
}
}
/**
* setStreaming 联动逻辑副本(同步自 src/composables/ai/streamingGuard.ts setStreaming)。
* 与源码逐行对齐:flag 开 → 幂等观测 + 联动 generatingConvs + 赋值;flag 关 → 直接赋值。
*/
function setStreaming(state, value, opts = {}, guardEnabled = true) {
const { convId } = opts
if (!guardEnabled) {
state.streaming = value
return
}
// 联动 generatingConvs(源码 streamingGuard.ts:80-87)
if (convId) {
if (value) {
state.generatingConvs.add(convId)
} else {
state.generatingConvs.delete(convId)
}
}
state.streaming = value
}
/** forceResetStreaming 副本(源码 streamingGuard.ts forceResetStreaming)。
* TD-260621-01 per-conv:convId 可选。传 convId → 仅清该 conv 的 generatingConvs(per-conv 超时场景);
* 省略 → 全局兜底,generatingConvs 不动(由 AiCompleted/AiError 精确管理)。 */
function forceResetStreaming(state, _reason, convId) {
setStreaming(state, false, convId ? { convId } : {})
state.currentText = ''
state.queue = []
}
console.log('streamingGuard 单测:')
// ── 1. 合法跃迁 ──
test('false→true: streaming 置 true + convId 入 Set', () => {
const s = makeState()
setStreaming(s, true, { convId: 'conv-1' })
assert.equal(s.streaming, true)
assert.ok(s.generatingConvs.has('conv-1'), 'conv-1 应在 generatingConvs 中')
})
test('true→false: streaming 置 false + convId 出 Set', () => {
const s = makeState()
s.generatingConvs.add('conv-1')
setStreaming(s, true, { convId: 'conv-1' })
setStreaming(s, false, { convId: 'conv-1' })
assert.equal(s.streaming, false)
assert.ok(!s.generatingConvs.has('conv-1'), 'conv-1 应已移除')
})
// ── 2. 幂等写不拒绝 ──
test('true→true 幂等: 不拒绝,convId add 幂等无副作用', () => {
const s = makeState()
setStreaming(s, true, { convId: 'conv-1' })
setStreaming(s, true, { convId: 'conv-1' }) // 幂等
assert.equal(s.streaming, true)
assert.equal(s.generatingConvs.size, 1, 'Set 不应重复添加')
})
test('false→false 幂等: 不拒绝', () => {
const s = makeState()
setStreaming(s, false, {})
assert.equal(s.streaming, false)
})
// ── 3. 联动规则 ──
test('无 convId 的 true: 不动 generatingConvs', () => {
const s = makeState()
setStreaming(s, true, {}) // 无 convId
assert.equal(s.streaming, true)
assert.equal(s.generatingConvs.size, 0, '无 convId 不应 add')
})
test('无 convId 的 false(全局兜底): 不动 generatingConvs(避免误删并发会话)', () => {
const s = makeState()
s.generatingConvs.add('conv-A')
s.generatingConvs.add('conv-B')
setStreaming(s, false, {}) // 全局兜底,无 convId
assert.equal(s.streaming, false)
assert.equal(s.generatingConvs.size, 2, '并发会话生成态不应被全局复位误删')
})
test('convId 为空串: 视为无 convId,不动 generatingConvs', () => {
const s = makeState()
setStreaming(s, true, { convId: '' })
assert.equal(s.streaming, true)
assert.equal(s.generatingConvs.size, 0)
})
// ── 4. feature flag 关 ──
test('flag 关: guard 退化为直接赋值,不联动 generatingConvs', () => {
const s = makeState()
setStreaming(s, true, { convId: 'conv-1' }, /* guardEnabled */ false)
assert.equal(s.streaming, true)
assert.equal(s.generatingConvs.size, 0, 'flag 关不联动 Set')
})
// ── 5. forceResetStreaming(panic 兜底) ──
test('forceResetStreaming: 复位 streaming + 清 currentText + 清 queue', () => {
const s = makeState()
s.streaming = true
s.currentText = '流式中途文字'
s.queue = [{ text: '排队消息' }]
s.generatingConvs.add('conv-1')
forceResetStreaming(s, 'onStreamTimeout(130s)')
assert.equal(s.streaming, false, 'streaming 应复位 false')
assert.equal(s.currentText, '', 'currentText 应清空')
assert.equal(s.queue.length, 0, 'queue 应清空')
// 注:forceResetStreaming 不传 convId,generatingConvs 不动(由 AiCompleted/AiError 精确管理)
assert.ok(s.generatingConvs.has('conv-1'), '兜底复位不应误删并发会话生成态')
})
// ── 6. TD-260621-01 forceResetStreaming per-conv ──
test('TD-260621-01 forceResetStreaming(convId): 仅清该 conv 的 generatingConvs,不误杀并发会话', () => {
const s = makeState()
s.streaming = true
s.generatingConvs.add('conv-A')
s.generatingConvs.add('conv-B')
// A 超时,携 convId='conv-A' → 仅 delete conv-A,B 保留
forceResetStreaming(s, 'onStreamTimeout(130s 无数据)', 'conv-A')
assert.equal(s.streaming, false, 'streaming 应复位 false')
assert.ok(!s.generatingConvs.has('conv-A'), 'conv-A(超时会话)应被清')
assert.ok(s.generatingConvs.has('conv-B'), 'conv-B(并发正常会话)不应被误杀')
})
console.log(`\n${passed} passed`)
if (process.exitCode === 1) {
console.error('FAILED')
process.exit(1)
} else {
console.log('ALL PASSED')
}