@mzzsfy/dsh-session-manager 0.7.1 → 0.9.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 +17 -28
- package/package.json +1 -1
- package/src/client.js +805 -1160
- package/src/core.mjs +262 -344
- package/src/index.js +851 -1137
- package/src/trash.mjs +40 -2
- package/test/client-delete.test.mjs +46 -0
- package/test/client-help.test.mjs +36 -0
- package/test/client-id.test.mjs +8 -0
- package/test/core.test.mjs +485 -595
- package/test/index.test.mjs +1730 -1968
- package/test/parity.test.mjs +0 -11
- package/test/trash.test.mjs +73 -2
- package/src/history-cache.mjs +0 -127
- package/test/history-cache.test.mjs +0 -84
package/src/trash.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// 处置:跨平台把文件或目录移入系统回收站;系统回收站不可用的环境(容器无
|
|
2
|
+
// gio / dbus 等)降级为插件回收区(rename 搬移,跨设备回退复制),保持可还原。
|
|
3
|
+
// 执行器与 fs 可注入以便测试;失败按原样抛出,由调用方按失败矩阵处理。
|
|
3
4
|
|
|
4
5
|
import { execFile } from 'node:child_process'
|
|
5
6
|
import { promisify } from 'node:util'
|
|
7
|
+
import { cp, mkdir, rename, rm } from 'node:fs/promises'
|
|
8
|
+
import { basename, join } from 'node:path'
|
|
6
9
|
|
|
7
10
|
const TRASH_TIMEOUT_MS = 60 * 1000
|
|
8
11
|
const execFilep = promisify(execFile)
|
|
@@ -47,3 +50,38 @@ export async function trashPath(path, options = {}) {
|
|
|
47
50
|
const env = command.env ? { ...process.env, ...command.env } : undefined
|
|
48
51
|
await run(command.file, command.args, { timeout: timeoutMs, env })
|
|
49
52
|
}
|
|
53
|
+
|
|
54
|
+
// 同一文件系统内 rename 即搬移;跨设备(容器卷挂载等)EXDEV 时复制后删除原路径
|
|
55
|
+
async function movePath(from, to, fsImpl) {
|
|
56
|
+
try {
|
|
57
|
+
await fsImpl.rename(from, to)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!error || error.code !== 'EXDEV') throw error
|
|
60
|
+
await fsImpl.cp(from, to, { recursive: true })
|
|
61
|
+
await fsImpl.rm(from, { recursive: true, force: true })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const REAL_FS = { rename, cp, rm, mkdir }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 把目录搬入插件回收区,返回暂存路径;同名基名经时间戳保证唯一。
|
|
69
|
+
* @param options.fs - 注入的 fs 操作(rename/cp/rm/mkdir),默认真实实现
|
|
70
|
+
* @param options.now - 注入的时钟,默认 Date.now
|
|
71
|
+
*/
|
|
72
|
+
export async function moveToQuarantine(path, quarantineDir, options = {}) {
|
|
73
|
+
const { fs: fsImpl = REAL_FS, now = Date.now } = options
|
|
74
|
+
await fsImpl.mkdir(quarantineDir, { recursive: true })
|
|
75
|
+
const heldPath = join(quarantineDir, basename(path) + '-' + now())
|
|
76
|
+
await movePath(path, heldPath, fsImpl)
|
|
77
|
+
return heldPath
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 把回收区暂存的目录移回原位置;原位置已被占用时失败上抛,由调用方响应。
|
|
82
|
+
* @param options.fs - 注入的 fs 操作(rename/cp/rm/mkdir),默认真实实现
|
|
83
|
+
*/
|
|
84
|
+
export async function restoreFromQuarantine(heldPath, originalPath, options = {}) {
|
|
85
|
+
const { fs: fsImpl = REAL_FS } = options
|
|
86
|
+
await movePath(heldPath, originalPath, fsImpl)
|
|
87
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// 删除确认链与强提示契约:删除 = 两击行内确认 + 弹窗强提示,弹窗内确认才发请求。
|
|
2
|
+
// 文案与行为同源锁定,防重构丢失强提示使用户误删。
|
|
3
|
+
|
|
4
|
+
import { test } from 'node:test'
|
|
5
|
+
import assert from 'node:assert/strict'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const CLIENT_SRC = readFileSync(fileURLToPath(new URL('../src/client.js', import.meta.url)), 'utf8')
|
|
10
|
+
|
|
11
|
+
test('源码契约:删除按钮悬停强提示齐备', () => {
|
|
12
|
+
assert.ok(CLIENT_SRC.includes('会影响其他插件'), '删除提示缺其他插件影响声明')
|
|
13
|
+
assert.ok(CLIENT_SRC.includes('若无特殊需求,请不要删除'), '删除提示缺"若无特殊需求不要删除"劝阻语')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Given 弹窗确认层, When 渲染, Then 强提示文案、会话标题、双按钮齐备且受控挂载
|
|
17
|
+
test('源码契约:删除弹窗强提示与受控挂载齐备', () => {
|
|
18
|
+
assert.ok(CLIENT_SRC.includes('sm-dialog'), '缺弹窗样式锚点')
|
|
19
|
+
assert.ok(CLIENT_SRC.includes('DELETE_DIALOG_TITLE'), '缺弹窗标题常量')
|
|
20
|
+
assert.ok(CLIENT_SRC.includes('DELETE_DIALOG_BODY'), '缺弹窗正文常量')
|
|
21
|
+
assert.ok(/DELETE_DIALOG_BODY[\s\S]{0,60}row\.title/.test(CLIENT_SRC), '弹窗正文未携带会话标题')
|
|
22
|
+
assert.ok(CLIENT_SRC.includes("h(DeleteDialog"), '弹窗组件未在应用内挂载')
|
|
23
|
+
assert.ok(CLIENT_SRC.includes('pendingDelete'), '缺待删会话受控状态')
|
|
24
|
+
assert.ok(/pendingDelete[\s\S]{0,80}setPendingDelete\(null\)/.test(CLIENT_SRC), '弹窗取消未清空待删状态')
|
|
25
|
+
// 可达性:aria 关联 + 初始焦点落取消 + Escape 出口
|
|
26
|
+
assert.ok(CLIENT_SRC.includes("'aria-labelledby'"), '弹窗缺 aria-labelledby')
|
|
27
|
+
assert.ok(CLIENT_SRC.includes("'aria-describedby'"), '弹窗缺 aria-describedby')
|
|
28
|
+
assert.ok(/cancelRef\.current\.focus\(\)/.test(CLIENT_SRC), '弹窗初始焦点未落取消键')
|
|
29
|
+
assert.ok(/key === 'Escape'/.test(CLIENT_SRC), '弹窗缺 Escape 取消出口')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Given 弹窗确认, When 确认删除, Then 才发删除请求;成功文案按处置模式区分
|
|
33
|
+
test('源码契约:弹窗确认触发请求且成功文案区分处置模式', () => {
|
|
34
|
+
assert.ok(CLIENT_SRC.includes('DELETE_TOAST_OS'), '缺 OS 回收站成功文案常量')
|
|
35
|
+
assert.ok(CLIENT_SRC.includes('DELETE_TOAST_QUARANTINE'), '缺回收区成功文案常量')
|
|
36
|
+
assert.ok(CLIENT_SRC.includes("mode === 'quarantine'"), '成功文案未按响应 mode 区分')
|
|
37
|
+
assert.ok(CLIENT_SRC.includes('DELETE_URL'), '缺删除请求地址')
|
|
38
|
+
// 弹窗确认函数与删除请求联动:确认键处理体内发出 DELETE 请求
|
|
39
|
+
assert.ok(/function onDialogConfirm[\s\S]{0,700}DELETE_URL/.test(CLIENT_SRC), '弹窗确认未触发删除请求')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Given 已删除区, When 渲染提示, Then 两种找回路径(OS 还原 / 回收区直接重挂载)均有说明
|
|
43
|
+
test('源码契约:已删除区说明覆盖两种找回路径', () => {
|
|
44
|
+
assert.ok(CLIENT_SRC.includes('到系统回收站将会话文件夹还原到原位置'), '缺 OS 回收站还原指引')
|
|
45
|
+
assert.ok(CLIENT_SRC.includes('回收区暂存的会话直接点「重新挂载」即可找回'), '缺回收区直接重挂载指引')
|
|
46
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
|
|
6
|
+
const CLIENT_SRC = readFileSync(fileURLToPath(new URL('../src/client.js', import.meta.url)), 'utf8')
|
|
7
|
+
|
|
8
|
+
// 设置项悬停说明契约:说明常量必须存在且逐项挂到对应控件(title 属性)。
|
|
9
|
+
// 开关行断言以常量为锚回看相邻的行文本,配置行断言以展示文案为锚——
|
|
10
|
+
// 防常量互换挂载或挂错行仍通过(纯存在性匹配检不出错挂)
|
|
11
|
+
test('源码契约:自动归档设置的悬停说明齐备且挂到对应控件', () => {
|
|
12
|
+
const TITLES = {
|
|
13
|
+
ARCHIVE_OVERVIEW_TITLE: '自动归档行',
|
|
14
|
+
ARCHIVE_DAYS_TITLE: '阈值字段',
|
|
15
|
+
ARCHIVE_INTERVAL_TITLE: '检查周期字段',
|
|
16
|
+
}
|
|
17
|
+
for (const [name, label] of Object.entries(TITLES)) {
|
|
18
|
+
assert.ok(CLIENT_SRC.includes('const ' + name + ' ='), '缺悬停说明常量 ' + name)
|
|
19
|
+
}
|
|
20
|
+
const configAnchors = [
|
|
21
|
+
{ constant: 'ARCHIVE_OVERVIEW_TITLE', text: "'自动归档'", label: TITLES.ARCHIVE_OVERVIEW_TITLE },
|
|
22
|
+
{ constant: 'ARCHIVE_DAYS_TITLE', text: "'阈值', numberInput('days')", label: TITLES.ARCHIVE_DAYS_TITLE },
|
|
23
|
+
{ constant: 'ARCHIVE_INTERVAL_TITLE', text: "'检查周期', numberInput('intervalHours')", label: TITLES.ARCHIVE_INTERVAL_TITLE },
|
|
24
|
+
]
|
|
25
|
+
for (const { constant, text, label } of configAnchors) {
|
|
26
|
+
const mounted = new RegExp('title: ' + constant + '[\\s\\S]{0,120}' + text.replace(/[()']/g, '\\$&')).test(CLIENT_SRC)
|
|
27
|
+
assert.ok(mounted, label + ' 未挂对应悬停说明(常量与字段不在同一配置行)')
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// 说明文案与行为同源的抽样锁定:核心语义缺失即为文案回归
|
|
32
|
+
test('源码契约:悬停说明覆盖各设置的关键行为语义', () => {
|
|
33
|
+
assert.ok(CLIENT_SRC.includes('空白(无消息)会话不参与归档'), '阈值说明缺空白会话例外(core.mjs selectArchiveCandidates 过滤 blank)')
|
|
34
|
+
assert.ok(CLIENT_SRC.includes('0 = 关闭自动归档'), '阈值说明缺 0 关闭语义')
|
|
35
|
+
assert.ok(CLIENT_SRC.includes('0 = 关闭周期检查'), '周期说明缺 0 关闭语义')
|
|
36
|
+
})
|
package/test/client-id.test.mjs
CHANGED
|
@@ -13,4 +13,12 @@ test('client.js 注册 id 为完整包名', () => {
|
|
|
13
13
|
const match = source.match(/__ModuleLoader__\.load\(\{\s*id:\s*'([^']+)'/)
|
|
14
14
|
assert.ok(match, 'client.js 缺少 __ModuleLoader__.load 注册')
|
|
15
15
|
assert.equal(match[1], name)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('client.js 样式注入自带 data-plugin', () => {
|
|
19
|
+
const source = readFileSync(join(PKG_ROOT, 'src', 'client.js'), 'utf8')
|
|
20
|
+
const injections = source.split("createElement('style')").length - 1
|
|
21
|
+
assert.ok(injections > 0, 'client.js 无样式注入点')
|
|
22
|
+
const marked = source.split(`setAttribute('data-plugin', '${name}')`).length - 1
|
|
23
|
+
assert.equal(marked, injections, '每个样式注入点都必须自带 data-plugin(值=注册包名),防宿主 claimStyles 误归属后随他插件 HMR 整批误删')
|
|
16
24
|
})
|