dsh-workspace-mover 2.0.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/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "dsh-workspace-mover",
3
+ "version": "2.0.3",
4
+ "description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史和一键撤回。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./client/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "client",
15
+ "test",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "docs/ARCHITECTURE.md",
19
+ "docs/media",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs",
24
+ "check:syntax": "node --check lib/index.js && node --check client/client.js",
25
+ "check:pack": "npm pack --dry-run",
26
+ "check": "npm run check:syntax && npm test && npm run check:pack"
27
+ },
28
+ "keywords": [
29
+ "deepseek-harness",
30
+ "dsh",
31
+ "dsh-plugin",
32
+ "workspace",
33
+ "session",
34
+ "session-migration",
35
+ "drag-and-drop"
36
+ ],
37
+ "engines": {
38
+ "node": ">=22",
39
+ "dsh": ">=0.1.5-rc.1"
40
+ },
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/PianoPrince/dsh-workspace-mover.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/PianoPrince/dsh-workspace-mover/issues"
48
+ },
49
+ "homepage": "https://github.com/PianoPrince/dsh-workspace-mover#readme",
50
+ "dsh": {
51
+ "bundle": {
52
+ "patch": "./cordis.patch.yml"
53
+ },
54
+ "client": {
55
+ "inject": [
56
+ "@deepseek-ai/dsh-client-connection",
57
+ "@deepseek-ai/dsh-client-ui-slots"
58
+ ],
59
+ "platform": "web"
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,124 @@
1
+ // 核心纯函数单元测试:帧扫描 / 头部读写 / 存储路径编码。
2
+ // 运行:npm test(或 node --test test/)
3
+ import { test } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { zstdCompressSync, constants } from 'node:zlib';
6
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { scanFrames, readHeader, rewriteHeaderCwd, generationLogFilename, parseGenerationLogFilename, findHighestArtifact, artifactPath, sessionDir } from '../lib/index.js';
10
+
11
+ const OPTS = { params: { [constants.ZSTD_c_checksumFlag]: 1 } };
12
+
13
+ /** 构造一个「头 + N 个事件帧」的合成档案。 */
14
+ function makeArtifact(header, events) {
15
+ const head = zstdCompressSync(Buffer.from(JSON.stringify(header) + '\n', 'utf8'), OPTS);
16
+ const rest = events.map((e) => zstdCompressSync(Buffer.from(JSON.stringify(e) + '\n', 'utf8'), OPTS));
17
+ return Buffer.concat([head, ...rest]);
18
+ }
19
+
20
+ test('scanFrames 定位全部帧边界', () => {
21
+ const buf = makeArtifact({ type: 'session', id: 's1', cwd: 'E:\\a' }, [{ seq: 1 }, { seq: 2 }]);
22
+ const frames = scanFrames(buf);
23
+ assert.equal(frames.length, 3);
24
+ for (const [s, e] of frames) assert.ok(e > s && e <= buf.length);
25
+ });
26
+
27
+ test('readHeader 解出首行会话头', () => {
28
+ const buf = makeArtifact({ type: 'session', id: 'session-x', cwd: 'C:\\work' }, [{ seq: 1 }]);
29
+ const header = readHeader(buf);
30
+ assert.equal(header.type, 'session');
31
+ assert.equal(header.id, 'session-x');
32
+ assert.equal(header.cwd, 'C:\\work');
33
+ });
34
+
35
+ test('rewriteHeaderCwd 只改头部,其余帧字节不变', () => {
36
+ const ev = [{ seq: 1, text: 'hello 世界' }, { seq: 2 }];
37
+ const original = makeArtifact({ type: 'session', id: 'session-y', cwd: 'E:\\old\\path' }, ev);
38
+ const rewritten = rewriteHeaderCwd(original, 'E:\\new\\home');
39
+ // 头部已更新
40
+ const header = readHeader(rewritten);
41
+ assert.equal(header.cwd, 'E:\\new\\home');
42
+ assert.equal(header.id, 'session-y');
43
+ // 其余事件帧逐字节保留
44
+ const of = scanFrames(original), nf = scanFrames(rewritten);
45
+ assert.equal(nf.length, of.length);
46
+ assert.ok(of.slice(1).every(([, e], i) => original.subarray(of[i + 1][0], e).equals(rewritten.subarray(nf[i + 1][0], nf[i + 1][1]))));
47
+ });
48
+
49
+ test('损坏档案被拒绝而不是静默通过', () => {
50
+ const buf = makeArtifact({ type: 'session', id: 's', cwd: 'x' }, []);
51
+ buf.writeUInt32LE(0x12345678, 8); // 破坏第一帧内部
52
+ assert.throws(() => readHeader(buf));
53
+ });
54
+
55
+ test('readHeader and rewriteHeaderCwd support uncompressed JSONL', () => {
56
+ const original = Buffer.from(
57
+ JSON.stringify({ type: 'session', id: 'session-raw', cwd: 'E:\\old' }) + '\n' +
58
+ JSON.stringify({ seq: 1, text: 'raw event' }) + '\n',
59
+ 'utf8'
60
+ );
61
+ assert.equal(readHeader(original, 'none').cwd, 'E:\\old');
62
+ const rewritten = rewriteHeaderCwd(original, 'E:\\new', 'none');
63
+ assert.equal(readHeader(rewritten, 'none').cwd, 'E:\\new');
64
+ const secondLine = rewritten.indexOf(0x0a) + 1;
65
+ assert.equal(rewritten.subarray(secondLine).toString('utf8'), JSON.stringify({ seq: 1, text: 'raw event' }) + '\n');
66
+ });
67
+
68
+
69
+ test('generationLogFilename prefers v3 zstd names', () => {
70
+ assert.equal(generationLogFilename(0, 'zstd'), 'session.jsonl.zstd');
71
+ assert.equal(generationLogFilename(3, 'zstd'), 'session.v3.jsonl.zstd');
72
+ assert.equal(parseGenerationLogFilename('session.jsonl.zstd')?.version, 0);
73
+ assert.equal(parseGenerationLogFilename('session.v3.jsonl.zstd')?.version, 3);
74
+ assert.equal(parseGenerationLogFilename('session.migration.tmp.jsonl.zstd'), null);
75
+ });
76
+
77
+ test('findHighestArtifact picks the newest canonical generation', () => {
78
+ const root = mkdtempSync(join(tmpdir(), 'wsm-gen-'));
79
+ try {
80
+ const dir = join(root, 'sess');
81
+ mkdirSync(dir, { recursive: true });
82
+ writeFileSync(join(dir, 'session.jsonl.zstd'), Buffer.from('old'));
83
+ writeFileSync(join(dir, 'session.v2.jsonl.zstd'), Buffer.from('mid'));
84
+ writeFileSync(join(dir, 'session.v3.jsonl.zstd'), Buffer.from('new'));
85
+ writeFileSync(join(dir, 'session.migration.abc.jsonl.zstd'), Buffer.from('tmp'));
86
+ const found = findHighestArtifact(dir);
87
+ assert.equal(found?.version, 3);
88
+ assert.equal(found?.name, 'session.v3.jsonl.zstd');
89
+ assert.equal(artifactPath(root, 'E:\\proj', 'session-x'), join(sessionDir(root, 'E:\\proj', 'session-x'), 'session.v3.jsonl.zstd'));
90
+ } finally {
91
+ rmSync(root, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ test('findHighestArtifact supports the official uncompressed encoding', () => {
96
+ const root = mkdtempSync(join(tmpdir(), 'wsm-raw-'));
97
+ try {
98
+ const dir = join(root, 'sess');
99
+ mkdirSync(dir, { recursive: true });
100
+ writeFileSync(join(dir, 'session.v3.jsonl'), Buffer.from('{"type":"session"}\n'));
101
+ const found = findHighestArtifact(dir, 'none');
102
+ assert.equal(found?.name, 'session.v3.jsonl');
103
+ assert.equal(artifactPath(root, 'E:\\proj', 'session-x', 'none'), join(sessionDir(root, 'E:\\proj', 'session-x'), 'session.v3.jsonl'));
104
+ } finally {
105
+ rmSync(root, { recursive: true, force: true });
106
+ }
107
+ });
108
+
109
+ test('mixed session compression encodings are rejected explicitly', () => {
110
+ const root = mkdtempSync(join(tmpdir(), 'wsm-mixed-'));
111
+ try {
112
+ const dir = join(root, 'sess');
113
+ mkdirSync(dir, { recursive: true });
114
+ writeFileSync(join(dir, 'session.v3.jsonl'), Buffer.from('{}\n'));
115
+ writeFileSync(join(dir, 'session.v3.jsonl.zstd'), Buffer.from('not-zstd'));
116
+ assert.throws(() => findHighestArtifact(dir, 'zstd'), (err) => {
117
+ assert.equal(err.code, 'unsupported');
118
+ assert.match(err.message, /mixed session compression encodings/);
119
+ return true;
120
+ });
121
+ } finally {
122
+ rmSync(root, { recursive: true, force: true });
123
+ }
124
+ });