@mzzsfy/dsh-session-manager 0.9.2 → 0.9.4
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 +8 -1
- package/src/client.js +1 -1
- package/src/core.mjs +10 -1
- package/src/index.js +61 -11
- package/test/core.test.mjs +14 -0
- package/test/index.test.mjs +192 -5
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
|
+
按仓库动态窗口验证(scripts/compat/window.mjs),不预置任何固定版本槽:主测 = 渠道 dist-tag 最新 rc,完全适配口径;基线 = npm latest,不崩溃底线口径;主测与基线同版时收敛单槽。兼容面以窗口实测为准,不逐版本承诺。逐轮实测结论见 `docs/兼容性测试/逐包测试进度.md` 与本包 `兼容性测试.md`。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzzsfy/dsh-session-manager",
|
|
3
3
|
"description": "会话自动归档与管理:超期自动归档、归档面板、删除进系统回收站、还原后一键重挂载",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
"external": [
|
|
19
19
|
"@mzzsfy/dsh-toast/client"
|
|
20
20
|
]
|
|
21
|
+
},
|
|
22
|
+
"compatibility": {
|
|
23
|
+
"dshReleases": {
|
|
24
|
+
"0.2.0-rc.1": "unknown",
|
|
25
|
+
"0.2.0-rc.2": "compatible",
|
|
26
|
+
"0.2.1-alpha.1": "unknown"
|
|
27
|
+
}
|
|
21
28
|
}
|
|
22
29
|
},
|
|
23
30
|
"scripts": {
|
package/src/client.js
CHANGED
|
@@ -665,7 +665,7 @@ function SessionManagerApp(props) {
|
|
|
665
665
|
|
|
666
666
|
// 面板设置项悬停说明:原生 title(设置侧栏为滚动容器,CSS 气泡会被 overflow
|
|
667
667
|
// 裁剪,JS 定位复杂度不成比例);文案与功能行为同源维护,由源码契约测试锁定
|
|
668
|
-
const ARCHIVE_OVERVIEW_TITLE = '
|
|
668
|
+
const ARCHIVE_OVERVIEW_TITLE = '超过阈值天数未活跃的会话自动移入归档;触发时机为新会话创建、插件启动与周期检查;归档会话在本面板管理,可恢复或删除;恢复后 10 分钟内暂停自动归档,期间发言即恢复正常活跃计时。'
|
|
669
669
|
const ARCHIVE_DAYS_TITLE = '会话超过该天数未活跃(产物无更新)即自动归档,空白(无消息)会话不参与归档;0 = 关闭自动归档。仅对产物可读的会话评估,产物不可读的跳过以免误归档,已归档的会话不会重复处理。'
|
|
670
670
|
const ARCHIVE_INTERVAL_TITLE = '周期检查的间隔小时数,到期会话在下一轮检查时归档;0 = 关闭周期检查(新会话创建与插件启动时的检查不受影响);调小最迟等新间隔即可提前触发,调大自下一轮排期起生效。'
|
|
671
671
|
|
package/src/core.mjs
CHANGED
|
@@ -24,10 +24,19 @@ export function updatedAtOf(header, activityAtMs) {
|
|
|
24
24
|
return Math.max(header.createdAt, activityAtMs ?? 0)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// 取消归档全局宽限时长:恢复会话到用户发出首条消息之间的评估窗口保护,
|
|
28
|
+
// 首条消息更新产物 mtime 后由阈值自然接管;只查阅不发言的会话宽限后正常归档
|
|
29
|
+
export const UNARCHIVE_GRACE_MS = 10 * 60 * 1000
|
|
30
|
+
|
|
31
|
+
/** 宽限期判定:取消归档后 graceMs 内暂停自动归档(用户恢复即意图信号)。 */
|
|
32
|
+
export function unarchiveGraceActive(unarchivedAtMs, nowMs, graceMs = UNARCHIVE_GRACE_MS) {
|
|
33
|
+
if (!Number.isFinite(unarchivedAtMs) || unarchivedAtMs <= 0) return false
|
|
34
|
+
return nowMs - unarchivedAtMs < graceMs
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* 归档评估状态机:按阈值筛出待归档会话 id。
|
|
29
39
|
* @param records - 候选行 {id, archived, running, blank, updatedAt}
|
|
30
|
-
* @returns 超期且未归档、非运行中、非空白的会话 id
|
|
31
40
|
*/
|
|
32
41
|
export function selectArchiveCandidates({ records, nowMs, thresholdDays }) {
|
|
33
42
|
if (!Number.isFinite(thresholdDays) || thresholdDays <= 0) return []
|
package/src/index.js
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
mergeDeletedEntry,
|
|
27
27
|
removeDeletedEntry,
|
|
28
28
|
selectArchiveCandidates,
|
|
29
|
+
UNARCHIVE_GRACE_MS,
|
|
30
|
+
unarchiveGraceActive,
|
|
29
31
|
updatedAtOf,
|
|
30
32
|
} from './core.mjs'
|
|
31
33
|
import { moveToQuarantine, restoreFromQuarantine, trashPath } from './trash.mjs'
|
|
@@ -98,6 +100,21 @@ const SETTINGS_SCHEMA = schemastery.object({
|
|
|
98
100
|
.description('周期评估间隔小时数,0 表示关闭周期评估;变更经周期 tick 对账,关闭即时暂停、重启用最迟下个 tick 生效'),
|
|
99
101
|
})
|
|
100
102
|
|
|
103
|
+
// 0.1.7 宿主以静态 Config 导出生成设置节表单(maintain 同构);legacy 宿主经
|
|
104
|
+
// settings.register 注册同名命名空间。根级 volatile 包装:0.1.7 下整节为 live
|
|
105
|
+
// 表单,节写经 loader 原地热更;旧宿主 schemastery 无 volatile 方法,特性检测原样返回
|
|
106
|
+
export const Config = volatileWrap(SETTINGS_SCHEMA)
|
|
107
|
+
|
|
108
|
+
function volatileWrap(schema) {
|
|
109
|
+
return typeof schema.volatile === 'function' ? schema.volatile() : schema
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// volatile ref 动态解包(get 协议):0.1.7 下 apply 入参 config 为整节单 ref;
|
|
113
|
+
// legacy 宿主 config 为普通对象原样透传
|
|
114
|
+
function unwrapVolatile(value) {
|
|
115
|
+
return typeof value?.get === 'function' ? value.get() : value
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
function sendJson(res, status, payload) {
|
|
102
119
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
103
120
|
res.end(JSON.stringify(payload))
|
|
@@ -114,9 +131,12 @@ function respondError(ctx, res, error) {
|
|
|
114
131
|
sendJson(res, 400, { error: message })
|
|
115
132
|
}
|
|
116
133
|
|
|
117
|
-
function readSettings(ctx) {
|
|
134
|
+
function readSettings(ctx, config) {
|
|
118
135
|
const settings = ctx.get('settings')
|
|
119
|
-
|
|
136
|
+
// 双形读(maintain 同构):legacy settings 命名空间 get;0.1.7+ apply 入参
|
|
137
|
+
// config 整节解包。方法面守卫兼防:服务在但缺 get(宿主升级变更面)时回落默认值,
|
|
138
|
+
// 防 timer 回调抛错崩进程
|
|
139
|
+
const value = settings && typeof settings.get === 'function' ? settings.get(NAMESPACE) : unwrapVolatile(config)
|
|
120
140
|
const days = Number(value && value.autoArchiveDays)
|
|
121
141
|
const hours = Number(value && value.autoArchiveIntervalHours)
|
|
122
142
|
return {
|
|
@@ -199,6 +219,9 @@ function rejectMethod(req, res, method) {
|
|
|
199
219
|
/** @param {import('@deepseek-ai/cordis').Context} ctx */
|
|
200
220
|
export function apply(ctx, config) {
|
|
201
221
|
let evaluating = false
|
|
222
|
+
// 取消归档全局宽限起点(进程内,重启失效可接受):恢复会话到用户发出首条
|
|
223
|
+
// 消息之间的评估窗口保护,首条消息更新产物 mtime 后由阈值自然接管
|
|
224
|
+
let unarchivedAtMs = 0
|
|
202
225
|
// 删除路由同 id 并发去重(进程内)
|
|
203
226
|
const inFlightDeletes = new Set()
|
|
204
227
|
|
|
@@ -228,7 +251,7 @@ export function apply(ctx, config) {
|
|
|
228
251
|
// 活跃会话零 IO。返回是否真正启动:被门闩/关闭挡下的调用方(周期 tick)保持到期态
|
|
229
252
|
// 由下个 tick 重试,不得据此重排周期
|
|
230
253
|
function evaluateArchives(cwd) {
|
|
231
|
-
const { days } = readSettings(ctx)
|
|
254
|
+
const { days } = readSettings(ctx, config)
|
|
232
255
|
if (days === 0 || evaluating) return false
|
|
233
256
|
evaluating = true
|
|
234
257
|
let timeoutGuard
|
|
@@ -240,6 +263,8 @@ export function apply(ctx, config) {
|
|
|
240
263
|
const persistence = ctx.get('sessionPersistence')
|
|
241
264
|
const records = await ctx.sessionQuery.listSessions()
|
|
242
265
|
const nowMs = Date.now()
|
|
266
|
+
// 全局宽限短路:恢复动作后 graceMs 内暂停归档,保护「恢复 → 首条消息」窗口
|
|
267
|
+
if (unarchiveGraceActive(unarchivedAtMs, nowMs)) return
|
|
243
268
|
const cutoff = nowMs - days * DAY_MS
|
|
244
269
|
const candidates = []
|
|
245
270
|
for (const recordItem of records) {
|
|
@@ -367,6 +392,11 @@ export function apply(ctx, config) {
|
|
|
367
392
|
})
|
|
368
393
|
}
|
|
369
394
|
|
|
395
|
+
// 宽限记账:取消归档时更新宽限起点(进程内变量,无持久化副作用)
|
|
396
|
+
function recordUnarchiveGrace() {
|
|
397
|
+
unarchivedAtMs = Date.now()
|
|
398
|
+
}
|
|
399
|
+
|
|
370
400
|
// 台账命中查询:同 id 曾删除过(重删场景的幽灵资格依据);域不可用时按未命中
|
|
371
401
|
async function ledgerHasEntry(sessionId) {
|
|
372
402
|
try {
|
|
@@ -433,7 +463,9 @@ export function apply(ctx, config) {
|
|
|
433
463
|
handler: async (req, res) => {
|
|
434
464
|
if (!rejectMethod(req, res, 'POST')) return
|
|
435
465
|
try {
|
|
436
|
-
|
|
466
|
+
const sessionId = await requireSessionId(req)
|
|
467
|
+
await removeArchivedId(sessionId)
|
|
468
|
+
await recordUnarchiveGrace()
|
|
437
469
|
sendJson(res, 200, { ok: true })
|
|
438
470
|
} catch (error) {
|
|
439
471
|
respondError(ctx, res, error)
|
|
@@ -624,7 +656,7 @@ export function apply(ctx, config) {
|
|
|
624
656
|
handler: async (req, res) => {
|
|
625
657
|
try {
|
|
626
658
|
if (req.method === 'GET') {
|
|
627
|
-
const current = readSettings(ctx)
|
|
659
|
+
const current = readSettings(ctx, config)
|
|
628
660
|
sendJson(res, 200, { days: current.days, intervalHours: current.intervalHours })
|
|
629
661
|
return
|
|
630
662
|
}
|
|
@@ -636,11 +668,20 @@ export function apply(ctx, config) {
|
|
|
636
668
|
throw new Error(MESSAGES.badJsonBody)
|
|
637
669
|
}
|
|
638
670
|
const patch = readAutoArchivePatch(body)
|
|
671
|
+
// 双形写(maintain 同构,判定延迟到使用点):legacy 方法面以 register 为准
|
|
672
|
+
// (0.1.7 的 settings.update 名义在场但按命名空间找条目必败,不可作判据);
|
|
673
|
+
// 0.1.7+ 走 configEditor.edit 落 profile 条目(fiber.entry,与命名空间解耦)。
|
|
674
|
+
// await 保证回显为新值,persist 失败走 respondError
|
|
639
675
|
const settings = ctx.get('settings')
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
676
|
+
if (typeof settings?.register === 'function' && typeof settings?.update === 'function') {
|
|
677
|
+
await settings.update(NAMESPACE, patch)
|
|
678
|
+
} else {
|
|
679
|
+
const editor = ctx.get('configEditor')
|
|
680
|
+
const entry = ctx.fiber?.entry
|
|
681
|
+
if (!editor || typeof editor.edit !== 'function' || !entry) throw new Error('宿主设置服务不可用')
|
|
682
|
+
await editor.edit(entry, (current) => ({ ...current, ...patch }))
|
|
683
|
+
}
|
|
684
|
+
const current = readSettings(ctx, config)
|
|
644
685
|
sendJson(res, 200, { days: current.days, intervalHours: current.intervalHours })
|
|
645
686
|
} catch (error) {
|
|
646
687
|
respondError(ctx, res, error)
|
|
@@ -735,13 +776,13 @@ export function apply(ctx, config) {
|
|
|
735
776
|
let nextDueAt = null
|
|
736
777
|
let lastArmedAt = 0
|
|
737
778
|
const scheduleNext = () => {
|
|
738
|
-
const { intervalHours } = readSettings(ctx)
|
|
779
|
+
const { intervalHours } = readSettings(ctx, config)
|
|
739
780
|
lastArmedAt = Date.now()
|
|
740
781
|
nextDueAt = intervalHours > 0 ? lastArmedAt + intervalHours * HOUR_MS : null
|
|
741
782
|
}
|
|
742
783
|
try {
|
|
743
784
|
const dispose = timerCtx.interval(() => {
|
|
744
|
-
const { intervalHours } = readSettings(ctx)
|
|
785
|
+
const { intervalHours } = readSettings(ctx, config)
|
|
745
786
|
// 设置变更对账:0 即时暂停;缺失 due(0 重启用)时补排,防周期轮静默死亡;
|
|
746
787
|
// 缩短间隔时以 lastArmedAt 锚定前移,最长等新间隔而非等满旧周期
|
|
747
788
|
if (intervalHours === 0) {
|
|
@@ -769,6 +810,15 @@ export function apply(ctx, config) {
|
|
|
769
810
|
})
|
|
770
811
|
|
|
771
812
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
813
|
+
// 方法面守卫:settings 服务在但缺 register(宿主升级变更面)时跳过注册,
|
|
814
|
+
// 周期轮与启动补扫按 readSettings 的默认值降级运行,不因服务形变炸 fiber;
|
|
815
|
+
// 0.1.7 关闭原生自动设置页,设置面由本包自带分区承担(maintain 同构)
|
|
816
|
+
if (typeof settingsCtx.settings.register !== 'function') {
|
|
817
|
+
if (typeof settingsCtx.settings?.configure === 'function') {
|
|
818
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
|
|
819
|
+
}
|
|
820
|
+
return
|
|
821
|
+
}
|
|
772
822
|
settingsCtx.settings.register(NAMESPACE, SETTINGS_SCHEMA, { base: config })
|
|
773
823
|
// 启动补扫不依赖定时服务:settings 就绪即评估一轮,清掉停机期间积压的超期会话
|
|
774
824
|
evaluateArchives()
|
package/test/core.test.mjs
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
projectDeletedRows,
|
|
27
27
|
removeDeletedEntry,
|
|
28
28
|
selectArchiveCandidates,
|
|
29
|
+
UNARCHIVE_GRACE_MS,
|
|
30
|
+
unarchiveGraceActive,
|
|
29
31
|
updatedAtOf,
|
|
30
32
|
workspaceTitleForSession,
|
|
31
33
|
workspaceTitleOf,
|
|
@@ -80,6 +82,18 @@ test('阈值负值与非有限值防御性关闭', () => {
|
|
|
80
82
|
assert.deepEqual(selectArchiveCandidates({ records: [record({})], nowMs: NOW, thresholdDays: Number.NaN }), [])
|
|
81
83
|
})
|
|
82
84
|
|
|
85
|
+
test('全局宽限:取消归档后 graceMs 内宽限生效,到期与非法输入失效', () => {
|
|
86
|
+
assert.equal(unarchiveGraceActive(NOW - 2 * 60 * 1000, NOW), true)
|
|
87
|
+
assert.equal(unarchiveGraceActive(NOW - UNARCHIVE_GRACE_MS, NOW), false)
|
|
88
|
+
assert.equal(unarchiveGraceActive(0, NOW), false)
|
|
89
|
+
assert.equal(unarchiveGraceActive(undefined, NOW), false)
|
|
90
|
+
assert.equal(unarchiveGraceActive(Number.NaN, NOW), false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('全局宽限:常量与天级时长的量级自洽', () => {
|
|
94
|
+
assert.equal(UNARCHIVE_GRACE_MS, 10 * 60 * 1000)
|
|
95
|
+
})
|
|
96
|
+
|
|
83
97
|
test('updatedAt 取创建时间与最近活跃的较大者', () => {
|
|
84
98
|
assert.equal(updatedAtOf({ createdAt: 100 }, 50), 100)
|
|
85
99
|
assert.equal(updatedAtOf({ createdAt: 100 }, 500), 500)
|
package/test/index.test.mjs
CHANGED
|
@@ -76,7 +76,7 @@ function makeDomain(initialIds) {
|
|
|
76
76
|
// 台账域句柄;openRejected 模拟台账域打开失败
|
|
77
77
|
function makeLedgerDomain(entries) {
|
|
78
78
|
const domain = {
|
|
79
|
-
state: { deleted: entries },
|
|
79
|
+
state: { deleted: entries, unarchivedAt: 0 },
|
|
80
80
|
writes: 0,
|
|
81
81
|
global: {
|
|
82
82
|
get: () => domain.state,
|
|
@@ -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
|
}
|
|
@@ -464,6 +481,40 @@ test('取消归档路由:快照领先域时补全清理(重试路径)', skipMiss
|
|
|
464
481
|
assert.deepEqual(registry.state.archivedSessionIds, [])
|
|
465
482
|
})
|
|
466
483
|
|
|
484
|
+
test('取消归档宽限:恢复后 graceMs 内同进程评估暂停,不秒归档', skipMissingDeps, async () => {
|
|
485
|
+
const dir = await mkdtemp(path.join(tmpdir(), 'sm-grace-'))
|
|
486
|
+
try {
|
|
487
|
+
const artifact = path.join(dir, 's1.jsonl')
|
|
488
|
+
await writeFile(artifact, '{"header":1}\n{"event":0}\n')
|
|
489
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
490
|
+
const stale = Date.now() - 30 * DAY_MS
|
|
491
|
+
await utimes(artifact, stale / 1000, stale / 1000)
|
|
492
|
+
const headers = [{ id: 's1', cwd: 'C:\\x', createdAt: stale }]
|
|
493
|
+
const ctxBundle = makeCtx({
|
|
494
|
+
archivedIds: [],
|
|
495
|
+
headers,
|
|
496
|
+
agents: new Map(),
|
|
497
|
+
sessionPersistence: { locate: () => ({ path: artifact }) },
|
|
498
|
+
domain: makeDomain([]),
|
|
499
|
+
})
|
|
500
|
+
const { activateSettings, registry, handlers, eventHandlers } = ctxBundle
|
|
501
|
+
activateSettings()
|
|
502
|
+
await waitFor(() => registry.archiveCalls.length > 0)
|
|
503
|
+
assert.deepEqual(registry.archiveCalls, ['s1'])
|
|
504
|
+
// 手动恢复:归档集合移除 + 进程内宽限生效
|
|
505
|
+
const res = response()
|
|
506
|
+
await handlers.get('/api/session-manager/unarchive')(request('s1'), res)
|
|
507
|
+
assert.equal(res.status, 200)
|
|
508
|
+
assert.deepEqual(registry.state.archivedSessionIds, [])
|
|
509
|
+
// 宽限内经新会话创建触发评估:同产物仍超期,但不再归档
|
|
510
|
+
eventHandlers['session/created']({ header: { cwd: 'C:\\x' } })
|
|
511
|
+
await new Promise((resolve) => setTimeout(resolve, 80))
|
|
512
|
+
assert.deepEqual(registry.archiveCalls, ['s1'], '宽限期内不得重新归档')
|
|
513
|
+
} finally {
|
|
514
|
+
await rm(dir, { recursive: true, force: true })
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
467
518
|
test('info 路由:会话不存在拒绝', skipMissingDeps, async () => {
|
|
468
519
|
const { handlers } = makeCtx({ archivedIds: [], headers: [], agents: new Map() })
|
|
469
520
|
const res = response()
|
|
@@ -812,6 +863,42 @@ test('周期评估:间隔设置运行时变更经 tick 对账(0 重启用 / 关
|
|
|
812
863
|
}
|
|
813
864
|
})
|
|
814
865
|
|
|
866
|
+
test('settings 方法面缺失(0.1.7 SettingsForms 形态):tick 与 status 路由不抛错,配置回落默认', skipMissingDeps, async () => {
|
|
867
|
+
const dir = await mkdtemp(path.join(tmpdir(), 'sm-methodface-'))
|
|
868
|
+
// 宿主 0.1.7-alpha.1 将 settings 服务换形为 SettingsForms:缺 get/register,仅剩 update。
|
|
869
|
+
// 旧形态直接 settings.get 会在 timer 回调抛 TypeError 崩进程(实测事故),守卫后应回落默认值
|
|
870
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
871
|
+
const TICK_MS = 60 * 1000
|
|
872
|
+
mock.timers.enable({ apis: ['setInterval', 'Date'], now: Date.now() })
|
|
873
|
+
try {
|
|
874
|
+
const artifact = path.join(dir, 'a.jsonl')
|
|
875
|
+
await writeFile(artifact, '{"header":1}\n{"event":0}\n')
|
|
876
|
+
const stale = Date.now() - 30 * DAY_MS
|
|
877
|
+
await utimes(artifact, stale / 1000, stale / 1000)
|
|
878
|
+
const settingsForms = { update: async () => {} }
|
|
879
|
+
const { activateSettings, handlers, registry } = makeCtx({
|
|
880
|
+
archivedIds: [],
|
|
881
|
+
headers: [{ id: 'a', cwd: 'C:\\x', createdAt: stale }],
|
|
882
|
+
agents: new Map(),
|
|
883
|
+
settingsService: settingsForms,
|
|
884
|
+
sessionPersistence: { locate: () => ({ path: artifact }) },
|
|
885
|
+
})
|
|
886
|
+
activateSettings()
|
|
887
|
+
// 守卫生效:配置读不到回落默认(days=7,intervalHours=24),30 天超期夹具到期即按默认轮归档
|
|
888
|
+
mock.timers.tick(DAY_MS + TICK_MS)
|
|
889
|
+
await waitFor(() => registry.archiveCalls.includes('a'))
|
|
890
|
+
assert.deepEqual(registry.archiveCalls, ['a'], '配置缺失按默认间隔正常评估,不因服务形变停摆')
|
|
891
|
+
// status 路由读设置不抛:回默认 days/intervalHours
|
|
892
|
+
const statusHandler = handlers.get('/api/session-manager/status')
|
|
893
|
+
const res = response()
|
|
894
|
+
await statusHandler(request('s1', 'GET'), res)
|
|
895
|
+
assert.equal(res.status, 200)
|
|
896
|
+
} finally {
|
|
897
|
+
mock.timers.reset()
|
|
898
|
+
await rm(dir, { recursive: true, force: true })
|
|
899
|
+
}
|
|
900
|
+
})
|
|
901
|
+
|
|
815
902
|
test('周期评估:缩短间隔经 earliest 钳制前移,不等满旧周期', skipMissingDeps, async () => {
|
|
816
903
|
const dir = await mkdtemp(path.join(tmpdir(), 'sm-shorten-'))
|
|
817
904
|
const TICK_MS = 60 * 1000
|
|
@@ -1728,3 +1815,103 @@ test('自动归档配置提交判定:client 与 core 镜像同源,提交守卫
|
|
|
1728
1815
|
assert.match(clientSource, /classifyAutoArchiveInput\(committed\[field\], text\)/, 'commit 必须以 committed[field] 为守卫基准')
|
|
1729
1816
|
})
|
|
1730
1817
|
|
|
1818
|
+
|
|
1819
|
+
// --- 0.1.7 settings 面双形适配(maintain a913c2c 同构) ---
|
|
1820
|
+
|
|
1821
|
+
test('Config 导出契约:根级 volatile 包装,validate 产整节单 ref,默认值对拍', () => {
|
|
1822
|
+
// Given 静态 Config 导出(0.1.7 节表单事实源)
|
|
1823
|
+
assert.ok(indexModule.Config, 'Config 导出必须在场')
|
|
1824
|
+
// When 空配置校验
|
|
1825
|
+
const resolved = indexModule.Config['~standard'].validate({})
|
|
1826
|
+
// Then 产整节单 ref(get 协议),字段为 schema 默认值
|
|
1827
|
+
assert.equal(resolved.issues, undefined)
|
|
1828
|
+
const section = resolved.value.get()
|
|
1829
|
+
assert.equal(section.autoArchiveDays, DEFAULT_AUTO_ARCHIVE_DAYS)
|
|
1830
|
+
assert.equal(section.autoArchiveIntervalHours, DEFAULT_AUTO_ARCHIVE_INTERVAL_HOURS)
|
|
1831
|
+
// When 显式值校验
|
|
1832
|
+
// Then 值透传
|
|
1833
|
+
const provided = indexModule.Config['~standard'].validate({ autoArchiveDays: 3 }).value.get()
|
|
1834
|
+
assert.equal(provided.autoArchiveDays, 3)
|
|
1835
|
+
})
|
|
1836
|
+
|
|
1837
|
+
test('0.1.7 形态:settings 无 get/register 时读回落 config ref 节值', skipMissingDeps, async () => {
|
|
1838
|
+
// Given settingsForms 只有 update(0.1.7 真实方法面)+ config ref 携带用户值
|
|
1839
|
+
const settingsForms = { update: async () => {} }
|
|
1840
|
+
const configRef = { get: () => ({ autoArchiveDays: 3, autoArchiveIntervalHours: 12 }) }
|
|
1841
|
+
const { handlers } = makeCtx({
|
|
1842
|
+
archivedIds: [],
|
|
1843
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
1844
|
+
agents: new Map(),
|
|
1845
|
+
settingsService: settingsForms,
|
|
1846
|
+
configRef,
|
|
1847
|
+
})
|
|
1848
|
+
// When GET auto-archive
|
|
1849
|
+
const res = response()
|
|
1850
|
+
await handlers.get('/api/session-manager/auto-archive')(getRequest2('/api/session-manager/auto-archive', 'GET'), res)
|
|
1851
|
+
// Then 返回 config 节值而非默认
|
|
1852
|
+
assert.deepEqual(res.body, { days: 3, intervalHours: 12 })
|
|
1853
|
+
})
|
|
1854
|
+
|
|
1855
|
+
test('0.1.7 形态:auto-archive POST 经 configEditor.edit 落 fiber.entry 并回显新值', skipMissingDeps, async () => {
|
|
1856
|
+
// Given settingsForms 只有 update + configEditor 桩(合并写回 configStore)
|
|
1857
|
+
const settingsForms = { update: async () => {} }
|
|
1858
|
+
const configStore = { autoArchiveDays: 7 }
|
|
1859
|
+
const editCalls = []
|
|
1860
|
+
const configRef = { get: () => ({ ...configStore }) }
|
|
1861
|
+
const { handlers } = makeCtx({
|
|
1862
|
+
archivedIds: [],
|
|
1863
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
1864
|
+
agents: new Map(),
|
|
1865
|
+
settingsService: settingsForms,
|
|
1866
|
+
configRef,
|
|
1867
|
+
configEditor: {
|
|
1868
|
+
async edit(entry, change) {
|
|
1869
|
+
editCalls.push(entry)
|
|
1870
|
+
const next = change({ ...configStore }, {})
|
|
1871
|
+
for (const [key, value] of Object.entries(next)) configStore[key] = value
|
|
1872
|
+
},
|
|
1873
|
+
},
|
|
1874
|
+
})
|
|
1875
|
+
// When POST 写 days
|
|
1876
|
+
const changed = await postJson(handlers, '/api/session-manager/auto-archive', { days: 3 })
|
|
1877
|
+
// Then edit 定位本条目,合并写回,响应回显新值
|
|
1878
|
+
assert.deepEqual(changed.body, { days: 3, intervalHours: DEFAULT_AUTO_ARCHIVE_INTERVAL_HOURS })
|
|
1879
|
+
assert.equal(editCalls.length, 1)
|
|
1880
|
+
assert.equal(editCalls[0].options.id, 'session-manager')
|
|
1881
|
+
assert.equal(configStore.autoArchiveDays, 3)
|
|
1882
|
+
})
|
|
1883
|
+
|
|
1884
|
+
test('0.1.7 形态:configEditor 缺失即 400 拒写,配置不变', skipMissingDeps, async () => {
|
|
1885
|
+
// Given settingsForms 只有 update 且无 configEditor
|
|
1886
|
+
const settingsForms = { update: async () => {} }
|
|
1887
|
+
const configRef = { get: () => ({}) }
|
|
1888
|
+
const { handlers } = makeCtx({
|
|
1889
|
+
archivedIds: [],
|
|
1890
|
+
headers: [{ id: 's1', cwd: 'C:\\x', createdAt: 0 }],
|
|
1891
|
+
agents: new Map(),
|
|
1892
|
+
settingsService: settingsForms,
|
|
1893
|
+
configRef,
|
|
1894
|
+
})
|
|
1895
|
+
// When POST 写 days
|
|
1896
|
+
const denied = await postJson(handlers, '/api/session-manager/auto-archive', { days: 3 })
|
|
1897
|
+
// Then 400 错误响应
|
|
1898
|
+
assert.equal(denied.status, 400)
|
|
1899
|
+
assert.ok(denied.body.error)
|
|
1900
|
+
})
|
|
1901
|
+
|
|
1902
|
+
test('0.1.7 形态:settings.configure 在场即关闭原生自动页', skipMissingDeps, () => {
|
|
1903
|
+
// Given settingsForms 带 configure 无 register
|
|
1904
|
+
const configureCalls = []
|
|
1905
|
+
const settingsForms = { configure: (presentation, owner) => { configureCalls.push({ presentation, owner }); return () => {} } }
|
|
1906
|
+
const { activateSettings } = makeCtx({
|
|
1907
|
+
archivedIds: [],
|
|
1908
|
+
headers: [],
|
|
1909
|
+
agents: new Map(),
|
|
1910
|
+
settingsService: settingsForms,
|
|
1911
|
+
})
|
|
1912
|
+
// When 激活 settings 注入
|
|
1913
|
+
activateSettings()
|
|
1914
|
+
// Then 关闭自动页
|
|
1915
|
+
assert.equal(configureCalls.length, 1)
|
|
1916
|
+
assert.deepEqual(configureCalls[0].presentation, { auto: false })
|
|
1917
|
+
})
|