@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
|
|
6
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
7
|
+
const { setDbForTesting, SCHEMA_VERSION } = await jiti.import("./sqlite-db.ts");
|
|
8
|
+
const {
|
|
9
|
+
getBoard,
|
|
10
|
+
listBoards,
|
|
11
|
+
createBoard,
|
|
12
|
+
renameBoard,
|
|
13
|
+
deleteBoard,
|
|
14
|
+
deleteBoardCascade,
|
|
15
|
+
getOrCreateTaskBoard,
|
|
16
|
+
getBoardCanvas,
|
|
17
|
+
putBoardCanvas,
|
|
18
|
+
addNode,
|
|
19
|
+
patchNode,
|
|
20
|
+
deleteNode,
|
|
21
|
+
deleteNodesByIds,
|
|
22
|
+
bindNodeToSession,
|
|
23
|
+
getNodeByGlobalId,
|
|
24
|
+
addEdge,
|
|
25
|
+
deleteEdge,
|
|
26
|
+
getSystemRunningBoard,
|
|
27
|
+
reorderBoards,
|
|
28
|
+
syncCardEdges,
|
|
29
|
+
syncExecEdge,
|
|
30
|
+
removeSessionFromBoards,
|
|
31
|
+
} = await jiti.import("./board-store.ts");
|
|
32
|
+
const { createCard, addLink, replaceLinks, updateCard, getCard } = await jiti.import("./task-card-store.ts");
|
|
33
|
+
const { SYSTEM_RUNNING_BOARD_ID } = await jiti.import("./board-types.ts");
|
|
34
|
+
|
|
35
|
+
const PROJECT = "test-project";
|
|
36
|
+
|
|
37
|
+
let db;
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
db = new DatabaseSync(":memory:");
|
|
42
|
+
setDbForTesting(db);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
db.close();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("schema version reaches latest with board tables", () => {
|
|
50
|
+
const tables = db
|
|
51
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'board_%' ORDER BY name")
|
|
52
|
+
.all();
|
|
53
|
+
const names = tables.map((t) => t.name).sort();
|
|
54
|
+
assert.deepEqual(names, ["board_edges", "board_nodes", "board_view", "boards"].sort());
|
|
55
|
+
const boards = db
|
|
56
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'boards'")
|
|
57
|
+
.all();
|
|
58
|
+
assert.equal(boards.length, 1);
|
|
59
|
+
assert.equal(SCHEMA_VERSION, 8);
|
|
60
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
61
|
+
assert.equal(row.user_version, 8);
|
|
62
|
+
// v5 迁移:boards.task_id 列存在
|
|
63
|
+
const cols = db.prepare("PRAGMA table_info(boards)").all().map((c) => c.name);
|
|
64
|
+
assert.ok(cols.includes("task_id"));
|
|
65
|
+
// v6 迁移:task_id 唯一索引(并发防重复创建任务看板)
|
|
66
|
+
const uniq = db.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'boards' AND name = 'idx_boards_task_unique'").all();
|
|
67
|
+
assert.equal(uniq.length, 1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("board CRUD: create / rename / list / delete", () => {
|
|
71
|
+
const b = createBoard(PROJECT, "默认看板");
|
|
72
|
+
assert.equal(b.isSystem, false);
|
|
73
|
+
assert.equal(b.name, "默认看板");
|
|
74
|
+
assert.equal(b.nodeCount, 0);
|
|
75
|
+
|
|
76
|
+
const renamed = renameBoard(b.id, "重构看板");
|
|
77
|
+
assert.equal(renamed?.name, "重构看板");
|
|
78
|
+
assert.equal(getBoard(b.id)?.name, "重构看板");
|
|
79
|
+
|
|
80
|
+
const other = createBoard("other-project", "别的项目");
|
|
81
|
+
assert.equal(listBoards(PROJECT).length, 1);
|
|
82
|
+
assert.equal(listBoards("other-project").length, 1);
|
|
83
|
+
assert.equal(getBoard(other.id)?.projectKey, "other-project");
|
|
84
|
+
|
|
85
|
+
assert.equal(deleteBoard(b.id), true);
|
|
86
|
+
assert.equal(getBoard(b.id), undefined);
|
|
87
|
+
assert.equal(listBoards(PROJECT).length, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("empty projectKey / name rejected", () => {
|
|
91
|
+
assert.throws(() => createBoard("", "x"), /projectKey is required/);
|
|
92
|
+
assert.throws(() => createBoard(PROJECT, " "), /name must not be empty/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("system running board: always present, not deletable/renamable", () => {
|
|
96
|
+
const sys = getSystemRunningBoard();
|
|
97
|
+
assert.equal(sys.id, SYSTEM_RUNNING_BOARD_ID);
|
|
98
|
+
assert.equal(sys.isSystem, true);
|
|
99
|
+
assert.equal(getBoard(SYSTEM_RUNNING_BOARD_ID)?.isSystem, true);
|
|
100
|
+
assert.equal(renameBoard(SYSTEM_RUNNING_BOARD_ID, "x"), null);
|
|
101
|
+
assert.equal(deleteBoard(SYSTEM_RUNNING_BOARD_ID), false);
|
|
102
|
+
assert.equal(addNode(SYSTEM_RUNNING_BOARD_ID, { x: 0, y: 0 }), null);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("nodes: add / patch / delete cascade edges", () => {
|
|
106
|
+
const b = createBoard(PROJECT, "b");
|
|
107
|
+
const n1 = addNode(b.id, { x: 10, y: 20, w: 280, h: 120, refId: "s1" });
|
|
108
|
+
const n2 = addNode(b.id, { x: 400, y: 50, refId: "s2" });
|
|
109
|
+
assert.ok(n1 && n2);
|
|
110
|
+
assert.equal(n1.kind, "session");
|
|
111
|
+
assert.equal(n1.x, 10);
|
|
112
|
+
|
|
113
|
+
const patched = patchNode(b.id, n1.id, { x: 99, expanded: true });
|
|
114
|
+
assert.equal(patched?.x, 99);
|
|
115
|
+
assert.equal(patched?.expanded, true);
|
|
116
|
+
|
|
117
|
+
const e = addEdge(b.id, { fromId: n1.id, toId: n2.id, label: "depends", dashed: true });
|
|
118
|
+
assert.ok(e);
|
|
119
|
+
assert.equal(e.dashed, true);
|
|
120
|
+
|
|
121
|
+
// deleting n1 removes the edge
|
|
122
|
+
assert.equal(deleteNode(b.id, n1.id), true);
|
|
123
|
+
const canvas = getBoardCanvas(b.id);
|
|
124
|
+
assert.equal(canvas?.nodes.length, 1);
|
|
125
|
+
assert.equal(canvas?.edges.length, 0);
|
|
126
|
+
assert.equal(deleteNode(b.id, n1.id), false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("getOrCreateTaskBoard: 多次调用同一行 + task_id UNIQUE 拒绝重复(并发防重建)", () => {
|
|
130
|
+
const b1 = getOrCreateTaskBoard("task-conc-1", PROJECT, "并发任务");
|
|
131
|
+
// 连续多次调用(模拟并发请求)→ 恒返回同一看板
|
|
132
|
+
for (let i = 0; i < 5; i += 1) {
|
|
133
|
+
const again = getOrCreateTaskBoard("task-conc-1", PROJECT, "并发任务");
|
|
134
|
+
assert.equal(again.id, b1.id);
|
|
135
|
+
}
|
|
136
|
+
// 库里 task_id 唯一(UNIQUE 索引生效)
|
|
137
|
+
const rows = db.prepare("SELECT COUNT(*) c FROM boards WHERE task_id = ?").get("task-conc-1");
|
|
138
|
+
assert.equal(rows.c, 1);
|
|
139
|
+
// 手动重复插入被 UNIQUE 拒绝(并发时只有第一个 INSERT 成功)
|
|
140
|
+
assert.throws(() => {
|
|
141
|
+
db.prepare("INSERT INTO boards (id, project_key, name, is_system, task_id, sort_order, created, updated) VALUES (?, ?, ?, 0, ?, 0, 1, 1)")
|
|
142
|
+
.run("dup-id", PROJECT, "dup", "task-conc-1");
|
|
143
|
+
}, /UNIQUE/i);
|
|
144
|
+
// 不同 task_id 互不影响
|
|
145
|
+
const b2 = getOrCreateTaskBoard("task-conc-2", PROJECT, "另一个");
|
|
146
|
+
assert.notEqual(b2.id, b1.id);
|
|
147
|
+
assert.equal(db.prepare("SELECT COUNT(*) c FROM boards WHERE task_id IN ('task-conc-1','task-conc-2')").get().c, 2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("patchNode: refId 可定向写入/缺省保留/null 解绑(draft 卡转正)", () => {
|
|
151
|
+
const b = createBoard(PROJECT, "b");
|
|
152
|
+
// draft 卡:refId 为空
|
|
153
|
+
const draft = addNode(b.id, { x: 0, y: 0, refId: null });
|
|
154
|
+
assert.ok(draft);
|
|
155
|
+
assert.equal(draft.refId, null);
|
|
156
|
+
const beforeUpdated = getBoard(b.id)?.updated ?? 0;
|
|
157
|
+
|
|
158
|
+
// 转正:写入真实会话 id
|
|
159
|
+
const bound = patchNode(b.id, draft.id, { refId: "session-real-1" });
|
|
160
|
+
assert.equal(bound?.refId, "session-real-1");
|
|
161
|
+
// 缺省不改
|
|
162
|
+
assert.equal(patchNode(b.id, draft.id, { x: 55 })?.refId, "session-real-1");
|
|
163
|
+
// 显式 null 解绑
|
|
164
|
+
assert.equal(patchNode(b.id, draft.id, { refId: null })?.refId, null);
|
|
165
|
+
|
|
166
|
+
// PATCH 会 bump boards.updated(乐观锁基线):非递减(同毫秒内可能相等,不依赖单调性)
|
|
167
|
+
const afterUpdated = getBoard(b.id)?.updated ?? 0;
|
|
168
|
+
assert.ok(afterUpdated >= beforeUpdated);
|
|
169
|
+
// 携带过期基线的全量保存被 409 拒绝:不回头把绑定覆盖回 draft(哨兵基线 0 必过期)
|
|
170
|
+
const stale = putBoardCanvas(b.id, {
|
|
171
|
+
nodes: [{ id: draft.id, boardId: b.id, kind: "session", refId: null, x: 0, y: 0, w: 340, h: 160, expanded: false, props: {}, created: 1, updated: 1 }],
|
|
172
|
+
}, { baseUpdated: 0 });
|
|
173
|
+
assert.equal(stale, "conflict");
|
|
174
|
+
// 用当前基线则放行
|
|
175
|
+
const fresh = putBoardCanvas(b.id, {
|
|
176
|
+
nodes: [{ id: draft.id, boardId: b.id, kind: "session", refId: "session-real-1", x: 0, y: 0, w: 340, h: 160, expanded: false, props: {}, created: 1, updated: 1 }],
|
|
177
|
+
}, { baseUpdated: afterUpdated });
|
|
178
|
+
assert.equal(fresh, true);
|
|
179
|
+
assert.equal(getBoardCanvas(b.id)?.nodes[0].refId, "session-real-1");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("deleteNodesByIds: 跨看板批量删节点 + 级联删边 + bump updated", () => {
|
|
183
|
+
const b1 = createBoard(PROJECT, "b1");
|
|
184
|
+
const b2 = createBoard(PROJECT, "b2");
|
|
185
|
+
const n1 = addNode(b1.id, { x: 0, y: 0, refId: "s1" });
|
|
186
|
+
const n2 = addNode(b1.id, { x: 100, y: 0, refId: "s2" });
|
|
187
|
+
const n3 = addNode(b2.id, { x: 0, y: 0, refId: "s3" });
|
|
188
|
+
assert.ok(n1 && n2 && n3);
|
|
189
|
+
// 连线:n1→n2(同板)、n1→n3(跨板不允许 addEdge,用同板验证)
|
|
190
|
+
assert.ok(addEdge(b1.id, { fromId: n1.id, toId: n2.id, label: "x" }));
|
|
191
|
+
const before1 = getBoard(b1.id)?.updated ?? 0;
|
|
192
|
+
|
|
193
|
+
// 删 n1 + n3(跨两个 board)→ n1 的边被级联删,n2 保留
|
|
194
|
+
const r = deleteNodesByIds([n1.id, n3.id, "no-such"]);
|
|
195
|
+
assert.equal(r.deletedNodes, 2);
|
|
196
|
+
assert.equal(r.deletedEdges, 1);
|
|
197
|
+
assert.deepEqual(r.boards.sort(), [b1.id, b2.id].sort());
|
|
198
|
+
const c1 = getBoardCanvas(b1.id);
|
|
199
|
+
assert.equal(c1?.nodes.length, 1);
|
|
200
|
+
assert.equal(c1?.nodes[0].id, n2.id);
|
|
201
|
+
assert.equal(c1?.edges.length, 0);
|
|
202
|
+
const c2 = getBoardCanvas(b2.id);
|
|
203
|
+
assert.equal(c2?.nodes.length, 0);
|
|
204
|
+
// bump 看板 updated(乐观锁基线;同毫秒内可能相等,不依赖单调性)
|
|
205
|
+
assert.ok((getBoard(b1.id)?.updated ?? 0) >= before1);
|
|
206
|
+
// 空列表 / 系统看板节点不删
|
|
207
|
+
assert.deepEqual(deleteNodesByIds([]), { deletedNodes: 0, deletedEdges: 0, boards: [] });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("bindNodeToSession: 全局 nodeId 写 ref_id(服务端后台转正)+ bump 看板 updated", () => {
|
|
211
|
+
const b = createBoard(PROJECT, "b");
|
|
212
|
+
const draft = addNode(b.id, { x: 0, y: 0, refId: null });
|
|
213
|
+
assert.ok(draft);
|
|
214
|
+
assert.equal(draft.refId, null);
|
|
215
|
+
const beforeUpdated = getBoard(b.id)?.updated ?? 0;
|
|
216
|
+
|
|
217
|
+
// 服务端拿到 boardNodeId 后直接绑定(不依赖 boardId,nodeId 全局唯一)
|
|
218
|
+
const bound = bindNodeToSession(draft.id, "session-real-2");
|
|
219
|
+
assert.ok(bound);
|
|
220
|
+
assert.equal(bound.refId, "session-real-2");
|
|
221
|
+
assert.equal(bound.boardId, b.id);
|
|
222
|
+
// bump 看板 updated(乐观锁基线:迟到的旧全量保存会被 409 拒绝)
|
|
223
|
+
const afterUpdated = getBoard(b.id)?.updated ?? 0;
|
|
224
|
+
assert.ok(afterUpdated >= beforeUpdated);
|
|
225
|
+
// 全量保存读到的 refId 已是绑定值
|
|
226
|
+
assert.equal(getBoardCanvas(b.id)?.nodes[0].refId, "session-real-2");
|
|
227
|
+
|
|
228
|
+
// 不存在的 nodeId → null;系统看板节点 → null
|
|
229
|
+
assert.equal(bindNodeToSession("no-such-node", "x"), null);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("getNodeByGlobalId: 按全局 nodeId 读节点(未转正卡轮询用)", () => {
|
|
233
|
+
const b = createBoard(PROJECT, "b");
|
|
234
|
+
const n1 = addNode(b.id, { x: 0, y: 0, refId: null });
|
|
235
|
+
const n2 = addNode(b.id, { x: 100, y: 0, refId: "s1" });
|
|
236
|
+
assert.ok(n1 && n2);
|
|
237
|
+
|
|
238
|
+
// draft 卡:refId null
|
|
239
|
+
assert.equal(getNodeByGlobalId(n1.id)?.refId, null);
|
|
240
|
+
// 已绑定:refId 有值
|
|
241
|
+
assert.equal(getNodeByGlobalId(n2.id)?.refId, "s1");
|
|
242
|
+
// 轮询发现服务端已写入 ref_id → 前端据此转正
|
|
243
|
+
bindNodeToSession(n1.id, "session-real-3");
|
|
244
|
+
assert.equal(getNodeByGlobalId(n1.id)?.refId, "session-real-3");
|
|
245
|
+
assert.equal(getNodeByGlobalId("no-such"), undefined);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("addEdge rejects self-loop and cross-board refs", () => {
|
|
249
|
+
const b = createBoard(PROJECT, "b");
|
|
250
|
+
const b2 = createBoard(PROJECT, "b2");
|
|
251
|
+
const n1 = addNode(b.id, { x: 0, y: 0 });
|
|
252
|
+
const n2 = addNode(b.id, { x: 100, y: 0 });
|
|
253
|
+
const n3 = addNode(b2.id, { x: 0, y: 0 });
|
|
254
|
+
assert.ok(n1 && n2 && n3);
|
|
255
|
+
assert.equal(addEdge(b.id, { fromId: n1.id, toId: n1.id }), null);
|
|
256
|
+
assert.equal(addEdge(b.id, { fromId: n1.id, toId: n3.id }), null);
|
|
257
|
+
assert.ok(addEdge(b.id, { fromId: n1.id, toId: n2.id }));
|
|
258
|
+
assert.equal(deleteEdge(b.id, "nope"), false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("putBoardCanvas full replace inside transaction, rollback on error", () => {
|
|
262
|
+
const b = createBoard(PROJECT, "b");
|
|
263
|
+
const n1 = addNode(b.id, { x: 0, y: 0, refId: "s1" });
|
|
264
|
+
const n2 = addNode(b.id, { x: 100, y: 0, refId: "s2" });
|
|
265
|
+
assert.ok(n1 && n2);
|
|
266
|
+
addEdge(b.id, { fromId: n1.id, toId: n2.id });
|
|
267
|
+
|
|
268
|
+
const nodes = [
|
|
269
|
+
{ ...n1, x: 5, y: 5, w: 300, h: 150, props: { color: "#f00" }, created: n1.created, updated: n1.updated },
|
|
270
|
+
];
|
|
271
|
+
const edges = [];
|
|
272
|
+
const ok = putBoardCanvas(b.id, {
|
|
273
|
+
nodes,
|
|
274
|
+
edges,
|
|
275
|
+
view: { boardId: b.id, cameraX: -50, cameraY: -30, cameraZ: 0.8, updated: 1 },
|
|
276
|
+
});
|
|
277
|
+
assert.equal(ok, true);
|
|
278
|
+
|
|
279
|
+
const canvas = getBoardCanvas(b.id);
|
|
280
|
+
assert.equal(canvas?.nodes.length, 1);
|
|
281
|
+
assert.equal(canvas?.nodes[0].x, 5);
|
|
282
|
+
assert.deepEqual(canvas?.nodes[0].props, { color: "#f00" });
|
|
283
|
+
assert.equal(canvas?.edges.length, 0);
|
|
284
|
+
assert.equal(canvas?.view?.cameraZ, 0.8);
|
|
285
|
+
|
|
286
|
+
// system board / missing board not writable
|
|
287
|
+
assert.equal(putBoardCanvas(SYSTEM_RUNNING_BOARD_ID, { nodes: [] }), false);
|
|
288
|
+
assert.equal(putBoardCanvas("missing", { nodes: [] }), false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("putBoardCanvas empty overwrite guard + allowEmpty escape hatch", () => {
|
|
292
|
+
const b = createBoard(PROJECT, "b");
|
|
293
|
+
addNode(b.id, { x: 0, y: 0, refId: "s1" });
|
|
294
|
+
// 非空看板被空节点集覆盖 → 拒绝(防客户端未加载完成清空看板)
|
|
295
|
+
assert.equal(putBoardCanvas(b.id, { nodes: [] }), "empty-overwrite");
|
|
296
|
+
assert.equal(getBoardCanvas(b.id)?.nodes.length, 1);
|
|
297
|
+
// 显式 allowEmpty → 放行
|
|
298
|
+
assert.equal(putBoardCanvas(b.id, { nodes: [], edges: [] }, { allowEmpty: true }), true);
|
|
299
|
+
assert.equal(getBoardCanvas(b.id)?.nodes.length, 0);
|
|
300
|
+
// 原本就空的看板,空覆盖本就允许
|
|
301
|
+
assert.equal(putBoardCanvas(b.id, { nodes: [] }), true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("rollback keeps db consistent when transaction throws", () => {
|
|
305
|
+
const b = createBoard(PROJECT, "b");
|
|
306
|
+
const n1 = addNode(b.id, { x: 0, y: 0 });
|
|
307
|
+
assert.ok(n1);
|
|
308
|
+
// addEdge with a bad ref throws inside a transaction? no — returns null.
|
|
309
|
+
// Force a real throw via a duplicate primary key through putBoardCanvas.
|
|
310
|
+
const nodes = [
|
|
311
|
+
{ ...n1, id: "dup", created: n1.created, updated: n1.updated },
|
|
312
|
+
{ ...n1, id: "dup", created: n1.created, updated: n1.updated },
|
|
313
|
+
];
|
|
314
|
+
assert.throws(() => putBoardCanvas(b.id, { nodes }));
|
|
315
|
+
// rollback: original node still there
|
|
316
|
+
const canvas = getBoardCanvas(b.id);
|
|
317
|
+
assert.equal(canvas?.nodes.length, 1);
|
|
318
|
+
assert.equal(canvas?.nodes[0].id, n1.id);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("board reorder within project", () => {
|
|
322
|
+
const a = createBoard(PROJECT, "A");
|
|
323
|
+
const b = createBoard(PROJECT, "B");
|
|
324
|
+
const c = createBoard(PROJECT, "C");
|
|
325
|
+
// 新板置顶:创建顺序 C, B, A
|
|
326
|
+
assert.deepEqual(listBoards(PROJECT).map((x) => x.id), [c.id, b.id, a.id]);
|
|
327
|
+
|
|
328
|
+
const reordered = reorderBoards(PROJECT, [a.id, c.id, b.id]);
|
|
329
|
+
assert.deepEqual(reordered.map((x) => x.id), [a.id, c.id, b.id]);
|
|
330
|
+
|
|
331
|
+
// foreign id → rollback keeps order
|
|
332
|
+
assert.throws(() => reorderBoards(PROJECT, [a.id, "foreign"]));
|
|
333
|
+
assert.deepEqual(listBoards(PROJECT).map((x) => x.id), [a.id, c.id, b.id]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("canvas of missing board is null", () => {
|
|
337
|
+
assert.equal(getBoardCanvas("missing"), null);
|
|
338
|
+
assert.equal(addNode("missing", { x: 0, y: 0 }), null);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("task board: lazy create (upsert), id = taskId, not in manual reorder scope", () => {
|
|
342
|
+
const b1 = getOrCreateTaskBoard("task-1", PROJECT, "任务一");
|
|
343
|
+
assert.equal(b1.id, "task-1");
|
|
344
|
+
assert.equal(b1.taskId, "task-1");
|
|
345
|
+
assert.equal(b1.isSystem, false);
|
|
346
|
+
assert.equal(b1.name, "任务一");
|
|
347
|
+
|
|
348
|
+
// 幂等:再次调用返回同一 board(不重复创建)
|
|
349
|
+
const b2 = getOrCreateTaskBoard("task-1", PROJECT, "任务一");
|
|
350
|
+
assert.equal(b2.id, b1.id);
|
|
351
|
+
assert.equal(getBoard("task-1")?.taskId, "task-1");
|
|
352
|
+
|
|
353
|
+
// 出现在 listBoards 中(展示层自行过滤 taskId == null)
|
|
354
|
+
assert.ok(listBoards(PROJECT).some((b) => b.id === "task-1"));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("task board: deleteBoardCascade removes nodes/edges/view within caller transaction", () => {
|
|
358
|
+
const b = getOrCreateTaskBoard("task-2", PROJECT, "任务二");
|
|
359
|
+
const n1 = addNode(b.id, { x: 0, y: 0, refId: "s1" });
|
|
360
|
+
const n2 = addNode(b.id, { x: 100, y: 0, refId: "s2" });
|
|
361
|
+
assert.ok(n1 && n2);
|
|
362
|
+
addEdge(b.id, { fromId: n1.id, toId: n2.id });
|
|
363
|
+
putBoardCanvas(b.id, {
|
|
364
|
+
nodes: undefined,
|
|
365
|
+
edges: undefined,
|
|
366
|
+
view: { boardId: b.id, cameraX: 1, cameraY: 2, cameraZ: 3, updated: 1 },
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// 事务内调用:异常回滚不残留
|
|
370
|
+
const dbLocal = db;
|
|
371
|
+
dbLocal.exec("BEGIN");
|
|
372
|
+
try {
|
|
373
|
+
deleteBoardCascade(b.id);
|
|
374
|
+
throw new Error("boom");
|
|
375
|
+
} catch {
|
|
376
|
+
dbLocal.exec("ROLLBACK");
|
|
377
|
+
}
|
|
378
|
+
assert.ok(getBoard(b.id)); // 回滚后看板仍在
|
|
379
|
+
|
|
380
|
+
dbLocal.exec("BEGIN");
|
|
381
|
+
try {
|
|
382
|
+
deleteBoardCascade(b.id);
|
|
383
|
+
dbLocal.exec("COMMIT");
|
|
384
|
+
} catch {
|
|
385
|
+
dbLocal.exec("ROLLBACK");
|
|
386
|
+
throw new Error("unexpected");
|
|
387
|
+
}
|
|
388
|
+
assert.equal(getBoard(b.id), undefined);
|
|
389
|
+
assert.equal(getBoardCanvas(b.id), null);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("syncCardEdges: 依赖边自动生成/更新/补回(label=kind,禁删语义)", () => {
|
|
393
|
+
const b = createBoard(PROJECT, "看板A");
|
|
394
|
+
const c1 = createCard({ boardId: b.id, projectKey: PROJECT, name: "卡1" });
|
|
395
|
+
const c2 = createCard({ boardId: b.id, projectKey: PROJECT, name: "卡2" });
|
|
396
|
+
const c3 = createCard({ boardId: b.id, projectKey: PROJECT, name: "卡3" });
|
|
397
|
+
const n1 = addNode(b.id, { kind: "taskcard", refId: c1.id, x: 10, y: 10 });
|
|
398
|
+
const n2 = addNode(b.id, { kind: "taskcard", refId: c2.id, x: 300, y: 10 });
|
|
399
|
+
const n3 = addNode(b.id, { kind: "taskcard", refId: c3.id, x: 600, y: 10 });
|
|
400
|
+
|
|
401
|
+
addLink(c1.id, c2.id, "prerequisite");
|
|
402
|
+
syncCardEdges(c1.id);
|
|
403
|
+
|
|
404
|
+
const canvas = getBoardCanvas(b.id);
|
|
405
|
+
const auto = canvas.edges.filter((e) => e.fromId === n1.id);
|
|
406
|
+
assert.equal(auto.length, 1);
|
|
407
|
+
assert.equal(auto[0].toId, n2.id);
|
|
408
|
+
assert.equal(auto[0].label, "prerequisite");
|
|
409
|
+
|
|
410
|
+
// 改 target:删旧边加新边
|
|
411
|
+
replaceLinks(c1.id, [c3.id], [c2.id]);
|
|
412
|
+
syncCardEdges(c1.id);
|
|
413
|
+
let edges = getBoardCanvas(b.id).edges.filter((e) => e.fromId === n1.id);
|
|
414
|
+
assert.deepEqual(
|
|
415
|
+
edges.map((e) => [e.toId, e.label]).sort(),
|
|
416
|
+
[[n2.id, "related"], [n3.id, "prerequisite"]].sort(),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// 手动删自动边 → syncCardEdges 补回
|
|
420
|
+
const autoEdge = getBoardCanvas(b.id).edges.find((e) => e.fromId === n1.id && e.label === "prerequisite");
|
|
421
|
+
assert.ok(autoEdge);
|
|
422
|
+
deleteEdge(b.id, autoEdge.id);
|
|
423
|
+
assert.equal(getBoardCanvas(b.id).edges.filter((e) => e.id === autoEdge.id).length, 0);
|
|
424
|
+
syncCardEdges(c1.id);
|
|
425
|
+
assert.ok(getBoardCanvas(b.id).edges.find((e) => e.fromId === n1.id && e.label === "prerequisite"));
|
|
426
|
+
|
|
427
|
+
// 被依赖方删除 link 后,再次 sync 清掉出边
|
|
428
|
+
replaceLinks(c1.id, [], []);
|
|
429
|
+
syncCardEdges(c1.id);
|
|
430
|
+
assert.equal(getBoardCanvas(b.id).edges.filter((e) => e.fromId === n1.id).length, 0);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("syncCardEdges: target 无画布节点时跳过不崩;无 node 的卡直接返回", () => {
|
|
434
|
+
const b = createBoard(PROJECT, "看板A");
|
|
435
|
+
const c1 = createCard({ boardId: b.id, projectKey: PROJECT, name: "卡1" });
|
|
436
|
+
const c2 = createCard({ boardId: b.id, projectKey: PROJECT, name: "卡2" });
|
|
437
|
+
addNode(b.id, { kind: "taskcard", refId: c1.id, x: 10, y: 10 });
|
|
438
|
+
// c2 无 node
|
|
439
|
+
addLink(c1.id, c2.id, "prerequisite");
|
|
440
|
+
syncCardEdges(c1.id); // 不抛错,无边
|
|
441
|
+
assert.equal(getBoardCanvas(b.id).edges.length, 0);
|
|
442
|
+
|
|
443
|
+
// 无 node 的卡直接返回
|
|
444
|
+
const c3 = createCard({ boardId: b.id, projectKey: PROJECT, name: "卡3" });
|
|
445
|
+
addLink(c3.id, c1.id, "prerequisite");
|
|
446
|
+
syncCardEdges(c3.id);
|
|
447
|
+
assert.equal(getBoardCanvas(b.id).edges.length, 0);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("syncExecEdge: 绑 sessionId → 建 label=exec 边;手动删补回;解绑删边", () => {
|
|
451
|
+
const b = createBoard(PROJECT, "看板A");
|
|
452
|
+
const c = createCard({ boardId: b.id, projectKey: PROJECT, name: "任务" });
|
|
453
|
+
const cardNode = addNode(b.id, { kind: "taskcard", refId: c.id, x: 10, y: 10 });
|
|
454
|
+
const sessionNode = addNode(b.id, { kind: "session", refId: "sess-1", x: 300, y: 10 });
|
|
455
|
+
const boardId = b.id;
|
|
456
|
+
|
|
457
|
+
// 未绑定 → 无边
|
|
458
|
+
syncExecEdge(c.id);
|
|
459
|
+
assert.equal(getBoardCanvas(boardId).edges.filter((e) => e.label === "exec").length, 0);
|
|
460
|
+
|
|
461
|
+
// 绑定 → 建边(from=taskcard node, to=session node)
|
|
462
|
+
updateCard(c.id, { sessionId: "sess-1" });
|
|
463
|
+
syncExecEdge(c.id);
|
|
464
|
+
const execEdge = getBoardCanvas(boardId).edges.find((e) => e.label === "exec");
|
|
465
|
+
assert.ok(execEdge);
|
|
466
|
+
assert.equal(execEdge.fromId, cardNode.id);
|
|
467
|
+
assert.equal(execEdge.toId, sessionNode.id);
|
|
468
|
+
|
|
469
|
+
// 手动删边 → 补回(禁删语义)
|
|
470
|
+
deleteEdge(boardId, execEdge.id);
|
|
471
|
+
assert.equal(getBoardCanvas(boardId).edges.filter((e) => e.label === "exec").length, 0);
|
|
472
|
+
syncExecEdge(c.id);
|
|
473
|
+
assert.ok(
|
|
474
|
+
getBoardCanvas(boardId).edges.find(
|
|
475
|
+
(e) => e.label === "exec" && e.fromId === cardNode.id && e.toId === sessionNode.id,
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// 解绑 → 删边
|
|
480
|
+
updateCard(c.id, { sessionId: null });
|
|
481
|
+
syncExecEdge(c.id);
|
|
482
|
+
assert.equal(getBoardCanvas(boardId).edges.filter((e) => e.label === "exec").length, 0);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("syncExecEdge: session 节点缺失不建线;节点后补/换绑重建", () => {
|
|
486
|
+
const b = createBoard(PROJECT, "看板A");
|
|
487
|
+
const c = createCard({ boardId: b.id, projectKey: PROJECT, name: "任务" });
|
|
488
|
+
addNode(b.id, { kind: "taskcard", refId: c.id, x: 10, y: 10 });
|
|
489
|
+
const boardId = b.id;
|
|
490
|
+
|
|
491
|
+
// 绑到不存在的会话 → 不建线(无残留边)
|
|
492
|
+
updateCard(c.id, { sessionId: "ghost-session" });
|
|
493
|
+
syncExecEdge(c.id);
|
|
494
|
+
assert.equal(getBoardCanvas(boardId).edges.filter((e) => e.label === "exec").length, 0);
|
|
495
|
+
|
|
496
|
+
// 换绑到 sess-2(节点尚不存在)→ 仍不建线
|
|
497
|
+
updateCard(c.id, { sessionId: "sess-2" });
|
|
498
|
+
syncExecEdge(c.id);
|
|
499
|
+
assert.equal(getBoardCanvas(boardId).edges.filter((e) => e.label === "exec").length, 0);
|
|
500
|
+
|
|
501
|
+
// 会话节点后补 → 建线
|
|
502
|
+
const s2 = addNode(boardId, { kind: "session", refId: "sess-2", x: 400, y: 10 });
|
|
503
|
+
syncExecEdge(c.id);
|
|
504
|
+
const e2 = getBoardCanvas(boardId).edges.find((x) => x.label === "exec");
|
|
505
|
+
assert.ok(e2);
|
|
506
|
+
assert.equal(e2.toId, s2.id);
|
|
507
|
+
|
|
508
|
+
// 换绑到另一会话 → 边改向,只保留一条
|
|
509
|
+
const s3 = addNode(boardId, { kind: "session", refId: "sess-3", x: 500, y: 10 });
|
|
510
|
+
updateCard(c.id, { sessionId: "sess-3" });
|
|
511
|
+
syncExecEdge(c.id);
|
|
512
|
+
const e3 = getBoardCanvas(boardId).edges.find((x) => x.label === "exec");
|
|
513
|
+
assert.ok(e3);
|
|
514
|
+
assert.equal(e3.toId, s3.id);
|
|
515
|
+
assert.equal(getBoardCanvas(boardId).edges.filter((x) => x.label === "exec").length, 1);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("removeSessionFromBoards: 删会话清任务卡引用 + exec 边 + 画布节点,幂等", () => {
|
|
519
|
+
const b = createBoard(PROJECT, "看板A");
|
|
520
|
+
const b2 = createBoard(PROJECT, "看板B");
|
|
521
|
+
const c = createCard({ boardId: b.id, projectKey: PROJECT, name: "任务" });
|
|
522
|
+
addNode(b.id, { kind: "taskcard", refId: c.id, x: 10, y: 10 });
|
|
523
|
+
addNode(b.id, { kind: "session", refId: "sess-x", x: 300, y: 10 });
|
|
524
|
+
// 同一会话也在手动看板上有卡
|
|
525
|
+
addNode(b2.id, { kind: "session", refId: "sess-x", x: 0, y: 0 });
|
|
526
|
+
updateCard(c.id, { sessionId: "sess-x" });
|
|
527
|
+
syncExecEdge(c.id);
|
|
528
|
+
assert.ok(getBoardCanvas(b.id).edges.find((e) => e.label === "exec"));
|
|
529
|
+
|
|
530
|
+
const removed = removeSessionFromBoards("sess-x");
|
|
531
|
+
assert.equal(removed.removedNodes, 2);
|
|
532
|
+
// 受影响看板 updated 返回(乐观锁基线刷新用)
|
|
533
|
+
assert.equal(removed.boards.length, 2);
|
|
534
|
+
// exec 边消失 + 会话节点从两个看板消失
|
|
535
|
+
assert.equal(getBoardCanvas(b.id).edges.filter((e) => e.label === "exec").length, 0);
|
|
536
|
+
assert.equal(getBoardCanvas(b.id).nodes.filter((n) => n.refId === "sess-x").length, 0);
|
|
537
|
+
assert.equal(getBoardCanvas(b2.id).nodes.filter((n) => n.refId === "sess-x").length, 0);
|
|
538
|
+
// 任务卡 session_id 清空
|
|
539
|
+
assert.equal(getCard(c.id)?.sessionId, null);
|
|
540
|
+
// 幂等:无引用返回 0
|
|
541
|
+
assert.equal(removeSessionFromBoards("sess-x").removedNodes, 0);
|
|
542
|
+
});
|