#!/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') }