@mzzsfy/dsh-session-manager 0.9.2 → 0.9.3
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 +1 -1
- package/package.json +1 -1
- package/src/index.js +46 -10
- package/test/index.test.mjs +157 -4
package/README.md
CHANGED
|
@@ -89,4 +89,4 @@ node scripts/dev-link.mjs dsh-session-manager # 仓库根执行:归一 profile
|
|
|
89
89
|
|
|
90
90
|
## dsh 版本兼容
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
0.1.2-rc.1 / 0.1.5-rc.3 通过:归档入口、激活 live、diagnostics 0 findings。0.1.7-rc.1 上宿主服务拓扑变化(workspaceRegistry 激活链)导致本包七项 inject 未满足、HTTP 面 404,功能待宿主 rc 线定型后专项对齐(详见包内 兼容性测试.md 与仓库兼容性进度文档 FIND-RC1-1)。
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -98,6 +98,21 @@ const SETTINGS_SCHEMA = schemastery.object({
|
|
|
98
98
|
.description('周期评估间隔小时数,0 表示关闭周期评估;变更经周期 tick 对账,关闭即时暂停、重启用最迟下个 tick 生效'),
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
+
// 0.1.7 宿主以静态 Config 导出生成设置节表单(maintain 同构);legacy 宿主经
|
|
102
|
+
// settings.register 注册同名命名空间。根级 volatile 包装:0.1.7 下整节为 live
|
|
103
|
+
// 表单,节写经 loader 原地热更;旧宿主 schemastery 无 volatile 方法,特性检测原样返回
|
|
104
|
+
export const Config = volatileWrap(SETTINGS_SCHEMA)
|
|
105
|
+
|
|
106
|
+
function volatileWrap(schema) {
|
|
107
|
+
return typeof schema.volatile === 'function' ? schema.volatile() : schema
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// volatile ref 动态解包(get 协议):0.1.7 下 apply 入参 config 为整节单 ref;
|
|
111
|
+
// legacy 宿主 config 为普通对象原样透传
|
|
112
|
+
function unwrapVolatile(value) {
|
|
113
|
+
return typeof value?.get === 'function' ? value.get() : value
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
function sendJson(res, status, payload) {
|
|
102
117
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
103
118
|
res.end(JSON.stringify(payload))
|
|
@@ -114,9 +129,12 @@ function respondError(ctx, res, error) {
|
|
|
114
129
|
sendJson(res, 400, { error: message })
|
|
115
130
|
}
|
|
116
131
|
|
|
117
|
-
function readSettings(ctx) {
|
|
132
|
+
function readSettings(ctx, config) {
|
|
118
133
|
const settings = ctx.get('settings')
|
|
119
|
-
|
|
134
|
+
// 双形读(maintain 同构):legacy settings 命名空间 get;0.1.7+ apply 入参
|
|
135
|
+
// config 整节解包。方法面守卫兼防:服务在但缺 get(宿主升级变更面)时回落默认值,
|
|
136
|
+
// 防 timer 回调抛错崩进程
|
|
137
|
+
const value = settings && typeof settings.get === 'function' ? settings.get(NAMESPACE) : unwrapVolatile(config)
|
|
120
138
|
const days = Number(value && value.autoArchiveDays)
|
|
121
139
|
const hours = Number(value && value.autoArchiveIntervalHours)
|
|
122
140
|
return {
|
|
@@ -228,7 +246,7 @@ export function apply(ctx, config) {
|
|
|
228
246
|
// 活跃会话零 IO。返回是否真正启动:被门闩/关闭挡下的调用方(周期 tick)保持到期态
|
|
229
247
|
// 由下个 tick 重试,不得据此重排周期
|
|
230
248
|
function evaluateArchives(cwd) {
|
|
231
|
-
const { days } = readSettings(ctx)
|
|
249
|
+
const { days } = readSettings(ctx, config)
|
|
232
250
|
if (days === 0 || evaluating) return false
|
|
233
251
|
evaluating = true
|
|
234
252
|
let timeoutGuard
|
|
@@ -624,7 +642,7 @@ export function apply(ctx, config) {
|
|
|
624
642
|
handler: async (req, res) => {
|
|
625
643
|
try {
|
|
626
644
|
if (req.method === 'GET') {
|
|
627
|
-
const current = readSettings(ctx)
|
|
645
|
+
const current = readSettings(ctx, config)
|
|
628
646
|
sendJson(res, 200, { days: current.days, intervalHours: current.intervalHours })
|
|
629
647
|
return
|
|
630
648
|
}
|
|
@@ -636,11 +654,20 @@ export function apply(ctx, config) {
|
|
|
636
654
|
throw new Error(MESSAGES.badJsonBody)
|
|
637
655
|
}
|
|
638
656
|
const patch = readAutoArchivePatch(body)
|
|
657
|
+
// 双形写(maintain 同构,判定延迟到使用点):legacy 方法面以 register 为准
|
|
658
|
+
// (0.1.7 的 settings.update 名义在场但按命名空间找条目必败,不可作判据);
|
|
659
|
+
// 0.1.7+ 走 configEditor.edit 落 profile 条目(fiber.entry,与命名空间解耦)。
|
|
660
|
+
// await 保证回显为新值,persist 失败走 respondError
|
|
639
661
|
const settings = ctx.get('settings')
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
662
|
+
if (typeof settings?.register === 'function' && typeof settings?.update === 'function') {
|
|
663
|
+
await settings.update(NAMESPACE, patch)
|
|
664
|
+
} else {
|
|
665
|
+
const editor = ctx.get('configEditor')
|
|
666
|
+
const entry = ctx.fiber?.entry
|
|
667
|
+
if (!editor || typeof editor.edit !== 'function' || !entry) throw new Error('宿主设置服务不可用')
|
|
668
|
+
await editor.edit(entry, (current) => ({ ...current, ...patch }))
|
|
669
|
+
}
|
|
670
|
+
const current = readSettings(ctx, config)
|
|
644
671
|
sendJson(res, 200, { days: current.days, intervalHours: current.intervalHours })
|
|
645
672
|
} catch (error) {
|
|
646
673
|
respondError(ctx, res, error)
|
|
@@ -735,13 +762,13 @@ export function apply(ctx, config) {
|
|
|
735
762
|
let nextDueAt = null
|
|
736
763
|
let lastArmedAt = 0
|
|
737
764
|
const scheduleNext = () => {
|
|
738
|
-
const { intervalHours } = readSettings(ctx)
|
|
765
|
+
const { intervalHours } = readSettings(ctx, config)
|
|
739
766
|
lastArmedAt = Date.now()
|
|
740
767
|
nextDueAt = intervalHours > 0 ? lastArmedAt + intervalHours * HOUR_MS : null
|
|
741
768
|
}
|
|
742
769
|
try {
|
|
743
770
|
const dispose = timerCtx.interval(() => {
|
|
744
|
-
const { intervalHours } = readSettings(ctx)
|
|
771
|
+
const { intervalHours } = readSettings(ctx, config)
|
|
745
772
|
// 设置变更对账:0 即时暂停;缺失 due(0 重启用)时补排,防周期轮静默死亡;
|
|
746
773
|
// 缩短间隔时以 lastArmedAt 锚定前移,最长等新间隔而非等满旧周期
|
|
747
774
|
if (intervalHours === 0) {
|
|
@@ -769,6 +796,15 @@ export function apply(ctx, config) {
|
|
|
769
796
|
})
|
|
770
797
|
|
|
771
798
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
799
|
+
// 方法面守卫:settings 服务在但缺 register(宿主升级变更面)时跳过注册,
|
|
800
|
+
// 周期轮与启动补扫按 readSettings 的默认值降级运行,不因服务形变炸 fiber;
|
|
801
|
+
// 0.1.7 关闭原生自动设置页,设置面由本包自带分区承担(maintain 同构)
|
|
802
|
+
if (typeof settingsCtx.settings.register !== 'function') {
|
|
803
|
+
if (typeof settingsCtx.settings?.configure === 'function') {
|
|
804
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
|
|
805
|
+
}
|
|
806
|
+
return
|
|
807
|
+
}
|
|
772
808
|
settingsCtx.settings.register(NAMESPACE, SETTINGS_SCHEMA, { base: config })
|
|
773
809
|
// 启动补扫不依赖定时服务:settings 就绪即评估一轮,清掉停机期间积压的超期会话
|
|
774
810
|
evaluateArchives()
|
package/test/index.test.mjs
CHANGED
|
@@ -118,16 +118,20 @@ function makeCtx({
|
|
|
118
118
|
openRejected,
|
|
119
119
|
workspaces,
|
|
120
120
|
settingsValue,
|
|
121
|
+
settingsService: settingsServiceOverride,
|
|
121
122
|
timerAvailable,
|
|
122
123
|
readSessions,
|
|
124
|
+
configRef,
|
|
125
|
+
configEditor,
|
|
123
126
|
}) {
|
|
124
127
|
const routes = []
|
|
125
128
|
const eventHandlers = {}
|
|
126
129
|
const pendingInjects = []
|
|
130
|
+
const disposers = []
|
|
127
131
|
const ledgerDomain = ledger === undefined ? makeLedgerDomain([]) : ledger
|
|
128
132
|
const workspaceList = workspaces || []
|
|
129
133
|
const settingsState = { value: settingsValue }
|
|
130
|
-
const settingsService = {
|
|
134
|
+
const settingsService = settingsServiceOverride ?? {
|
|
131
135
|
get: () => settingsState.value,
|
|
132
136
|
register: () => ({ resolved: undefined }),
|
|
133
137
|
// 宿主 update 为异步串行:合并进微任务队列后生效,读旧值发生在 flush 前
|
|
@@ -173,7 +177,12 @@ function makeCtx({
|
|
|
173
177
|
const base = {
|
|
174
178
|
effect: (fn) => fn(),
|
|
175
179
|
inject: (_deps, fn) => { pendingInjects.push(fn) },
|
|
176
|
-
|
|
180
|
+
// fiber 桩:0.1.7 configure 装配以本条目 fiber 为 owner
|
|
181
|
+
fiber: { entry: { options: { id: 'session-manager', name: '@mzzsfy/dsh-session-manager' } } },
|
|
182
|
+
get: (name) => {
|
|
183
|
+
if (name === 'configEditor') return configEditor
|
|
184
|
+
return { agents, sessionPersistence, settings: settingsService }[name]
|
|
185
|
+
},
|
|
177
186
|
on: (event, handler) => { eventHandlers[event] = handler },
|
|
178
187
|
// 日志桩:partial 降级点(logger.warn)在测试中可执行且可断言,不再被 undefined 短路
|
|
179
188
|
logger: { warns: [], warn(message) { this.warns.push(message) }, infos: [], info(message) { this.infos.push(message) } },
|
|
@@ -188,7 +197,7 @@ function makeCtx({
|
|
|
188
197
|
return services[prop]
|
|
189
198
|
},
|
|
190
199
|
})
|
|
191
|
-
apply(ctx,
|
|
200
|
+
apply(ctx, configRef)
|
|
192
201
|
return {
|
|
193
202
|
handlers: new Map(routes.map((route) => [route.path, route.handler])),
|
|
194
203
|
eventHandlers,
|
|
@@ -199,7 +208,15 @@ function makeCtx({
|
|
|
199
208
|
readCounts,
|
|
200
209
|
// 模拟宿主 settings 与 timer 服务激活:触发 inject 回调(注册 + 启动补扫 + 周期武装)
|
|
201
210
|
activateSettings: () => {
|
|
202
|
-
const injected = {
|
|
211
|
+
const injected = {
|
|
212
|
+
settings: settingsService,
|
|
213
|
+
interval: timerAvailable === false ? undefined : intervalStub,
|
|
214
|
+
// 注入上下文的 effect 桩:0.1.7 分支经它注册 configure 装配
|
|
215
|
+
effect(fn) {
|
|
216
|
+
const disposer = fn()
|
|
217
|
+
if (typeof disposer === 'function') disposers.push(disposer)
|
|
218
|
+
},
|
|
219
|
+
}
|
|
203
220
|
while (pendingInjects.length > 0) pendingInjects.shift()(injected)
|
|
204
221
|
},
|
|
205
222
|
}
|
|
@@ -812,6 +829,42 @@ test('周期评估:间隔设置运行时变更经 tick 对账(0 重启用 / 关
|
|
|
812
829
|
}
|
|
813
830
|
})
|
|
814
831
|
|
|
832
|
+
test('settings 方法面缺失(0.1.7 SettingsForms 形态):tick 与 status 路由不抛错,配置回落默认', skipMissingDeps, async () => {
|
|
833
|
+
const dir = await mkdtemp(path.join(tmpdir(), 'sm-methodface-'))
|
|
834
|
+
// 宿主 0.1.7-alpha.1 将 settings 服务换形为 SettingsForms:缺 get/register,仅剩 update。
|
|
835
|
+
// 旧形态直接 settings.get 会在 timer 回调抛 TypeError 崩进程(实测事故),守卫后应回落默认值
|
|
836
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
837
|
+
const TICK_MS = 60 * 1000
|
|
838
|
+
mock.timers.enable({ apis: ['setInterval', 'Date'], now: Date.now() })
|
|
839
|
+
try {
|
|
840
|
+
const artifact = path.join(dir, 'a.jsonl')
|
|
841
|
+
await writeFile(artifact, '{"header":1}\n{"event":0}\n')
|
|
842
|
+
const stale = Date.now() - 30 * DAY_MS
|
|
843
|
+
await utimes(artifact, stale / 1000, stale / 1000)
|
|
844
|
+
const settingsForms = { update: async () => {} }
|
|
845
|
+
const { activateSettings, handlers, registry } = makeCtx({
|
|
846
|
+
archivedIds: [],
|
|
847
|
+
headers: [{ id: 'a', cwd: 'C:\\x', createdAt: stale }],
|
|
848
|
+
agents: new Map(),
|
|
849
|
+
settingsService: settingsForms,
|
|
850
|
+
sessionPersistence: { locate: () => ({ path: artifact }) },
|
|
851
|
+
})
|
|
852
|
+
activateSettings()
|
|
853
|
+
// 守卫生效:配置读不到回落默认(days=7,intervalHours=24),30 天超期夹具到期即按默认轮归档
|
|
854
|
+
mock.timers.tick(DAY_MS + TICK_MS)
|
|
855
|
+
await waitFor(() => registry.archiveCalls.includes('a'))
|
|
856
|
+
assert.deepEqual(registry.archiveCalls, ['a'], '配置缺失按默认间隔正常评估,不因服务形变停摆')
|
|
857
|
+
// status 路由读设置不抛:回默认 days/intervalHours
|
|
858
|
+
const statusHandler = handlers.get('/api/session-manager/status')
|
|
859
|
+
const res = response()
|
|
860
|
+
await statusHandler(request('s1', 'GET'), res)
|
|
861
|
+
assert.equal(res.status, 200)
|
|
862
|
+
} finally {
|
|
863
|
+
mock.timers.reset()
|
|
864
|
+
await rm(dir, { recursive: true, force: true })
|
|
865
|
+
}
|
|
866
|
+
})
|
|
867
|
+
|
|
815
868
|
test('周期评估:缩短间隔经 earliest 钳制前移,不等满旧周期', skipMissingDeps, async () => {
|
|
816
869
|
const dir = await mkdtemp(path.join(tmpdir(), 'sm-shorten-'))
|
|
817
870
|
const TICK_MS = 60 * 1000
|
|
@@ -1728,3 +1781,103 @@ test('自动归档配置提交判定:client 与 core 镜像同源,提交守卫
|
|
|
1728
1781
|
assert.match(clientSource, /classifyAutoArchiveInput\(committed\[field\], text\)/, 'commit 必须以 committed[field] 为守卫基准')
|
|
1729
1782
|
})
|
|
1730
1783
|
|
|
1784
|
+
|
|
1785
|
+
// --- 0.1.7 settings 面双形适配(maintain a913c2c 同构) ---
|
|
1786
|
+
|
|
1787
|
+
test('Config 导出契约:根级 volatile 包装,validate 产整节单 ref,默认值对拍', () => {
|
|
1788
|
+
// Given 静态 Config 导出(0.1.7 节表单事实源)
|
|
1789
|
+
assert.ok(indexModule.Config, 'Config 导出必须在场')
|
|
1790
|
+
// When 空配置校验
|
|
1791
|
+
const resolved = indexModule.Config['~standard'].validate({})
|
|
1792
|
+
// Then 产整节单 ref(get 协议),字段为 schema 默认值
|
|
1793
|
+
assert.equal(resolved.issues, undefined)
|
|
1794
|
+
const section = resolved.value.get()
|
|
1795
|
+
assert.equal(section.autoArchiveDays, DEFAULT_AUTO_ARCHIVE_DAYS)
|
|
1796
|
+
assert.equal(section.autoArchiveIntervalHours, DEFAULT_AUTO_ARCHIVE_INTERVAL_HOURS)
|
|
1797
|
+
// When 显式值校验
|
|
1798
|
+
// Then 值透传
|
|
1799
|
+
const provided = indexModule.Config['~standard'].validate({ autoArchiveDays: 3 }).value.get()
|
|
1800
|
+
assert.equal(provided.autoArchiveDays, 3)
|
|
1801
|
+
})
|
|
1802
|
+
|
|
1803
|
+
test('0.1.7 形态:settings 无 get/register 时读回落 config ref 节值', skipMissingDeps, async () => {
|
|
1804
|
+
// Given settingsForms 只有 update(0.1.7 真实方法面)+ config ref 携带用户值
|
|
1805
|
+
const settingsForms = { update: async () => {} }
|
|
1806
|
+
const configRef = { get: () => ({ autoArchiveDays: 3, autoArchiveIntervalHours: 12 }) }
|
|
1807
|
+
const { handlers } = makeCtx({
|
|
1808
|
+
archivedIds: [],
|
|
1809
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
1810
|
+
agents: new Map(),
|
|
1811
|
+
settingsService: settingsForms,
|
|
1812
|
+
configRef,
|
|
1813
|
+
})
|
|
1814
|
+
// When GET auto-archive
|
|
1815
|
+
const res = response()
|
|
1816
|
+
await handlers.get('/api/session-manager/auto-archive')(getRequest2('/api/session-manager/auto-archive', 'GET'), res)
|
|
1817
|
+
// Then 返回 config 节值而非默认
|
|
1818
|
+
assert.deepEqual(res.body, { days: 3, intervalHours: 12 })
|
|
1819
|
+
})
|
|
1820
|
+
|
|
1821
|
+
test('0.1.7 形态:auto-archive POST 经 configEditor.edit 落 fiber.entry 并回显新值', skipMissingDeps, async () => {
|
|
1822
|
+
// Given settingsForms 只有 update + configEditor 桩(合并写回 configStore)
|
|
1823
|
+
const settingsForms = { update: async () => {} }
|
|
1824
|
+
const configStore = { autoArchiveDays: 7 }
|
|
1825
|
+
const editCalls = []
|
|
1826
|
+
const configRef = { get: () => ({ ...configStore }) }
|
|
1827
|
+
const { handlers } = makeCtx({
|
|
1828
|
+
archivedIds: [],
|
|
1829
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
1830
|
+
agents: new Map(),
|
|
1831
|
+
settingsService: settingsForms,
|
|
1832
|
+
configRef,
|
|
1833
|
+
configEditor: {
|
|
1834
|
+
async edit(entry, change) {
|
|
1835
|
+
editCalls.push(entry)
|
|
1836
|
+
const next = change({ ...configStore }, {})
|
|
1837
|
+
for (const [key, value] of Object.entries(next)) configStore[key] = value
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
})
|
|
1841
|
+
// When POST 写 days
|
|
1842
|
+
const changed = await postJson(handlers, '/api/session-manager/auto-archive', { days: 3 })
|
|
1843
|
+
// Then edit 定位本条目,合并写回,响应回显新值
|
|
1844
|
+
assert.deepEqual(changed.body, { days: 3, intervalHours: DEFAULT_AUTO_ARCHIVE_INTERVAL_HOURS })
|
|
1845
|
+
assert.equal(editCalls.length, 1)
|
|
1846
|
+
assert.equal(editCalls[0].options.id, 'session-manager')
|
|
1847
|
+
assert.equal(configStore.autoArchiveDays, 3)
|
|
1848
|
+
})
|
|
1849
|
+
|
|
1850
|
+
test('0.1.7 形态:configEditor 缺失即 400 拒写,配置不变', skipMissingDeps, async () => {
|
|
1851
|
+
// Given settingsForms 只有 update 且无 configEditor
|
|
1852
|
+
const settingsForms = { update: async () => {} }
|
|
1853
|
+
const configRef = { get: () => ({}) }
|
|
1854
|
+
const { handlers } = makeCtx({
|
|
1855
|
+
archivedIds: [],
|
|
1856
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
1857
|
+
agents: new Map(),
|
|
1858
|
+
settingsService: settingsForms,
|
|
1859
|
+
configRef,
|
|
1860
|
+
})
|
|
1861
|
+
// When POST 写 days
|
|
1862
|
+
const denied = await postJson(handlers, '/api/session-manager/auto-archive', { days: 3 })
|
|
1863
|
+
// Then 400 错误响应
|
|
1864
|
+
assert.equal(denied.status, 400)
|
|
1865
|
+
assert.ok(denied.body.error)
|
|
1866
|
+
})
|
|
1867
|
+
|
|
1868
|
+
test('0.1.7 形态:settings.configure 在场即关闭原生自动页', skipMissingDeps, () => {
|
|
1869
|
+
// Given settingsForms 带 configure 无 register
|
|
1870
|
+
const configureCalls = []
|
|
1871
|
+
const settingsForms = { configure: (presentation, owner) => { configureCalls.push({ presentation, owner }); return () => {} } }
|
|
1872
|
+
const { activateSettings } = makeCtx({
|
|
1873
|
+
archivedIds: [],
|
|
1874
|
+
headers: [],
|
|
1875
|
+
agents: new Map(),
|
|
1876
|
+
settingsService: settingsForms,
|
|
1877
|
+
})
|
|
1878
|
+
// When 激活 settings 注入
|
|
1879
|
+
activateSettings()
|
|
1880
|
+
// Then 关闭自动页
|
|
1881
|
+
assert.equal(configureCalls.length, 1)
|
|
1882
|
+
assert.deepEqual(configureCalls[0].presentation, { auto: false })
|
|
1883
|
+
})
|