@mzzsfy/dsh-context-manager 0.2.1 → 0.4.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/README.md +8 -6
- package/package.json +1 -1
- package/src/client.js +238 -26
- package/src/core.mjs +15 -3
- package/src/index.js +97 -39
- package/test/client-fork-enhance.test.mjs +70 -5
- package/test/client-help.test.mjs +3 -1
- package/test/client-history-button.test.mjs +158 -0
- package/test/client-history-scope.test.mjs +34 -0
- package/test/client-runtime.test.mjs +148 -0
- package/test/core.test.mjs +50 -0
- package/test/index.test.mjs +215 -8
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { runInThisContext } from 'node:vm'
|
|
6
|
+
|
|
7
|
+
// client.js 运行时冒烟:契约测试只比对源码字符串,查不出跨组件作用域错误
|
|
8
|
+
// (实测漏过一个 ReferenceError:重拉入口引用了他组件的 ref,浏览器控制台才报,
|
|
9
|
+
// 143 条契约测试全绿)。这里真正加载 client.js、跑通 factory 模块求值、并把每个
|
|
10
|
+
// 已注册的插槽组件渲染一遍,让作用域/初始化类错误在测试阶段暴露。
|
|
11
|
+
|
|
12
|
+
const CLIENT_SRC = readFileSync(fileURLToPath(new URL('../src/client.js', import.meta.url)), 'utf8')
|
|
13
|
+
|
|
14
|
+
// 组件函数体不渲染真实 DOM,只消费 hooks(useState/useEffect/useRef)与 createElement:
|
|
15
|
+
// 自造最小桩,零依赖全平台可跑(真 react 反而是环境耦合:CI 无宿主 profile)
|
|
16
|
+
const ReactStub = {
|
|
17
|
+
createElement: (type, props, ...children) => ({ type, props, children }),
|
|
18
|
+
useState: (init) => [typeof init === 'function' ? init() : init, () => {}],
|
|
19
|
+
useEffect: () => {},
|
|
20
|
+
useRef: (value) => ({ current: value }),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const HOST_HOOK = (name) => () => { throw new Error('缺少 react,无法执行 ' + name) }
|
|
24
|
+
|
|
25
|
+
// __ModuleLoader__ 桩:捕获 factory 与注册的插槽条目
|
|
26
|
+
function loadPlugin() {
|
|
27
|
+
assert.ok(ReactStub, '运行时冒烟需要宿主的 react;未找到时本测试无法建立保障')
|
|
28
|
+
const slots = []
|
|
29
|
+
let captured = null
|
|
30
|
+
// client.js 以 window/document/ MutationObserver 为宿主全局:桩全局后即在同上下文求值
|
|
31
|
+
const saved = {
|
|
32
|
+
window: globalThis.window,
|
|
33
|
+
document: globalThis.document,
|
|
34
|
+
MutationObserver: globalThis.MutationObserver,
|
|
35
|
+
}
|
|
36
|
+
// client.js 在 factory 求值期访问 window(工厂在宿主窗口内执行),故全局桩在
|
|
37
|
+
// factory 调用期间也必须就位,不能只在 runInThisContext 期间挂载
|
|
38
|
+
const stubWindow = {
|
|
39
|
+
__ModuleLoader__: {
|
|
40
|
+
load(definition) {
|
|
41
|
+
captured = definition
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
// 导航图标声明入口:吞噬,避免污染桩 window
|
|
45
|
+
__navicIcons: { register() {} },
|
|
46
|
+
}
|
|
47
|
+
globalThis.window = stubWindow
|
|
48
|
+
globalThis.document = {
|
|
49
|
+
createElement: () => ({
|
|
50
|
+
style: {},
|
|
51
|
+
setAttribute() {},
|
|
52
|
+
appendChild() {},
|
|
53
|
+
addEventListener() {},
|
|
54
|
+
querySelector: () => null,
|
|
55
|
+
querySelectorAll: () => [],
|
|
56
|
+
}),
|
|
57
|
+
querySelector: () => null,
|
|
58
|
+
querySelectorAll: () => [],
|
|
59
|
+
addEventListener() {},
|
|
60
|
+
documentElement: { appendChild() {} },
|
|
61
|
+
head: { appendChild() {} },
|
|
62
|
+
}
|
|
63
|
+
globalThis.MutationObserver = class {
|
|
64
|
+
observe() {}
|
|
65
|
+
disconnect() {}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
runInThisContext(CLIENT_SRC, { filename: 'client.js' })
|
|
69
|
+
assert.ok(captured, 'client.js 必须调用 window.__ModuleLoader__.load 注册插件')
|
|
70
|
+
const ctx = {
|
|
71
|
+
get: () => undefined,
|
|
72
|
+
slots: {
|
|
73
|
+
inject: (name, register) => { register() },
|
|
74
|
+
register: (definition, component) => { slots.push({ definition, component }); return () => {} },
|
|
75
|
+
},
|
|
76
|
+
remote: undefined,
|
|
77
|
+
on: () => () => {},
|
|
78
|
+
effect: () => {},
|
|
79
|
+
}
|
|
80
|
+
const plugin = captured.factory((specifier) => {
|
|
81
|
+
if (specifier === 'react') return ReactStub
|
|
82
|
+
throw new Error('模块表缺依赖:' + specifier)
|
|
83
|
+
})
|
|
84
|
+
assert.ok(typeof plugin === 'object' && plugin !== null, 'factory 应返回插件对象')
|
|
85
|
+
return { plugin, slots, ctx, id: captured.id }
|
|
86
|
+
} finally {
|
|
87
|
+
globalThis.window = saved.window
|
|
88
|
+
globalThis.document = saved.document
|
|
89
|
+
globalThis.MutationObserver = saved.MutationObserver
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
test('运行时:client.js 可加载并注册插件对象', () => {
|
|
94
|
+
const { plugin, id } = loadPlugin()
|
|
95
|
+
assert.equal(id, '@mzzsfy/dsh-context-manager')
|
|
96
|
+
assert.equal(typeof plugin.apply, 'function', '插件应有 apply')
|
|
97
|
+
assert.ok(Array.isArray(plugin.inject), '插件应声明 inject 依赖面')
|
|
98
|
+
assert.ok(plugin.inject.includes('slots'), '插槽面是全部 UI 能力的载体')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('运行时:apply 可执行且插槽全部注册成功(作用域错误在此暴露)', () => {
|
|
102
|
+
const { plugin, slots, ctx } = loadPlugin()
|
|
103
|
+
// 无远程服务面:走「干净禁用」路径,插槽注册仍应完成,不得抛错
|
|
104
|
+
plugin.apply(ctx)
|
|
105
|
+
assert.ok(slots.length > 0, 'apply 应至少注册一个插槽条目')
|
|
106
|
+
for (const { component } of slots) {
|
|
107
|
+
assert.equal(typeof component, 'function', '插槽条目必须是可渲染组件')
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('运行时:每个插槽组件均可求值(跨组件作用域错误在此暴露)', () => {
|
|
112
|
+
const { plugin, slots, ctx } = loadPlugin()
|
|
113
|
+
plugin.apply(ctx)
|
|
114
|
+
assert.ok(slots.length > 0)
|
|
115
|
+
for (const { definition, component } of slots) {
|
|
116
|
+
// 组件函数体在渲染期执行:跨组件变量引用错位会在此抛 ReferenceError
|
|
117
|
+
// (实测漏过的正是 ReferenceError;hooks 与宿主注入的 hook 面在组件体外调用
|
|
118
|
+
// 属宿主渲染器职责,本冒烟不模拟渲染器,故只对 ReferenceError 断言)
|
|
119
|
+
try {
|
|
120
|
+
component({ session: undefined, inputActions: undefined, useInput: undefined })
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof ReferenceError) throw error
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 宿主侧无远程服务面时各入口的注册是「干净禁用」路径:插槽注册不得因缺面而中断
|
|
128
|
+
test('运行时:缺 remote.session 时仍完成全部插槽注册', () => {
|
|
129
|
+
const { plugin, slots, ctx } = loadPlugin()
|
|
130
|
+
assert.equal(ctx.remote, undefined)
|
|
131
|
+
plugin.apply(ctx)
|
|
132
|
+
const names = slots.map(({ definition }) => definition.name)
|
|
133
|
+
assert.ok(names.includes('conversation.input.dock'), '输入框 dock 是 fork/历史/撤回三入口的载体')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// 双代插件卡:settings.plugin.item(0.1.2-0.1.5)与 plugins.bundle.config(0.1.7-rc.1+)
|
|
137
|
+
// 并存注册,各自宿主只消费其一;两代定义都必须在 apply 后注册在案
|
|
138
|
+
test('运行时:插件卡双代槽位并存注册且组件可渲染', () => {
|
|
139
|
+
const { plugin, slots, ctx } = loadPlugin()
|
|
140
|
+
plugin.apply(ctx)
|
|
141
|
+
const legacy = slots.find(({ definition }) => definition.name === 'settings.plugin.item')
|
|
142
|
+
const modern = slots.find(({ definition }) => definition.name === 'plugins.bundle.config')
|
|
143
|
+
assert.ok(legacy, '缺 settings.plugin.item 注册(0.1.2-0.1.5 设置卡)')
|
|
144
|
+
assert.equal(legacy.definition.key, 'context', '旧代 key 须配对 settings namespace')
|
|
145
|
+
assert.ok(modern, '缺 plugins.bundle.config 注册(0.1.7-rc.1+ 插件管理页卡)')
|
|
146
|
+
assert.equal(modern.definition.key, '@mzzsfy/dsh-context-manager', '新代 key 须为完整包名')
|
|
147
|
+
assert.equal(typeof modern.component, 'function')
|
|
148
|
+
})
|
package/test/core.test.mjs
CHANGED
|
@@ -59,6 +59,56 @@ test('历史输入提取:多 text block 按行拼接为单条', () => {
|
|
|
59
59
|
assert.deepEqual(extractUserInputs(events), [{ text: '第一段\n第二段', at: 300 }])
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
+
// ── 历史输入:斜杠命令提取(command/run)──
|
|
63
|
+
|
|
64
|
+
test('历史输入提取:command/run 重组为完整命令行,args 为命令名后 verbatim rawInput', () => {
|
|
65
|
+
const events = [{
|
|
66
|
+
type: 'command/run',
|
|
67
|
+
seq: 2,
|
|
68
|
+
time: 600,
|
|
69
|
+
data: { commandId: 'c1', name: 'goal', args: ' 完成目标文档', source: { kind: 'user' } },
|
|
70
|
+
}]
|
|
71
|
+
assert.deepEqual(extractUserInputs(events), [{ text: '/goal 完成目标文档', at: 600 }])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('历史输入提取:command/run 无 args(recordInput: false)产出裸命令名', () => {
|
|
75
|
+
const events = [{
|
|
76
|
+
type: 'command/run',
|
|
77
|
+
seq: 2,
|
|
78
|
+
time: 600,
|
|
79
|
+
data: { commandId: 'c1', name: 'think', source: { kind: 'user' } },
|
|
80
|
+
}]
|
|
81
|
+
assert.deepEqual(extractUserInputs(events), [{ text: '/think', at: 600 }])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('历史输入提取:非用户来源的 command/run 不产出', () => {
|
|
85
|
+
const events = [{
|
|
86
|
+
type: 'command/run',
|
|
87
|
+
seq: 2,
|
|
88
|
+
time: 600,
|
|
89
|
+
data: { commandId: 'c1', name: 'goal', args: ' x', source: { kind: 'agent' } },
|
|
90
|
+
}]
|
|
91
|
+
assert.deepEqual(extractUserInputs(events), [])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('历史输入提取:畸形 command/run(data 缺失/name 空串/args 非字符串)安全跳过或归一', () => {
|
|
95
|
+
const noData = { type: 'command/run', seq: 1, time: 100 }
|
|
96
|
+
const emptyName = { type: 'command/run', seq: 2, time: 200, data: { commandId: 'c1', name: '', source: { kind: 'user' } } }
|
|
97
|
+
const nonStringArgs = { type: 'command/run', seq: 3, time: 300, data: { commandId: 'c1', name: 'goal', args: 42, source: { kind: 'user' } } }
|
|
98
|
+
assert.deepEqual(extractUserInputs([noData, emptyName, nonStringArgs]), [{ text: '/goal', at: 300 }])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('历史输入提取:普通输入与命令混合按事件序全部产出', () => {
|
|
102
|
+
const events = [
|
|
103
|
+
userEvent(null, '先看看日志', 100),
|
|
104
|
+
{ type: 'command/run', seq: 2, time: 200, data: { commandId: 'c1', name: 'goal', args: ' 收尾', source: { kind: 'user' } } },
|
|
105
|
+
]
|
|
106
|
+
assert.deepEqual(extractUserInputs(events), [
|
|
107
|
+
{ text: '先看看日志', at: 100 },
|
|
108
|
+
{ text: '/goal 收尾', at: 200 },
|
|
109
|
+
])
|
|
110
|
+
})
|
|
111
|
+
|
|
62
112
|
// ── 历史输入:聚合(G4-G5)──
|
|
63
113
|
|
|
64
114
|
test('历史输入聚合:同文本去重保留最新时间,按时间倒序', () => {
|
package/test/index.test.mjs
CHANGED
|
@@ -54,11 +54,16 @@ function makeCtx({
|
|
|
54
54
|
settingsValue,
|
|
55
55
|
settingsGetError,
|
|
56
56
|
readSessions,
|
|
57
|
+
configRef,
|
|
58
|
+
configEditor,
|
|
59
|
+
settingsService: settingsServiceOverride,
|
|
57
60
|
}) {
|
|
58
61
|
const routes = []
|
|
59
62
|
const pendingInjects = []
|
|
60
63
|
const settingsState = { value: settingsValue }
|
|
61
|
-
|
|
64
|
+
// settingsServiceOverride(0.1.7 形态桩,如仅 update 的 forms 面)优先;
|
|
65
|
+
// 缺省时构造 legacy 方法面桩(get/register/update 命名空间语义)
|
|
66
|
+
const settingsService = settingsServiceOverride ?? {
|
|
62
67
|
// settingsGetError 注入读取期故障:验证 respondError 的系统级错误收敛分支
|
|
63
68
|
get: () => {
|
|
64
69
|
if (settingsGetError) throw settingsGetError
|
|
@@ -95,7 +100,12 @@ function makeCtx({
|
|
|
95
100
|
if (typeof disposer === 'function') effectDisposers.push(disposer)
|
|
96
101
|
},
|
|
97
102
|
inject: (_deps, fn) => { pendingInjects.push(fn) },
|
|
98
|
-
|
|
103
|
+
// fiber 桩:0.1.7 configure 装配以本条目 fiber 为 owner
|
|
104
|
+
fiber: { entry: { options: { id: 'context-manager', name: '@mzzsfy/dsh-context-manager' } } },
|
|
105
|
+
get: (name) => {
|
|
106
|
+
if (name === 'configEditor') return configEditor
|
|
107
|
+
return { agents, sessionPersistence, settings: settingsService }[name]
|
|
108
|
+
},
|
|
99
109
|
logger: { warns: [], warn(message) { this.warns.push(message) }, infos: [], info(message) { this.infos.push(message) } },
|
|
100
110
|
}
|
|
101
111
|
const ctx = new Proxy(base, {
|
|
@@ -108,14 +118,23 @@ function makeCtx({
|
|
|
108
118
|
return services[prop]
|
|
109
119
|
},
|
|
110
120
|
})
|
|
111
|
-
apply(ctx,
|
|
121
|
+
apply(ctx, configRef)
|
|
112
122
|
return {
|
|
113
123
|
handlers: new Map(routes.map((route) => [route.path, route.handler])),
|
|
114
124
|
logger: base.logger,
|
|
115
125
|
readCounts,
|
|
116
126
|
// 模拟宿主 settings 服务激活:触发 inject 回调(注册)
|
|
117
127
|
activateSettings: () => {
|
|
118
|
-
while (pendingInjects.length > 0)
|
|
128
|
+
while (pendingInjects.length > 0) {
|
|
129
|
+
pendingInjects.shift()({
|
|
130
|
+
settings: settingsService,
|
|
131
|
+
// 注入上下文的 effect 桩:0.1.7 分支经它注册 configure 装配
|
|
132
|
+
effect(fn) {
|
|
133
|
+
const disposer = fn()
|
|
134
|
+
if (typeof disposer === 'function') effectDisposers.push(disposer)
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
}
|
|
119
138
|
},
|
|
120
139
|
}
|
|
121
140
|
}
|
|
@@ -482,6 +501,84 @@ test('历史输入路由:产物未变的会话跨对齐零解压(磁盘 extracts
|
|
|
482
501
|
})
|
|
483
502
|
})
|
|
484
503
|
|
|
504
|
+
test('历史输入路由:旧版本提取缓存自动重解压(命令历史自愈)', skipMissingDeps, async () => {
|
|
505
|
+
await withHistoryCacheDir(async (cacheDir) => {
|
|
506
|
+
const dir = await mkdtemp(path.join(tmpdir(), 'cx-hist-ver-'))
|
|
507
|
+
const artifactPath = path.join(dir, 'session.jsonl.zstd')
|
|
508
|
+
await writeFile(artifactPath, 'log-bytes')
|
|
509
|
+
try {
|
|
510
|
+
const { stat } = await import('node:fs/promises')
|
|
511
|
+
const info = await stat(artifactPath)
|
|
512
|
+
await ensureCacheDir(cacheDir)
|
|
513
|
+
// 预置旧版本提取器写下的缓存:指纹与产物一致但条目缺命令输入(无 v 字段)
|
|
514
|
+
await writeWorkspaceCache(cacheDir, 'C:\\x', {
|
|
515
|
+
entries: [{ text: '旧输入', at: 100, sid: 's1' }],
|
|
516
|
+
extracts: {
|
|
517
|
+
s1: { mtimeMs: info.mtimeMs, size: info.size, entries: [{ text: '旧输入', at: 100 }] },
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
const commandEvent = {
|
|
521
|
+
type: 'command/run',
|
|
522
|
+
seq: 2,
|
|
523
|
+
time: 200,
|
|
524
|
+
data: { commandId: 'c1', name: 'goal', args: ' 收尾', source: { kind: 'user' } },
|
|
525
|
+
}
|
|
526
|
+
const { handlers, readCounts } = makeCtx({
|
|
527
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
528
|
+
agents: new Map(),
|
|
529
|
+
sessionPersistence: { locate: (header) => ({ path: artifactPath }) },
|
|
530
|
+
readSessions: { s1: [userMessageEvent('旧输入', 100), commandEvent] },
|
|
531
|
+
})
|
|
532
|
+
const res = await requestUntil(handlers, '?sessionId=s1&scope=workspace',
|
|
533
|
+
(body) => body.aligned && body.inputs.some((item) => item.text === '/goal 收尾'))
|
|
534
|
+
assert.ok(readCounts.get('s1') >= 1, '版本不符触发重解压')
|
|
535
|
+
assert.deepEqual(res.body.inputs.map((item) => item.text), ['/goal 收尾', '旧输入'])
|
|
536
|
+
} finally {
|
|
537
|
+
await rm(dir, { recursive: true, force: true })
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('历史输入路由:session 范围版本不符判 stale,聚焦对齐首请求内补齐', skipMissingDeps, async () => {
|
|
543
|
+
await withHistoryCacheDir(async (cacheDir) => {
|
|
544
|
+
const dir = await mkdtemp(path.join(tmpdir(), 'cx-hist-ver2-'))
|
|
545
|
+
const artifactPath = path.join(dir, 'session.jsonl.zstd')
|
|
546
|
+
await writeFile(artifactPath, 'log-bytes')
|
|
547
|
+
try {
|
|
548
|
+
const { stat } = await import('node:fs/promises')
|
|
549
|
+
const info = await stat(artifactPath)
|
|
550
|
+
await ensureCacheDir(cacheDir)
|
|
551
|
+
// 预置旧版本缓存:指纹与产物一致(不改条目则指纹校验判新鲜)但缺 v 字段
|
|
552
|
+
await writeWorkspaceCache(cacheDir, 'C:\\x', {
|
|
553
|
+
entries: [{ text: '旧输入', at: 100, sid: 's1' }],
|
|
554
|
+
extracts: {
|
|
555
|
+
s1: { mtimeMs: info.mtimeMs, size: info.size, entries: [{ text: '旧输入', at: 100 }] },
|
|
556
|
+
},
|
|
557
|
+
})
|
|
558
|
+
const commandEvent = {
|
|
559
|
+
type: 'command/run',
|
|
560
|
+
seq: 2,
|
|
561
|
+
time: 200,
|
|
562
|
+
data: { commandId: 'c1', name: 'goal', args: ' 收尾', source: { kind: 'user' } },
|
|
563
|
+
}
|
|
564
|
+
const { handlers, readCounts } = makeCtx({
|
|
565
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
566
|
+
agents: new Map(),
|
|
567
|
+
sessionPersistence: { locate: (header) => ({ path: artifactPath }) },
|
|
568
|
+
readSessions: { s1: [userMessageEvent('旧输入', 100), commandEvent] },
|
|
569
|
+
})
|
|
570
|
+
// 首请求即 aligned:true 且含命令条目——版本不符走聚焦对齐,单次请求内补齐
|
|
571
|
+
const res = await requestUntil(handlers, '?sessionId=s1&scope=session',
|
|
572
|
+
(body) => body.aligned && body.inputs.some((item) => item.text === '/goal 收尾'))
|
|
573
|
+
assert.equal(res.body.aligned, true, '聚焦对齐快于等待时限,首请求即就绪')
|
|
574
|
+
assert.deepEqual(res.body.inputs.map((item) => item.text), ['/goal 收尾', '旧输入'])
|
|
575
|
+
assert.ok(readCounts.get('s1') >= 1, '版本不符触发聚焦重解压')
|
|
576
|
+
} finally {
|
|
577
|
+
await rm(dir, { recursive: true, force: true })
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
485
582
|
// ── 历史输入路由:窗口与 subagent 排除 ──
|
|
486
583
|
|
|
487
584
|
test('历史输入路由:启动对齐只回溯最近 STARTUP_SCAN 个会话,老会话不解压', skipMissingDeps, async () => {
|
|
@@ -661,12 +758,12 @@ test('常用提示词:超长截断、上限裁剪、空 text 拒绝', skipMissin
|
|
|
661
758
|
|
|
662
759
|
// ── 路由层横切守卫:方法白名单 / JSON 体 / 系统级错误收敛 / 非字符串 text ──
|
|
663
760
|
|
|
664
|
-
test('横切守卫:
|
|
761
|
+
test('横切守卫:8 条路由方法白名单,白名单外一律 405', skipMissingDeps, async () => {
|
|
665
762
|
await withHistoryCacheDir(async () => {
|
|
666
763
|
const { handlers } = makeCtx({ headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }], agents: new Map() })
|
|
667
764
|
const getOnly = '/api/context/inputs'
|
|
668
765
|
const postOnly = '/api/context/prompts/toggle'
|
|
669
|
-
const dualMethods = ['/api/context/history-enabled', '/api/context/steer-recall-enabled', '/api/context/fork-enabled', '/api/context/copy-sid-enabled']
|
|
766
|
+
const dualMethods = ['/api/context/history-enabled', '/api/context/history-button-enabled', '/api/context/steer-recall-enabled', '/api/context/fork-enabled', '/api/context/fork-auto-resend-enabled', '/api/context/copy-sid-enabled']
|
|
670
767
|
// GET-only 路由:POST 拒绝
|
|
671
768
|
const inputsPost = response()
|
|
672
769
|
await handlers.get(getOnly)(request('s1'), inputsPost)
|
|
@@ -684,10 +781,10 @@ test('横切守卫:6 条路由方法白名单,白名单外一律 405', skipMissi
|
|
|
684
781
|
})
|
|
685
782
|
})
|
|
686
783
|
|
|
687
|
-
test('横切守卫:
|
|
784
|
+
test('横切守卫:7 条 POST 路由的非法 JSON 请求体统一 400(badJsonBody)', skipMissingDeps, async () => {
|
|
688
785
|
await withHistoryCacheDir(async () => {
|
|
689
786
|
const { handlers } = makeCtx({ headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }], agents: new Map() })
|
|
690
|
-
const postPaths = ['/api/context/prompts/toggle', '/api/context/history-enabled', '/api/context/steer-recall-enabled', '/api/context/fork-enabled', '/api/context/copy-sid-enabled']
|
|
787
|
+
const postPaths = ['/api/context/prompts/toggle', '/api/context/history-enabled', '/api/context/history-button-enabled', '/api/context/steer-recall-enabled', '/api/context/fork-enabled', '/api/context/fork-auto-resend-enabled', '/api/context/copy-sid-enabled']
|
|
691
788
|
for (const routePath of postPaths) {
|
|
692
789
|
const res = await postRaw(handlers, routePath, '{oops')
|
|
693
790
|
assert.equal(res.status, 400, routePath + ' 非法 JSON 应回 400')
|
|
@@ -804,3 +901,113 @@ test('标题栏复制 sessionId 启停:GET 默认启用,POST 切换经 settings
|
|
|
804
901
|
await handlers.get('/api/context/copy-sid-enabled')(getRequest2('/api/context/copy-sid-enabled', 'GET'), restored)
|
|
805
902
|
assert.equal(restored.body.enabled, true)
|
|
806
903
|
})
|
|
904
|
+
|
|
905
|
+
test('输入框历史按钮启停:GET 默认启用,POST 切换经 settings 持久,GET 反映新值', skipMissingDeps, async () => {
|
|
906
|
+
const { handlers } = makeCtx({ headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }], agents: new Map() })
|
|
907
|
+
const initial = response()
|
|
908
|
+
await handlers.get('/api/context/history-button-enabled')(getRequest2('/api/context/history-button-enabled', 'GET'), initial)
|
|
909
|
+
assert.equal(initial.body.enabled, true, '默认启用')
|
|
910
|
+
const off = await postJson(handlers, '/api/context/history-button-enabled', { enabled: false })
|
|
911
|
+
assert.equal(off.body.enabled, false)
|
|
912
|
+
// settings 持久:重读反映关闭态;POST 回 true 恢复
|
|
913
|
+
const reread = response()
|
|
914
|
+
await handlers.get('/api/context/history-button-enabled')(getRequest2('/api/context/history-button-enabled', 'GET'), reread)
|
|
915
|
+
assert.equal(reread.body.enabled, false, 'settings 持久化关闭态')
|
|
916
|
+
await postJson(handlers, '/api/context/history-button-enabled', { enabled: true })
|
|
917
|
+
const restored = response()
|
|
918
|
+
await handlers.get('/api/context/history-button-enabled')(getRequest2('/api/context/history-button-enabled', 'GET'), restored)
|
|
919
|
+
assert.equal(restored.body.enabled, true)
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
// --- 0.1.7 settings 面双形适配(maintain a913c2c 同构) ---
|
|
923
|
+
|
|
924
|
+
test('Config 导出契约:根级 volatile 包装,validate 产整节单 ref,默认值对拍', () => {
|
|
925
|
+
// Given 静态 Config 导出(0.1.7 节表单事实源)
|
|
926
|
+
assert.ok(indexModule.Config, 'Config 导出必须在场')
|
|
927
|
+
// When 空配置校验
|
|
928
|
+
const resolved = indexModule.Config['~standard'].validate({})
|
|
929
|
+
// Then 产整节单 ref(get 协议),布尔开关默认全开(forkAutoResend 除外)
|
|
930
|
+
assert.equal(resolved.issues, undefined)
|
|
931
|
+
const section = resolved.value.get()
|
|
932
|
+
assert.equal(section.historyEnabled, true)
|
|
933
|
+
assert.equal(section.forkAutoResendEnabled, false)
|
|
934
|
+
assert.equal(section.copySidEnabled, true)
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
test('0.1.7 形态:settings 无 get/register 时读回落 config ref 节值', skipMissingDeps, async () => {
|
|
938
|
+
// Given settingsForms 只有 update(0.1.7 真实方法面)+ config ref 携带用户值
|
|
939
|
+
const settingsForms = { update: async () => {} }
|
|
940
|
+
const configRef = { get: () => ({ historyEnabled: false, copySidEnabled: false }) }
|
|
941
|
+
const { handlers } = makeCtx({
|
|
942
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
943
|
+
agents: new Map(),
|
|
944
|
+
settingsService: settingsForms,
|
|
945
|
+
configRef,
|
|
946
|
+
})
|
|
947
|
+
// When GET 开关路由
|
|
948
|
+
const res = response()
|
|
949
|
+
await handlers.get('/api/context/history-enabled')(getRequest2('/api/context/history-enabled', 'GET'), res)
|
|
950
|
+
// Then 返回 config 节值而非默认
|
|
951
|
+
assert.equal(res.body.enabled, false)
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
test('0.1.7 形态:开关 POST 经 configEditor.edit 落 fiber.entry 并回显新值', skipMissingDeps, async () => {
|
|
955
|
+
// Given settingsForms 只有 update + configEditor 桩(合并写回 configStore)
|
|
956
|
+
const settingsForms = { update: async () => {} }
|
|
957
|
+
const configStore = {}
|
|
958
|
+
const editCalls = []
|
|
959
|
+
const configRef = { get: () => ({ ...configStore }) }
|
|
960
|
+
const { handlers } = makeCtx({
|
|
961
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
962
|
+
agents: new Map(),
|
|
963
|
+
settingsService: settingsForms,
|
|
964
|
+
configRef,
|
|
965
|
+
configEditor: {
|
|
966
|
+
async edit(entry, change) {
|
|
967
|
+
editCalls.push(entry)
|
|
968
|
+
const next = change({ ...configStore }, {})
|
|
969
|
+
for (const [key, value] of Object.entries(next)) configStore[key] = value
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
})
|
|
973
|
+
// When POST 关闭历史浮层
|
|
974
|
+
const off = await postJson(handlers, '/api/context/history-enabled', { enabled: false })
|
|
975
|
+
// Then edit 定位本条目,合并写回,回显新值
|
|
976
|
+
assert.equal(off.body.enabled, false)
|
|
977
|
+
assert.equal(editCalls.length, 1)
|
|
978
|
+
assert.equal(editCalls[0].options.id, 'context-manager')
|
|
979
|
+
assert.equal(configStore.historyEnabled, false)
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
test('0.1.7 形态:configEditor 缺失即 400 拒写,配置不变', skipMissingDeps, async () => {
|
|
983
|
+
// Given settingsForms 只有 update 且无 configEditor
|
|
984
|
+
const settingsForms = { update: async () => {} }
|
|
985
|
+
const configRef = { get: () => ({}) }
|
|
986
|
+
const { handlers } = makeCtx({
|
|
987
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
988
|
+
agents: new Map(),
|
|
989
|
+
settingsService: settingsForms,
|
|
990
|
+
configRef,
|
|
991
|
+
})
|
|
992
|
+
// When POST 写开关
|
|
993
|
+
const denied = await postJson(handlers, '/api/context/history-enabled', { enabled: false })
|
|
994
|
+
// Then 400 错误响应
|
|
995
|
+
assert.equal(denied.status, 400)
|
|
996
|
+
assert.ok(denied.body.error)
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
test('0.1.7 形态:settings.configure 在场即关闭原生自动页,register 缺失不抛错', skipMissingDeps, () => {
|
|
1000
|
+
// Given settingsForms 带 configure 无 register
|
|
1001
|
+
const configureCalls = []
|
|
1002
|
+
const settingsForms = { configure: (presentation, owner) => { configureCalls.push({ presentation, owner }); return () => {} } }
|
|
1003
|
+
const { activateSettings } = makeCtx({
|
|
1004
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
1005
|
+
agents: new Map(),
|
|
1006
|
+
settingsService: settingsForms,
|
|
1007
|
+
})
|
|
1008
|
+
// When 激活 settings 注入
|
|
1009
|
+
activateSettings()
|
|
1010
|
+
// Then 关闭自动页且 owner 为本插件 fiber
|
|
1011
|
+
assert.equal(configureCalls.length, 1)
|
|
1012
|
+
assert.deepEqual(configureCalls[0].presentation, { auto: false })
|
|
1013
|
+
})
|