@mzzsfy/dsh-session-manager 0.9.6 → 0.9.7
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/package.json +5 -3
- package/src/index.js +6 -23
- package/test/index.test.mjs +35 -62
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.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
22
|
"compatibility": {
|
|
23
|
+
"hostMin": "0.2.1-rc.2",
|
|
23
24
|
"dshReleases": {
|
|
24
25
|
"0.2.0-rc.1": "unknown",
|
|
25
|
-
"0.2.0-rc.2": "
|
|
26
|
-
"0.2.1-alpha.1": "unknown"
|
|
26
|
+
"0.2.0-rc.2": "unknown",
|
|
27
|
+
"0.2.1-alpha.1": "unknown",
|
|
28
|
+
"0.2.1-rc.2": "compatible"
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
},
|
package/src/index.js
CHANGED
|
@@ -37,7 +37,6 @@ export const name = 'dsh-session-manager'
|
|
|
37
37
|
export const inject = ['webServer', 'workspaceRegistry', 'sessionQuery', 'storageDomain', 'agents', 'sessions', 'sessionPersistence']
|
|
38
38
|
|
|
39
39
|
const NAMESPACE = 'session-manager'
|
|
40
|
-
const WORKSPACE_DOMAIN_NAME = 'workspace'
|
|
41
40
|
|
|
42
41
|
// 已删除台账域:global 单列表,条目为回收站还原后重挂载所需的最小信息;
|
|
43
42
|
// heldPath 仅回收区降级条目携带(暂存路径,重挂载时移回 path 原位置)
|
|
@@ -226,9 +225,9 @@ export function apply(ctx, config) {
|
|
|
226
225
|
const inFlightDeletes = new Set()
|
|
227
226
|
|
|
228
227
|
// 插件回收区:系统回收站不可用环境的删除降级暂存地,重挂载时移回原位置。
|
|
229
|
-
// 测试经 env DSH_QUARANTINE_DIR
|
|
228
|
+
// 测试经 env DSH_QUARANTINE_DIR 注入临时目录;缺省从宿主 home 派生(DSH_HOME 即 .dsh 根)
|
|
230
229
|
const quarantineDir = process.env.DSH_QUARANTINE_DIR
|
|
231
|
-
|| join(homedir(), '.dsh', 'trash', 'session-manager')
|
|
230
|
+
|| join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'trash', 'session-manager')
|
|
232
231
|
|
|
233
232
|
// header 查找缓存:归档/删除/重挂载路由共用(inputs 浮层已迁 dsh-context-manager)
|
|
234
233
|
const headerCache = new Map()
|
|
@@ -332,27 +331,11 @@ export function apply(ctx, config) {
|
|
|
332
331
|
evaluateArchives(cwd)
|
|
333
332
|
})
|
|
334
333
|
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
// 判定也会因旧快照静默跳过。合并 durable 与快照再移除,可自愈历史失同步与清理
|
|
339
|
-
// 半失败(经取消归档重试即补全),代价是快照中的陈旧 id 会随任一次清理重新落盘,
|
|
340
|
-
// 可经再次取消归档清除。inject 保证注册表就绪,快照缺失即上游形态变更,loud fail;
|
|
341
|
-
// 域 global 仅支持整体写回,读改写窗口若与注册表两阶段变更交错,理论上可回退其
|
|
342
|
-
// pendingMutation/workspaceIds 中间态,下次启动 validateStoredState 将 loud fail,
|
|
343
|
-
// 窗口极窄且域 API 无原子原语可用,属已知取舍(详见 README)。
|
|
334
|
+
// 取消归档:官方 workspaceRegistry.unarchiveSession(0.2.1-rc.2+ 串行实现,
|
|
335
|
+
// 未归档 id 幂等不写)写穿 state,无私有直写的读改写窗口与快照失同步问题;
|
|
336
|
+
// 合并自愈语义随私有写一并退役(开发守则:移除被宿主能力取代的实现)
|
|
344
337
|
async function removeArchivedId(sessionId) {
|
|
345
|
-
|
|
346
|
-
if (domain === undefined) throw new Error('workspace 域未打开')
|
|
347
|
-
const durable = domain.global.get()
|
|
348
|
-
const cached = ctx.workspaceRegistry.state
|
|
349
|
-
if (cached === undefined) throw new Error('workspace 注册表快照不可用')
|
|
350
|
-
const merged = new Set(durable.archivedSessionIds.map(String))
|
|
351
|
-
for (const id of cached.archivedSessionIds) merged.add(String(id))
|
|
352
|
-
if (!merged.delete(String(sessionId))) return
|
|
353
|
-
const next = { ...durable, archivedSessionIds: [...merged] }
|
|
354
|
-
await domain.global.set(next)
|
|
355
|
-
ctx.workspaceRegistry.state = next
|
|
338
|
+
await ctx.workspaceRegistry.unarchiveSession(sessionId)
|
|
356
339
|
}
|
|
357
340
|
|
|
358
341
|
// 台账记账:trash 成功即记录,路径为回收站「原位置」还原所需。
|
package/test/index.test.mjs
CHANGED
|
@@ -39,17 +39,26 @@ const GHOST_DIR = path.join(tmpdir(), 'sm-ghost-fixture', 's1')
|
|
|
39
39
|
const GHOST_PATH = path.join(GHOST_DIR, 'session.jsonl.zstd')
|
|
40
40
|
|
|
41
41
|
function makeRegistry(initialIds) {
|
|
42
|
-
//
|
|
43
|
-
// archiveSession
|
|
42
|
+
// 真实注册表契约(0.2.1-rc.2+):archivedSessionIds 是进程内快照的 getter;
|
|
43
|
+
// archiveSession 归档即入集合,unarchiveSession 串行移除(未归档 id 幂等不写),
|
|
44
|
+
// 两个官方入口都写穿 state——快照与持久集由注册表自身保证一致
|
|
44
45
|
const registry = {
|
|
45
46
|
state: { initialized: true, workspaceIds: [], archivedSessionIds: initialIds },
|
|
46
47
|
list: () => [],
|
|
47
48
|
archiveCalls: [],
|
|
49
|
+
unarchiveCalls: [],
|
|
48
50
|
archiveSession: async (id) => {
|
|
49
51
|
const sid = String(id)
|
|
50
52
|
registry.archiveCalls.push(sid)
|
|
51
53
|
registry.state.archivedSessionIds = [...registry.state.archivedSessionIds, sid]
|
|
52
54
|
},
|
|
55
|
+
unarchiveSession: async (id) => {
|
|
56
|
+
const sid = String(id)
|
|
57
|
+
const next = registry.state.archivedSessionIds.filter((existing) => String(existing) !== sid)
|
|
58
|
+
if (next.length === registry.state.archivedSessionIds.length) return
|
|
59
|
+
registry.unarchiveCalls.push(sid)
|
|
60
|
+
registry.state.archivedSessionIds = next
|
|
61
|
+
},
|
|
53
62
|
}
|
|
54
63
|
Object.defineProperty(registry, 'archivedSessionIds', {
|
|
55
64
|
get: () => registry.state.archivedSessionIds,
|
|
@@ -399,17 +408,17 @@ test('多工作区 detach:首个失败不终止后续,全部失败进日志', sk
|
|
|
399
408
|
}
|
|
400
409
|
})
|
|
401
410
|
|
|
402
|
-
test('
|
|
403
|
-
const domain = makeDomain(['s1'])
|
|
404
|
-
const { handlers, registry } = makeCtx({ archivedIds: [], headers: [HEADER], agents: new Map(), domain })
|
|
411
|
+
test('取消归档路由:走官方 unarchiveSession 写穿注册表', skipMissingDeps, async () => {
|
|
412
|
+
const domain = makeDomain(['s1', 's2'])
|
|
413
|
+
const { handlers, registry } = makeCtx({ archivedIds: ['s1', 's2'], headers: [HEADER], agents: new Map(), domain })
|
|
405
414
|
const res = response()
|
|
406
415
|
await handlers.get('/api/session-manager/unarchive')(request('s1'), res)
|
|
407
416
|
assert.equal(res.status, 200)
|
|
408
417
|
assert.deepEqual(res.body, { ok: true })
|
|
409
|
-
//
|
|
410
|
-
assert.deepEqual(
|
|
411
|
-
assert.
|
|
412
|
-
assert.deepEqual(registry.
|
|
418
|
+
// 官方 API 写穿:注册表快照移除,无私有直写路径
|
|
419
|
+
assert.deepEqual(registry.unarchiveCalls, ['s1'])
|
|
420
|
+
assert.deepEqual(registry.state.archivedSessionIds, ['s2'])
|
|
421
|
+
assert.deepEqual(registry.archivedSessionIds, ['s2'])
|
|
413
422
|
})
|
|
414
423
|
|
|
415
424
|
test('台账 deletedAt 为删除时刻的数值(面板排序依据)', skipMissingDeps, async () => {
|
|
@@ -435,52 +444,16 @@ test('台账 deletedAt 为删除时刻的数值(面板排序依据)', skipMissin
|
|
|
435
444
|
}
|
|
436
445
|
})
|
|
437
446
|
|
|
438
|
-
test('
|
|
439
|
-
const
|
|
440
|
-
const { handlers, registry } = makeCtx({ archivedIds: ['s1'], headers: [HEADER], agents: new Map(), domain })
|
|
441
|
-
const res = response()
|
|
442
|
-
await handlers.get('/api/session-manager/unarchive')(request('s1'), res)
|
|
443
|
-
assert.equal(res.status, 200)
|
|
444
|
-
assert.deepEqual(res.body, { ok: true })
|
|
445
|
-
assert.deepEqual(domain.state.archivedSessionIds, ['s2'])
|
|
446
|
-
assert.equal(domain.writes, 1)
|
|
447
|
-
// 复活守卫:注册表进程内快照必须同步移除,否则官方全量写回与 list 基线会恢复该 id
|
|
448
|
-
assert.deepEqual(registry.state.archivedSessionIds, ['s2'])
|
|
449
|
-
assert.deepEqual(registry.archivedSessionIds, ['s2'])
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
test('取消归档路由:workspace 域未打开时拒绝', skipMissingDeps, async () => {
|
|
453
|
-
const { handlers } = makeCtx({ archivedIds: ['s1'], headers: [HEADER], agents: new Map(), domain: null })
|
|
454
|
-
const res = response()
|
|
455
|
-
await handlers.get('/api/session-manager/unarchive')(request('s1'), res)
|
|
456
|
-
assert.equal(res.status, 400)
|
|
457
|
-
assert.equal(res.body.error, 'workspace 域未打开')
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
test('取消归档路由:未归档 id 幂等跳过,不产生写回', skipMissingDeps, async () => {
|
|
461
|
-
const domain = makeDomain(['s1'])
|
|
462
|
-
const { handlers, registry } = makeCtx({ archivedIds: ['s1'], headers: [HEADER], agents: new Map(), domain })
|
|
447
|
+
test('取消归档路由:未归档 id 幂等,注册表零写', skipMissingDeps, async () => {
|
|
448
|
+
const { handlers, registry } = makeCtx({ archivedIds: ['s1'], headers: [HEADER], agents: new Map() })
|
|
463
449
|
const res = response()
|
|
464
450
|
await handlers.get('/api/session-manager/unarchive')(request('s9'), res)
|
|
465
451
|
assert.equal(res.status, 200)
|
|
466
452
|
assert.deepEqual(res.body, { ok: true })
|
|
467
|
-
assert.deepEqual(
|
|
468
|
-
assert.equal(domain.writes, 0)
|
|
453
|
+
assert.deepEqual(registry.unarchiveCalls, [])
|
|
469
454
|
assert.deepEqual(registry.state.archivedSessionIds, ['s1'])
|
|
470
455
|
})
|
|
471
456
|
|
|
472
|
-
test('取消归档路由:快照领先域时补全清理(重试路径)', skipMissingDeps, async () => {
|
|
473
|
-
const domain = makeDomain([])
|
|
474
|
-
const { handlers, registry } = makeCtx({ archivedIds: ['s1'], headers: [HEADER], agents: new Map(), domain })
|
|
475
|
-
const res = response()
|
|
476
|
-
await handlers.get('/api/session-manager/unarchive')(request('s1'), res)
|
|
477
|
-
assert.equal(res.status, 200)
|
|
478
|
-
assert.deepEqual(res.body, { ok: true })
|
|
479
|
-
assert.deepEqual(domain.state.archivedSessionIds, [])
|
|
480
|
-
assert.equal(domain.writes, 1)
|
|
481
|
-
assert.deepEqual(registry.state.archivedSessionIds, [])
|
|
482
|
-
})
|
|
483
|
-
|
|
484
457
|
test('取消归档宽限:恢复后 graceMs 内同进程评估暂停,不秒归档', skipMissingDeps, async () => {
|
|
485
458
|
const dir = await mkdtemp(path.join(tmpdir(), 'sm-grace-'))
|
|
486
459
|
try {
|
|
@@ -1077,7 +1050,7 @@ test('删除成功:trash 目标为会话目录,台账记录目录路径', skipMi
|
|
|
1077
1050
|
try {
|
|
1078
1051
|
const domain = makeDomain(['s1'])
|
|
1079
1052
|
const workspace = makeWorkspace('C:\\x', ['s1'])
|
|
1080
|
-
const { handlers, ledger } = makeCtx({
|
|
1053
|
+
const { handlers, ledger, registry } = makeCtx({
|
|
1081
1054
|
archivedIds: ['s1'],
|
|
1082
1055
|
headers: [HEADER],
|
|
1083
1056
|
agents: IDLE_S1,
|
|
@@ -1097,7 +1070,8 @@ test('删除成功:trash 目标为会话目录,台账记录目录路径', skipMi
|
|
|
1097
1070
|
assert.equal(ledger.state.deleted.length, 1)
|
|
1098
1071
|
assert.equal(ledger.state.deleted[0].sessionId, 's1')
|
|
1099
1072
|
assert.equal(ledger.state.deleted[0].path, artifact.locatedDir)
|
|
1100
|
-
|
|
1073
|
+
// 取消归档走官方 unarchiveSession:注册表快照为事实源,域不再被插件直写
|
|
1074
|
+
assert.deepEqual(registry.state.archivedSessionIds, [])
|
|
1101
1075
|
assert.deepEqual(workspace.sessionIds, [])
|
|
1102
1076
|
} finally {
|
|
1103
1077
|
await artifact.cleanup()
|
|
@@ -1251,17 +1225,18 @@ test('删除:产物已缺失时运行中守卫仍然生效', skipMissingDeps, as
|
|
|
1251
1225
|
assert.equal(res.body.error, MESSAGES.running)
|
|
1252
1226
|
})
|
|
1253
1227
|
|
|
1254
|
-
test('
|
|
1228
|
+
test('删除:产物已缺失且归档清理半失败时聚合失败点', skipMissingDeps, async () => {
|
|
1255
1229
|
const workspace = makeWorkspace('C:\\x', ['s1'], { detachError: new Error('detach boom') })
|
|
1256
|
-
const { handlers, logger } = makeCtx({
|
|
1257
|
-
archivedIds: [],
|
|
1230
|
+
const { handlers, logger, registry } = makeCtx({
|
|
1231
|
+
archivedIds: ['s1'],
|
|
1258
1232
|
headers: [HEADER],
|
|
1259
1233
|
agents: IDLE_S1,
|
|
1260
|
-
domain: null,
|
|
1261
1234
|
sessionPersistence: { locate: (header) => header.id === 's1' ? { path: GHOST_PATH } : undefined },
|
|
1262
1235
|
ledger: makeLedgerDomain([{ sessionId: 's1', path: GHOST_DIR, deletedAt: 1 }]),
|
|
1263
1236
|
workspaces: [workspace],
|
|
1264
1237
|
})
|
|
1238
|
+
// 官方取消归档半失败:注入 unarchiveSession 抛错
|
|
1239
|
+
registry.unarchiveSession = async () => { throw new Error('unarchive boom') }
|
|
1265
1240
|
const res = response()
|
|
1266
1241
|
await withTrashStub(async () => {}, async () => {
|
|
1267
1242
|
await handlers.get('/api/session-manager/delete')(request('s1'), res)
|
|
@@ -1323,16 +1298,16 @@ test('归档评估:超期运行中会话零产物 IO(早退,core 过滤为第二
|
|
|
1323
1298
|
assert.deepEqual(registry.archiveCalls, [])
|
|
1324
1299
|
})
|
|
1325
1300
|
|
|
1326
|
-
test('
|
|
1301
|
+
test('删除:取消归档半失败时响应 partial 且台账已记录', skipMissingDeps, async () => {
|
|
1327
1302
|
const artifact = await makeLocatedArtifact()
|
|
1328
1303
|
try {
|
|
1329
|
-
const { handlers, ledger } = makeCtx({
|
|
1304
|
+
const { handlers, ledger, registry } = makeCtx({
|
|
1330
1305
|
archivedIds: ['s1'],
|
|
1331
1306
|
headers: [HEADER],
|
|
1332
1307
|
agents: IDLE_S1,
|
|
1333
|
-
domain: null,
|
|
1334
1308
|
sessionPersistence: { locate: (header) => header.id === 's1' ? { path: artifact.locatedPath } : undefined },
|
|
1335
1309
|
})
|
|
1310
|
+
registry.unarchiveSession = async () => { throw new Error('unarchive boom') }
|
|
1336
1311
|
const res = response()
|
|
1337
1312
|
await withTrashStub(async () => {}, async () => {
|
|
1338
1313
|
await handlers.get('/api/session-manager/delete')(request('s1'), res)
|
|
@@ -1383,12 +1358,10 @@ test('删除:detach 与台账同时失败时,partial 消息聚合两个失败点
|
|
|
1383
1358
|
test('台账域打开失败:删除其余成功仍响应 partial', skipMissingDeps, async () => {
|
|
1384
1359
|
const artifact = await makeLocatedArtifact()
|
|
1385
1360
|
try {
|
|
1386
|
-
const
|
|
1387
|
-
const { handlers } = makeCtx({
|
|
1361
|
+
const { handlers, registry } = makeCtx({
|
|
1388
1362
|
archivedIds: ['s1'],
|
|
1389
1363
|
headers: [HEADER],
|
|
1390
1364
|
agents: IDLE_S1,
|
|
1391
|
-
domain,
|
|
1392
1365
|
sessionPersistence: { locate: (header) => header.id === 's1' ? { path: artifact.locatedPath } : undefined },
|
|
1393
1366
|
openRejected: new Error('ledger down'),
|
|
1394
1367
|
})
|
|
@@ -1399,8 +1372,8 @@ test('台账域打开失败:删除其余成功仍响应 partial', skipMissingDep
|
|
|
1399
1372
|
assert.equal(res.status, 200)
|
|
1400
1373
|
assert.equal(res.body.partial, true)
|
|
1401
1374
|
assert.equal(res.body.message, '已移入回收站,但重挂载记录失败')
|
|
1402
|
-
//
|
|
1403
|
-
assert.deepEqual(
|
|
1375
|
+
// 台账失败不阻断删除主链路:归档清理经官方 API 照常完成
|
|
1376
|
+
assert.deepEqual(registry.state.archivedSessionIds, [])
|
|
1404
1377
|
} finally {
|
|
1405
1378
|
await artifact.cleanup()
|
|
1406
1379
|
}
|