dsh-workspace-mover 2.3.0 → 2.4.1
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/CHANGELOG.md +32 -0
- package/README.md +25 -9
- package/README_EN.md +23 -9
- package/client/client.js +80 -15
- package/docs/ARCHITECTURE.md +224 -214
- package/docs/compatibility/dsh-0.2.1-rc.2-2026-10-18.md +37 -0
- package/docs/compatibility/issue-5-desktop-acceptance-2026-10-22.md +106 -0
- package/docs/compatibility/pr-review-2026-10-22.md +56 -0
- package/lib/index.js +4507 -4263
- package/package.json +3 -2
- package/test/client-dom.test.mjs +79 -0
- package/test/e2e-sandbox.test.mjs +2724 -2607
- package/test/host-services.test.mjs +303 -0
- package/test/installed-host.test.mjs +119 -0
- package/test/workspace-tools.test.mjs +977 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workspace-mover",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"docs/media",
|
|
23
23
|
"LICENSE"
|
|
24
24
|
], "scripts": {
|
|
25
|
-
"test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/policy.test.mjs test/client-dom.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs",
|
|
25
|
+
"test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/policy.test.mjs test/client-dom.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs test/workspace-tools.test.mjs test/host-services.test.mjs",
|
|
26
|
+
"test:installed-host": "node --test test/installed-host.test.mjs",
|
|
26
27
|
"check:syntax": "node --check lib/index.js && node --check client/client.js",
|
|
27
28
|
"check:pack": "npm pack --dry-run",
|
|
28
29
|
"check": "npm run check:syntax && npm test && npm run check:pack"
|
package/test/client-dom.test.mjs
CHANGED
|
@@ -100,6 +100,7 @@ function createElement(tag) {
|
|
|
100
100
|
tagName: String(tag).toUpperCase(),
|
|
101
101
|
childNodes: [],
|
|
102
102
|
parentNode: null,
|
|
103
|
+
get parentElement() { return this.parentNode?.nodeType === 1 ? this.parentNode : null; },
|
|
103
104
|
className: '',
|
|
104
105
|
id: '',
|
|
105
106
|
style: {},
|
|
@@ -297,12 +298,26 @@ const reactShim = {
|
|
|
297
298
|
|
|
298
299
|
let mod = null;
|
|
299
300
|
const rpcCalls = [];
|
|
301
|
+
const animationFrames = new Map();
|
|
302
|
+
let animationFrameId = 0;
|
|
303
|
+
function frame() {
|
|
304
|
+
const pending = [...animationFrames.values()];
|
|
305
|
+
animationFrames.clear();
|
|
306
|
+
for (const callback of pending) callback();
|
|
307
|
+
}
|
|
300
308
|
let rpcResponder = () => ({ ok: true, value: {} });
|
|
301
309
|
|
|
302
310
|
function setResponder(fn) { rpcResponder = fn; }
|
|
303
311
|
|
|
304
312
|
before(async () => {
|
|
305
313
|
globalThis.document = doc;
|
|
314
|
+
globalThis.getComputedStyle = (el) => ({ overflowY: el.style.overflowY ?? 'visible' });
|
|
315
|
+
globalThis.requestAnimationFrame = (callback) => {
|
|
316
|
+
const id = ++animationFrameId;
|
|
317
|
+
animationFrames.set(id, callback);
|
|
318
|
+
return id;
|
|
319
|
+
};
|
|
320
|
+
globalThis.cancelAnimationFrame = (id) => animationFrames.delete(id);
|
|
306
321
|
globalThis.MutationObserver = MutationObserverStub;
|
|
307
322
|
globalThis.Node = { ELEMENT_NODE: 1 };
|
|
308
323
|
globalThis.Element = function Element() {};
|
|
@@ -371,6 +386,7 @@ function unmount() {
|
|
|
371
386
|
|
|
372
387
|
function resetDom() {
|
|
373
388
|
unmount();
|
|
389
|
+
animationFrames.clear();
|
|
374
390
|
doc._body.childNodes = [];
|
|
375
391
|
doc._listeners = [];
|
|
376
392
|
rpcCalls.length = 0;
|
|
@@ -498,6 +514,44 @@ test('未卸载就重复装载时不得叠加第二份监听器(防重复实
|
|
|
498
514
|
`重复 apply 不得叠加:${afterFirst} -> ${doc._listeners.length}`);
|
|
499
515
|
});
|
|
500
516
|
|
|
517
|
+
test('drag autoscroll stops outside sidebar, at bounds, on dragleave/drop/dragend and unload', () => {
|
|
518
|
+
resetDom();
|
|
519
|
+
mount();
|
|
520
|
+
const row = sessionRow('Scroll test', 'session-aaa');
|
|
521
|
+
const scroller = createElement('div');
|
|
522
|
+
scroller.style.overflowY = 'auto';
|
|
523
|
+
Object.assign(scroller, { scrollHeight: 1000, clientHeight: 300, scrollTop: 100,
|
|
524
|
+
getBoundingClientRect: () => ({ left: 0, right: 200, top: 0, bottom: 300 }) });
|
|
525
|
+
scroller.appendChild(row);
|
|
526
|
+
attach(scroller);
|
|
527
|
+
const start = (x = 100, y = 290) => {
|
|
528
|
+
dispatch({ type: 'dragstart', target: row });
|
|
529
|
+
dispatch({ type: 'dragover', target: row, clientX: x, clientY: y });
|
|
530
|
+
};
|
|
531
|
+
start(); frame();
|
|
532
|
+
assert.equal(scroller.scrollTop, 112);
|
|
533
|
+
dispatch({ type: 'dragover', target: row, clientX: 400, clientY: 290 });
|
|
534
|
+
frame();
|
|
535
|
+
assert.equal(scroller.scrollTop, 112);
|
|
536
|
+
assert.equal(animationFrames.size, 0);
|
|
537
|
+
start(100, 150); frame();
|
|
538
|
+
assert.equal(animationFrames.size, 0, 'center does not scroll');
|
|
539
|
+
scroller.scrollTop = 700;
|
|
540
|
+
start(); frame();
|
|
541
|
+
assert.equal(animationFrames.size, 0, 'bottom boundary stops frames');
|
|
542
|
+
scroller.scrollTop = 0;
|
|
543
|
+
start(100, 5); frame();
|
|
544
|
+
assert.equal(animationFrames.size, 0, 'top boundary stops frames');
|
|
545
|
+
scroller.scrollTop = 100;
|
|
546
|
+
for (const type of ['dragleave', 'drop', 'dragend']) {
|
|
547
|
+
start();
|
|
548
|
+
dispatch({ type, target: doc._body, relatedTarget: null });
|
|
549
|
+
assert.equal(animationFrames.size, 0, `${type} cancels pending frames`);
|
|
550
|
+
}
|
|
551
|
+
start(); unmount();
|
|
552
|
+
assert.equal(animationFrames.size, 0, 'hot unload cancels frames');
|
|
553
|
+
});
|
|
554
|
+
|
|
501
555
|
test('拖拽跨组移动:经权威 fiber 通道解析出正确 sessionId', async () => {
|
|
502
556
|
resetDom();
|
|
503
557
|
mount();
|
|
@@ -512,6 +566,31 @@ test('拖拽跨组移动:经权威 fiber 通道解析出正确 sessionId', asy
|
|
|
512
566
|
assert.equal(calls[0].payload.targetWorkspaceId, 'wid-b');
|
|
513
567
|
});
|
|
514
568
|
|
|
569
|
+
test('迁移标题使用权威预览,不包含侧边栏时间;驻留队列提示可能需重启', async () => {
|
|
570
|
+
resetDom();
|
|
571
|
+
mount();
|
|
572
|
+
const row = sessionRow('测试刚刚', 'session-aaa');
|
|
573
|
+
const beta = header('proj-beta');
|
|
574
|
+
fixture({ rows: [row] });
|
|
575
|
+
const prior = rpcResponder;
|
|
576
|
+
setResponder((endpoint, payload) => endpoint === 'mover.preview' ? {
|
|
577
|
+
ok: true, value: {
|
|
578
|
+
title: '测试', status: 'queued', source: { cwd: 'C:/proj-alpha' },
|
|
579
|
+
target: { path: 'C:/proj-beta' }, estimatedBackupBytes: 0,
|
|
580
|
+
warnings: ['resident writer; restart required']
|
|
581
|
+
}
|
|
582
|
+
} : prior(endpoint, payload));
|
|
583
|
+
attach(beta);
|
|
584
|
+
await dragDrop(row, beta, 'session-aaa', { confirm: false });
|
|
585
|
+
const overlay = doc._body.querySelector('.wsm-overlay');
|
|
586
|
+
assert.equal(overlay.querySelector('.wsm-session').textContent, '测试');
|
|
587
|
+
assert.match(overlay.querySelector('.wsm-preview').textContent, /可能需重启/);
|
|
588
|
+
assert.doesNotMatch(overlay.querySelector('.wsm-preview').textContent, /进入空闲队列/);
|
|
589
|
+
dispatch({ type: 'click', target: overlay.querySelector('.wsm-ok') });
|
|
590
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
591
|
+
assert.equal(moveCalls()[0].payload.sessionTitle, '测试');
|
|
592
|
+
});
|
|
593
|
+
|
|
515
594
|
test('兜底映射错配时不得把错配 id 当成真实会话移动(P2 回归)', async () => {
|
|
516
595
|
resetDom();
|
|
517
596
|
mount();
|