@baique/pi-web-sky 0.1.55 → 0.1.57
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 +8 -4
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +2 -2
- package/.next/react-loadable-manifest.json +2 -2
- package/.next/required-server-files.js +3 -2
- package/.next/required-server-files.json +3 -2
- package/.next/routes-manifest.json +28 -0
- package/.next/server/app/_global-error/page.js +2 -2
- 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.js +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 +4 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +4 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +3 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/running/route.js +1 -1
- package/.next/server/app/api/agent/running/route.js.nft.json +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/all-providers/route.js +1 -1
- package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/providers/route.js +1 -1
- package/.next/server/app/api/board-assets/[...path]/route.js +2 -2
- package/.next/server/app/api/board-assets/route.js +2 -2
- package/.next/server/app/api/boards/[id]/add-session/route.js +1 -0
- package/.next/server/app/api/boards/[id]/add-session/route.js.nft.json +1 -0
- package/.next/server/app/api/boards/[id]/add-session/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/boards/[id]/reconcile/route.js +1 -1
- package/.next/server/app/api/boards/[id]/route.js +1 -1
- package/.next/server/app/api/boards/reorder/route.js +2 -2
- package/.next/server/app/api/boards/route.js +1 -1
- package/.next/server/app/api/cwd/browse/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +2 -2
- package/.next/server/app/api/default-cwd/route.js +2 -2
- package/.next/server/app/api/file-index/route.js +2 -2
- package/.next/server/app/api/files/[...path]/route.js +3 -3
- package/.next/server/app/api/git/diff/route.js +1 -1
- package/.next/server/app/api/git/status/route.js +1 -1
- package/.next/server/app/api/home/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +2 -2
- package/.next/server/app/api/models/route.js +2 -2
- package/.next/server/app/api/models-config/catalog/route.js +1 -1
- package/.next/server/app/api/models-config/discover/route.js +1 -1
- package/.next/server/app/api/models-config/route.js +1 -1
- package/.next/server/app/api/models-config/test/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +2 -2
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js.nft.json +1 -1
- package/.next/server/app/api/quota/route.js +1 -1
- package/.next/server/app/api/search/route.js +3 -3
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/auto-name/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
- package/.next/server/app/api/sessions/[id]/minimap/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/minimap/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/summary/route.js +2 -2
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/install/route.js +2 -2
- package/.next/server/app/api/skills/route.js +1 -1
- package/.next/server/app/api/skills/search/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/subagents/[id]/route.js +1 -0
- package/.next/server/app/api/subagents/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/subagents/[id]/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/subagents/profiles/route.js +30 -0
- package/.next/server/app/api/subagents/profiles/route.js.nft.json +1 -0
- package/.next/server/app/api/subagents/profiles/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/subagents/settings/route.js +1 -0
- package/.next/server/app/api/subagents/settings/route.js.nft.json +1 -0
- package/.next/server/app/api/subagents/settings/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/task-card-questions/[id]/answer/route.js +2 -2
- package/.next/server/app/api/task-card-questions/route.js +2 -2
- package/.next/server/app/api/task-cards/[id]/route.js +1 -1
- package/.next/server/app/api/task-cards/route.js +1 -1
- package/.next/server/app/api/task-cards/unbind/route.js +3 -3
- package/.next/server/app/api/task-scheduler/status/route.js +1 -1
- package/.next/server/app/api/task-scheduler/status/route.js.nft.json +1 -1
- package/.next/server/app/api/tasks/[id]/assign-session/route.js +2 -12
- package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/route.js +1 -1
- package/.next/server/app/api/tasks/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/tasks/reorder/route.js +2 -12
- package/.next/server/app/api/tasks/route.js +2 -12
- package/.next/server/app/api/terminal/[id]/events/route.js +2 -2
- package/.next/server/app/api/terminal/[id]/route.js +2 -2
- package/.next/server/app/api/terminal/route.js +2 -2
- package/.next/server/app/api/worktrees/route.js +2 -2
- package/.next/server/app/api/yjs-version/route.js +1 -1
- package/.next/server/app/favicon.ico/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +6 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -5
- package/.next/server/app/index.segments/_full.segment.rsc +6 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
- package/.next/server/app/manifest.webmanifest/route.js +2 -2
- package/.next/server/app/page.js +175 -8
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +8 -4
- package/.next/server/chunks/1937.js +14 -13
- package/.next/server/chunks/2524.js +2 -2
- package/.next/server/chunks/3379.js +1 -1
- package/.next/server/chunks/3500.js +1 -1
- package/.next/server/chunks/402.js +28 -7
- package/.next/server/chunks/426.js +14 -0
- package/.next/server/chunks/4367.js +1 -1
- package/.next/server/chunks/4457.js +3 -13
- package/.next/server/chunks/4980.js +1 -1
- package/.next/server/chunks/542.js +23 -4
- package/.next/server/chunks/5794.js +2 -2
- package/.next/server/chunks/5940.js +1 -1
- package/.next/server/chunks/6337.js +1 -1
- package/.next/server/chunks/6429.js +15 -2
- package/.next/server/chunks/6525.js +25 -4
- package/.next/server/chunks/6583.js +12 -11
- package/.next/server/chunks/7048.js +6 -0
- package/.next/server/instrumentation.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/middleware.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/MPBMmQqmDG8zo8tM_bkDy/_buildManifest.js +1 -0
- package/.next/static/chunks/752.bafff304e6b00987.js +1 -0
- package/.next/static/chunks/8088.39866dc5f1d15868.js +1 -0
- package/.next/static/chunks/app/_global-error/page-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/bash-output/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/agent/running/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/app-update/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/auth/all-providers/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/auth/api-key/[provider]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/auth/login/[provider]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/auth/logout/[provider]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/auth/providers/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/board-assets/[...path]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/board-assets/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/boards/[id]/add-session/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/boards/[id]/reconcile/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/boards/[id]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/boards/reorder/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/boards/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/cwd/browse/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/cwd/validate/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/default-cwd/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/file-index/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/files/[...path]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/git/diff/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/git/status/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/home/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/mcp/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/models/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/models-config/catalog/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/models-config/discover/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/models-config/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/models-config/test/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/project-trust/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/quota/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/search/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/context/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/minimap/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/state/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/todos/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/sessions/summary/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/skills/check/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/skills/install/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/skills/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/skills/search/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/skills/update/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/subagents/[id]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/subagents/profiles/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/subagents/settings/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/task-card-questions/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/task-cards/[id]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/task-cards/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/task-cards/unbind/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/task-scheduler/status/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/tasks/[id]/assign-session/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/tasks/[id]/board/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/tasks/[id]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/tasks/reorder/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/tasks/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/terminal/[id]/events/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/terminal/[id]/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/terminal/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/worktrees/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/api/yjs-version/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/layout-fae2b226b3c78448.js +1 -0
- package/.next/static/chunks/app/manifest.webmanifest/route-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/app/page-d5af3bd535589daf.js +256 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-ad6e695deb9de65c.js +1 -0
- package/.next/static/chunks/{webpack-b0effde51c1651b8.js → webpack-1ce35e1249cfd931.js} +1 -1
- package/.next/static/css/bb9361c7f832a89d.css +1 -0
- package/.next/static/css/c7d127e07b1a786c.css +1 -0
- package/.next/trace +8 -7
- package/.next/trace-build +1 -1
- package/.next/types/app/api/boards/[id]/add-session/route.ts +353 -0
- package/.next/types/app/api/subagents/[id]/route.ts +353 -0
- package/.next/types/app/api/subagents/profiles/route.ts +353 -0
- package/.next/types/app/api/subagents/settings/route.ts +353 -0
- package/.next/types/routes.d.ts +5 -1
- package/.next/types/validator.ts +36 -0
- package/lib/api-types.ts +10 -0
- package/lib/board-reconcile.test.mjs +46 -8
- package/lib/board-reconcile.ts +57 -1
- package/lib/board-store.test.mjs +2 -2
- package/lib/chat-lazy-load.test.mjs +0 -62
- package/lib/chat-lazy-load.ts +0 -13
- package/lib/chat-only.ts +23 -0
- package/lib/draft-stash.ts +4 -5
- package/lib/file-types.ts +16 -1
- package/lib/i18n/messages/en.ts +91 -0
- package/lib/i18n/messages/zh-CN.ts +91 -0
- package/lib/id.test.mjs +38 -0
- package/lib/id.ts +17 -0
- package/lib/ime-guard.test.mjs +52 -0
- package/lib/ime-guard.ts +54 -0
- package/lib/message-text.ts +24 -0
- package/lib/models-cache.ts +1 -1
- package/lib/pi-types.ts +19 -1
- package/lib/pin-store.ts +85 -0
- package/lib/powershell-settings.ts +106 -0
- package/lib/quoted-selection.ts +4 -0
- package/lib/rpc-manager.activity.test.mjs +223 -0
- package/lib/rpc-manager.test.mjs +22 -0
- package/lib/rpc-manager.title.test.mjs +152 -0
- package/lib/rpc-manager.ts +301 -28
- package/lib/session-activity.test.mjs +123 -0
- package/lib/session-activity.ts +77 -0
- package/lib/session-delete.test.mjs +45 -41
- package/lib/session-delete.ts +8 -34
- package/lib/session-family.test.mjs +49 -0
- package/lib/session-family.ts +80 -0
- package/lib/session-index-scanner.test.mjs +364 -2
- package/lib/session-index-scanner.ts +199 -55
- package/lib/session-reader.noscan.test.mjs +371 -0
- package/lib/session-reader.project.test.mjs +30 -52
- package/lib/session-reader.test.mjs +2 -65
- package/lib/session-reader.ts +177 -213
- package/lib/session-scanner.test.mjs +151 -38
- package/lib/session-scanner.ts +140 -61
- package/lib/session-search.ts +6 -5
- package/lib/session-time-group.test.mjs +38 -0
- package/lib/session-time-group.ts +55 -0
- package/lib/settings-navigation.ts +124 -0
- package/lib/sqlite-db.test.mjs +26 -0
- package/lib/sqlite-db.ts +10 -1
- package/lib/subagent-extension.test.mjs +424 -0
- package/lib/subagent-extension.ts +309 -0
- package/lib/subagent-input.test.mjs +63 -0
- package/lib/subagent-input.ts +71 -0
- package/lib/subagent-profile-precedence.ts +20 -0
- package/lib/subagent-prompt.test.mjs +64 -0
- package/lib/subagent-prompt.ts +31 -0
- package/lib/subagent-queue.test.mjs +35 -0
- package/lib/subagent-queue.ts +74 -0
- package/lib/subagent-runtime.test.mjs +150 -0
- package/lib/subagent-runtime.ts +629 -0
- package/lib/subagent-session-title.test.mjs +120 -0
- package/lib/subagent-settings.test.mjs +50 -0
- package/lib/subagent-settings.ts +93 -0
- package/lib/subagents.test.mjs +601 -0
- package/lib/subagents.ts +602 -0
- package/lib/task-scheduler.test.mjs +27 -0
- package/lib/task-scheduler.ts +8 -1
- package/lib/task-store.activity.test.mjs +141 -0
- package/lib/task-store.meta-row.test.mjs +25 -5
- package/lib/task-store.partial-row.test.mjs +198 -0
- package/lib/task-store.subtree.test.mjs +172 -0
- package/lib/task-store.test.mjs +46 -20
- package/lib/task-store.ts +344 -68
- package/lib/tool-presets.ts +4 -0
- package/lib/types.ts +19 -0
- package/lib/worktree-isolation.test.mjs +51 -0
- package/next.config.ts +3 -1
- package/package.json +1 -1
- package/.next/static/bsW3fla0-BlBHVMKtAM5I/_buildManifest.js +0 -1
- package/.next/static/chunks/752.0ad92735d5f38143.js +0 -1
- package/.next/static/chunks/8088.53d9cdaf15634d4b.js +0 -1
- package/.next/static/chunks/app/_global-error/page-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/bash-output/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/agent/running/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/app-update/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/auth/all-providers/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/auth/api-key/[provider]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/auth/login/[provider]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/auth/logout/[provider]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/auth/providers/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/board-assets/[...path]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/board-assets/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/reconcile/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/boards/[id]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/boards/reorder/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/boards/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/cwd/browse/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/cwd/validate/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/default-cwd/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/file-index/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/files/[...path]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/git/diff/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/git/status/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/home/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/mcp/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/models/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/models-config/catalog/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/models-config/discover/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/models-config/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/models-config/test/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/project-trust/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/quota/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/search/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/context/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/minimap/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/state/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/todos/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/sessions/summary/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/skills/check/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/skills/install/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/skills/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/skills/search/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/skills/update/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/task-card-questions/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/task-cards/[id]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/task-cards/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/task-cards/unbind/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/task-scheduler/status/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/tasks/[id]/assign-session/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/tasks/[id]/board/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/tasks/[id]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/tasks/reorder/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/tasks/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/terminal/[id]/events/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/terminal/[id]/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/terminal/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/worktrees/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/api/yjs-version/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/layout-dace09825b2d3857.js +0 -1
- package/.next/static/chunks/app/manifest.webmanifest/route-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/app/page-64b9f4b3567a2f73.js +0 -89
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-2d5f6296c7e693c6.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-2d5f6296c7e693c6.js +0 -1
- package/.next/static/css/fd25673802257262.css +0 -1
- package/lib/extension-command.test.mjs +0 -63
- package/lib/extension-command.ts +0 -81
- package/lib/subagent-widget.test.mjs +0 -87
- package/lib/subagent-widget.ts +0 -109
- /package/.next/static/{bsW3fla0-BlBHVMKtAM5I → MPBMmQqmDG8zo8tM_bkDy}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
|
|
6
|
+
const jiti = createJiti(import.meta.url, {
|
|
7
|
+
interopDefault: true,
|
|
8
|
+
moduleCache: false,
|
|
9
|
+
alias: { "@": process.cwd() },
|
|
10
|
+
});
|
|
11
|
+
const { setDbForTesting } = await jiti.import("./sqlite-db.ts");
|
|
12
|
+
const {
|
|
13
|
+
LAST_REPLY_MAX,
|
|
14
|
+
fillFirstMessageIfEmpty,
|
|
15
|
+
recordSessionOutcome,
|
|
16
|
+
touchSessionActivity,
|
|
17
|
+
} = await jiti.import("./task-store.ts");
|
|
18
|
+
|
|
19
|
+
function freshDb() {
|
|
20
|
+
const db = new DatabaseSync(":memory:");
|
|
21
|
+
setDbForTesting(db);
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 造一条「扫描器/创建链路建立」的完整行:写函数只允许改自己那一列。 */
|
|
26
|
+
function seedRow(db, sessionId) {
|
|
27
|
+
db.prepare(
|
|
28
|
+
`INSERT INTO session_meta
|
|
29
|
+
(session_id, task_id, updated, pinned, path, cwd, project_key, title, first_message, parent_id, created, modified, last_reply)
|
|
30
|
+
VALUES (?, NULL, 111, 0, ?, '/p', 'proj', '标题', NULL, NULL, 100, 200, NULL)`,
|
|
31
|
+
).run(sessionId, `/s/${sessionId}.jsonl`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rowOf = (db, sessionId) =>
|
|
35
|
+
db.prepare("SELECT * FROM session_meta WHERE session_id = ?").get(sessionId);
|
|
36
|
+
const count = (db) => db.prepare("SELECT COUNT(*) AS c FROM session_meta").get().c;
|
|
37
|
+
|
|
38
|
+
test("touchSessionActivity 只刷 modified,不建行(无行时静默 no-op)", () => {
|
|
39
|
+
const db = freshDb();
|
|
40
|
+
seedRow(db, "s1");
|
|
41
|
+
|
|
42
|
+
touchSessionActivity("s1", 5000);
|
|
43
|
+
|
|
44
|
+
const row = rowOf(db, "s1");
|
|
45
|
+
assert.equal(row.modified, 5000);
|
|
46
|
+
// 其它列一律不动:活跃时间不是归属、不是上次改写时间、也不是内容。
|
|
47
|
+
assert.equal(row.updated, 111);
|
|
48
|
+
assert.equal(row.last_reply, null);
|
|
49
|
+
assert.equal(row.first_message, null);
|
|
50
|
+
assert.equal(row.path, "/s/s1.jsonl");
|
|
51
|
+
assert.equal(row.pinned, 0);
|
|
52
|
+
|
|
53
|
+
// 缺省参数 = 当前时间(事件链路不需要自己算时间戳)
|
|
54
|
+
const before = Date.now();
|
|
55
|
+
touchSessionActivity("s1");
|
|
56
|
+
assert.ok(rowOf(db, "s1").modified >= before);
|
|
57
|
+
|
|
58
|
+
// 库里没有这条会话 → 只影响 0 行,绝不因事件建局部行(V4 缺陷的回归锁)
|
|
59
|
+
touchSessionActivity("no-such-session", 9999);
|
|
60
|
+
assert.equal(count(db), 1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("recordSessionOutcome 写 last_reply + modified,超 4000 字符截断", () => {
|
|
64
|
+
const db = freshDb();
|
|
65
|
+
seedRow(db, "s2");
|
|
66
|
+
|
|
67
|
+
recordSessionOutcome("s2", { lastReply: "最后一条回复", at: 7000 });
|
|
68
|
+
|
|
69
|
+
let row = rowOf(db, "s2");
|
|
70
|
+
assert.equal(row.last_reply, "最后一条回复");
|
|
71
|
+
assert.equal(row.modified, 7000);
|
|
72
|
+
assert.equal(row.updated, 111, "结果落库不当作一次归属改写");
|
|
73
|
+
|
|
74
|
+
const long = "x".repeat(LAST_REPLY_MAX + 100);
|
|
75
|
+
recordSessionOutcome("s2", { lastReply: long, at: 8000 });
|
|
76
|
+
row = rowOf(db, "s2");
|
|
77
|
+
assert.equal(row.last_reply.length, LAST_REPLY_MAX);
|
|
78
|
+
assert.equal(row.last_reply, long.slice(0, LAST_REPLY_MAX));
|
|
79
|
+
assert.equal(row.modified, 8000);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("recordSessionOutcome 空文本不清库:只刷 modified,保留上一条回复", () => {
|
|
83
|
+
const db = freshDb();
|
|
84
|
+
seedRow(db, "s3");
|
|
85
|
+
recordSessionOutcome("s3", { lastReply: "上一条回复", at: 1000 });
|
|
86
|
+
|
|
87
|
+
// 刚发出就被取消 / 整轮只有工具调用:不许把 last_reply 写成空串
|
|
88
|
+
recordSessionOutcome("s3", { lastReply: "", at: 9000 });
|
|
89
|
+
let row = rowOf(db, "s3");
|
|
90
|
+
assert.equal(row.last_reply, "上一条回复");
|
|
91
|
+
assert.equal(row.modified, 9000);
|
|
92
|
+
|
|
93
|
+
recordSessionOutcome("s3", { lastReply: " \n\t ", at: 9500 });
|
|
94
|
+
row = rowOf(db, "s3");
|
|
95
|
+
assert.equal(row.last_reply, "上一条回复", "纯空白同样不算一条消息");
|
|
96
|
+
|
|
97
|
+
// 从没存过回复的行:空文本保持 NULL(而不是被写成 "")
|
|
98
|
+
seedRow(db, "s3b");
|
|
99
|
+
recordSessionOutcome("s3b", { lastReply: "", at: 9600 });
|
|
100
|
+
assert.equal(rowOf(db, "s3b").last_reply, null);
|
|
101
|
+
|
|
102
|
+
// 无行 → 0 行,不建行
|
|
103
|
+
recordSessionOutcome("no-such-session", { lastReply: "x", at: 1 });
|
|
104
|
+
assert.equal(count(db), 2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("fillFirstMessageIfEmpty 只在为空时写入(已有 first_message 不被覆盖)", () => {
|
|
108
|
+
const db = freshDb();
|
|
109
|
+
seedRow(db, "s4");
|
|
110
|
+
|
|
111
|
+
fillFirstMessageIfEmpty("s4", "第一条用户消息");
|
|
112
|
+
assert.equal(rowOf(db, "s4").first_message, "第一条用户消息");
|
|
113
|
+
|
|
114
|
+
fillFirstMessageIfEmpty("s4", "后来的消息");
|
|
115
|
+
assert.equal(rowOf(db, "s4").first_message, "第一条用户消息", "首条消息只落一次");
|
|
116
|
+
assert.equal(rowOf(db, "s4").modified, 200, "回填首条消息不动活跃时间");
|
|
117
|
+
|
|
118
|
+
// '' 视为空(老行可能被写成空串)→ 允许回填
|
|
119
|
+
db.prepare("UPDATE session_meta SET first_message = '' WHERE session_id = 's4'").run();
|
|
120
|
+
fillFirstMessageIfEmpty("s4", "补上");
|
|
121
|
+
assert.equal(rowOf(db, "s4").first_message, "补上");
|
|
122
|
+
|
|
123
|
+
// 空白首条消息不写(避免把空串当成内容)
|
|
124
|
+
seedRow(db, "s5");
|
|
125
|
+
fillFirstMessageIfEmpty("s5", " ");
|
|
126
|
+
assert.equal(rowOf(db, "s5").first_message, null);
|
|
127
|
+
|
|
128
|
+
// 无行 → 0 行,不建行
|
|
129
|
+
fillFirstMessageIfEmpty("no-such-session", "x");
|
|
130
|
+
assert.equal(count(db), 2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("三个写函数一律只 UPDATE:库里没行时不留任何新行", () => {
|
|
134
|
+
const db = freshDb();
|
|
135
|
+
|
|
136
|
+
touchSessionActivity("ghost", 1);
|
|
137
|
+
recordSessionOutcome("ghost", { lastReply: "回复", at: 2 });
|
|
138
|
+
fillFirstMessageIfEmpty("ghost", "首条");
|
|
139
|
+
|
|
140
|
+
assert.equal(count(db), 0);
|
|
141
|
+
});
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test, { after } from "node:test";
|
|
3
6
|
import { DatabaseSync } from "node:sqlite";
|
|
4
7
|
import { createJiti } from "jiti";
|
|
5
8
|
|
|
9
|
+
// getAgentDir() 每次调用都读环境变量 → 指向临时树,测试绝不碰 ~/.pi/agent。
|
|
10
|
+
// sessions 子目录先建好:无行会话补行时会按 id 在这里找文件,miss 不回落扫开发机真实目录。
|
|
11
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-web-meta-row-"));
|
|
12
|
+
mkdirSync(join(agentDir, "sessions"), { recursive: true });
|
|
13
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
14
|
+
after(() => rmSync(agentDir, { recursive: true, force: true }));
|
|
15
|
+
|
|
6
16
|
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
7
17
|
const { getDb, setDbForTesting } = await jiti.import("./sqlite-db.ts");
|
|
8
18
|
const { ensureSessionMetaRow, assignSessionToTask, setSessionTitle, unassignSession, createTask } = await jiti.import("./task-store.ts");
|
|
@@ -34,7 +44,7 @@ test("ensureSessionMetaRow 建全列索引行(新建会话落盘即建行)",
|
|
|
34
44
|
assert.ok(row.modified > 0);
|
|
35
45
|
});
|
|
36
46
|
|
|
37
|
-
test("ensureSessionMetaRow 后 assignSessionToTask 只补归属、不破坏索引列", () => {
|
|
47
|
+
test("ensureSessionMetaRow 后 assignSessionToTask 只补归属、不破坏索引列", async () => {
|
|
38
48
|
const db = freshDb();
|
|
39
49
|
ensureSessionMetaRow("sess-2", {
|
|
40
50
|
path: "/p2/sessions/sess-2.jsonl",
|
|
@@ -42,27 +52,37 @@ test("ensureSessionMetaRow 后 assignSessionToTask 只补归属、不破坏索
|
|
|
42
52
|
projectKey: "p2",
|
|
43
53
|
});
|
|
44
54
|
const task = createTask("p2", "t");
|
|
45
|
-
assert.equal(assignSessionToTask("sess-2", task.id), true);
|
|
55
|
+
assert.equal(await assignSessionToTask("sess-2", task.id), true);
|
|
46
56
|
const row = db.prepare("SELECT * FROM session_meta WHERE session_id = ?").get("sess-2");
|
|
47
57
|
assert.equal(row.task_id, task.id);
|
|
48
58
|
assert.equal(row.path, "/p2/sessions/sess-2.jsonl");
|
|
49
59
|
assert.equal(row.project_key, "p2");
|
|
50
60
|
});
|
|
51
61
|
|
|
52
|
-
test("ensureSessionMetaRow 后 setSessionTitle 只补标题、不破坏索引列", () => {
|
|
62
|
+
test("ensureSessionMetaRow 后 setSessionTitle 只补标题、不破坏索引列", async () => {
|
|
53
63
|
const db = freshDb();
|
|
54
64
|
ensureSessionMetaRow("sess-3", {
|
|
55
65
|
path: "/p3/sessions/sess-3.jsonl",
|
|
56
66
|
cwd: "/p3",
|
|
57
67
|
projectKey: "p3",
|
|
58
68
|
});
|
|
59
|
-
setSessionTitle("sess-3", "新标题");
|
|
69
|
+
await setSessionTitle("sess-3", "新标题");
|
|
60
70
|
const row = db.prepare("SELECT * FROM session_meta WHERE session_id = ?").get("sess-3");
|
|
61
71
|
assert.equal(row.title, "新标题");
|
|
62
72
|
assert.equal(row.path, "/p3/sessions/sess-3.jsonl");
|
|
63
73
|
assert.equal(row.project_key, "p3");
|
|
64
74
|
});
|
|
65
75
|
|
|
76
|
+
test("ensureSessionMetaRow 写全 13 列(last_reply 显式 NULL,不依赖建表默认值)", () => {
|
|
77
|
+
const db = freshDb();
|
|
78
|
+
ensureSessionMetaRow("sess-13", { path: "/p13/x.jsonl", cwd: "/p13", projectKey: "p13" });
|
|
79
|
+
const columns = db.prepare("PRAGMA table_info(session_meta)").all();
|
|
80
|
+
assert.equal(columns.length, 13, "session_meta 契约是 13 列(v12)");
|
|
81
|
+
const row = db.prepare("SELECT * FROM session_meta WHERE session_id = ?").get("sess-13");
|
|
82
|
+
assert.equal(Object.keys(row).length, 13, "行值对象覆盖全部列");
|
|
83
|
+
assert.equal(row.last_reply, null);
|
|
84
|
+
});
|
|
85
|
+
|
|
66
86
|
test("unassignSession 删行后 ensureSessionMetaRow 可重建(幂等)", () => {
|
|
67
87
|
const db = freshDb();
|
|
68
88
|
ensureSessionMetaRow("sess-4", { path: "/p4/x.jsonl", cwd: "/p4", projectKey: "p4" });
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test, { after } from "node:test";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
// getAgentDir() 每次调用都读环境变量 → 指向临时树,测试绝不碰 ~/.pi/agent。
|
|
10
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-web-partial-row-"));
|
|
11
|
+
const sessionsDir = join(agentDir, "sessions");
|
|
12
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
13
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
14
|
+
after(() => rmSync(agentDir, { recursive: true, force: true }));
|
|
15
|
+
|
|
16
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
17
|
+
const { getDb, setDbForTesting } = await jiti.import("./sqlite-db.ts");
|
|
18
|
+
const store = await jiti.import("./task-store.ts");
|
|
19
|
+
|
|
20
|
+
/** 造一个真实会话文件:文件名带 id(按名可解析)、header.id 与之一致(header 权威)。
|
|
21
|
+
* parentPath 非空时写 header.parentSession(父链只在磁盘上时用)。 */
|
|
22
|
+
function writeSession(project, id, parentPath) {
|
|
23
|
+
const dir = join(sessionsDir, project);
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
const file = join(dir, `2026-09-18T00-00-00_${id}.jsonl`);
|
|
26
|
+
writeFileSync(
|
|
27
|
+
file,
|
|
28
|
+
`${JSON.stringify({ type: "session", id, timestamp: "2026-09-18T00:00:00.000Z", cwd: join("/w", project), ...(parentPath ? { parentSession: parentPath } : {}) })}\n`,
|
|
29
|
+
);
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function freshDb() {
|
|
34
|
+
const db = new DatabaseSync(":memory:");
|
|
35
|
+
setDbForTesting(db);
|
|
36
|
+
return db;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rowFor(sessionId) {
|
|
40
|
+
return getDb().prepare("SELECT * FROM session_meta WHERE session_id = ?").get(sessionId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 全列断言:缺 path/project_key/created 的局部行会在聊天区被 project_key 过滤掉、
|
|
44
|
+
* 在任务区显示 1970 —— 正是本任务要根除的形态。 */
|
|
45
|
+
function assertFullRow(sessionId, filePath) {
|
|
46
|
+
const row = rowFor(sessionId);
|
|
47
|
+
assert.ok(row, `${sessionId} 必须建行`);
|
|
48
|
+
assert.equal(row.path, filePath);
|
|
49
|
+
assert.equal(row.cwd, "/w/alpha");
|
|
50
|
+
assert.ok(row.project_key, "project_key 不能为空(否则被聊天区 project_key 过滤踢掉)");
|
|
51
|
+
assert.ok(row.created > 0, "created 必须来自文件,不能是 0(1970)");
|
|
52
|
+
assert.ok(row.modified > 0, "modified 必须来自文件");
|
|
53
|
+
return row;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 断言 miss 场景的可见性时静音 console.error(测试输出保持干净),仍校验调用次数。 */
|
|
57
|
+
function captureConsoleError(t) {
|
|
58
|
+
const original = console.error;
|
|
59
|
+
const calls = [];
|
|
60
|
+
console.error = (...args) => {
|
|
61
|
+
calls.push(args.map(String).join(" "));
|
|
62
|
+
};
|
|
63
|
+
t.after(() => {
|
|
64
|
+
console.error = original;
|
|
65
|
+
});
|
|
66
|
+
return calls;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test("setSessionTitle 对无行会话建全列行(不再是只有 title 的局部行)", async () => {
|
|
70
|
+
freshDb();
|
|
71
|
+
const file = writeSession("alpha", "sess-title");
|
|
72
|
+
|
|
73
|
+
await store.setSessionTitle("sess-title", "新名字");
|
|
74
|
+
|
|
75
|
+
const row = assertFullRow("sess-title", file);
|
|
76
|
+
assert.equal(row.title, "新名字");
|
|
77
|
+
assert.equal(row.task_id, null);
|
|
78
|
+
assert.equal(row.pinned, 0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("setSessionPinned 对无行会话建全列行(不再是只有 pinned 的局部行)", async () => {
|
|
82
|
+
freshDb();
|
|
83
|
+
const file = writeSession("alpha", "sess-pin");
|
|
84
|
+
|
|
85
|
+
await store.setSessionPinned("sess-pin", true);
|
|
86
|
+
|
|
87
|
+
const row = assertFullRow("sess-pin", file);
|
|
88
|
+
assert.equal(row.pinned, 1);
|
|
89
|
+
assert.equal(row.task_id, null);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("assignSessionToTask 对无行会话先建全列行再写归属", async () => {
|
|
93
|
+
freshDb();
|
|
94
|
+
const file = writeSession("alpha", "sess-assign");
|
|
95
|
+
const task = store.createTask("alpha-proj", "任务");
|
|
96
|
+
|
|
97
|
+
assert.equal(await store.assignSessionToTask("sess-assign", task.id), true);
|
|
98
|
+
|
|
99
|
+
const row = assertFullRow("sess-assign", file);
|
|
100
|
+
assert.equal(row.task_id, task.id, "归属与本轮建行同一次写入完成");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("assignSessionSubtreeToTask 对无行会话同样先建全列行(子树归属不静默写 0 行)", async () => {
|
|
104
|
+
freshDb();
|
|
105
|
+
const file = writeSession("alpha", "sess-subtree");
|
|
106
|
+
const task = store.createTask("alpha-proj", "任务");
|
|
107
|
+
|
|
108
|
+
assert.equal(await store.assignSessionSubtreeToTask("sess-subtree", task.id), true);
|
|
109
|
+
|
|
110
|
+
const row = assertFullRow("sess-subtree", file);
|
|
111
|
+
assert.equal(row.task_id, task.id);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("已有行时不看文件:库里行在 → 改名/置顶照常写入(不因文件丢失而丢写)", async () => {
|
|
115
|
+
const db = freshDb();
|
|
116
|
+
db.prepare(
|
|
117
|
+
"INSERT INTO session_meta (session_id, task_id, updated, pinned, path, cwd, project_key, created, modified) VALUES (?, NULL, ?, 0, ?, ?, ?, ?, ?)",
|
|
118
|
+
).run("sess-row-only", Date.now(), "/gone/sess-row-only.jsonl", "/w/alpha", "/w/alpha", 1, 2);
|
|
119
|
+
|
|
120
|
+
await store.setSessionTitle("sess-row-only", "改名");
|
|
121
|
+
await store.setSessionPinned("sess-row-only", true);
|
|
122
|
+
|
|
123
|
+
const row = rowFor("sess-row-only");
|
|
124
|
+
assert.equal(row.title, "改名");
|
|
125
|
+
assert.equal(row.pinned, 1);
|
|
126
|
+
assert.equal(row.path, "/gone/sess-row-only.jsonl", "既有索引列不被覆盖");
|
|
127
|
+
assert.equal(row.project_key, "/w/alpha");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("库里没行、磁盘也没文件 → 不写缺列局部行,报错可见", async (t) => {
|
|
131
|
+
const db = freshDb();
|
|
132
|
+
const logs = captureConsoleError(t);
|
|
133
|
+
|
|
134
|
+
await store.setSessionTitle("sess-ghost", "幽灵改名");
|
|
135
|
+
await store.setSessionPinned("sess-ghost", true);
|
|
136
|
+
|
|
137
|
+
assert.equal(db.prepare("SELECT COUNT(*) c FROM session_meta").get().c, 0, "绝不退回插局部行");
|
|
138
|
+
assert.equal(logs.length, 2, "两次写库各报一次错(失败可见,不静默)");
|
|
139
|
+
for (const line of logs) assert.match(line, /sess-ghost/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── 补行时的 parent_id(归属守卫的可见性)────────────────────────────────────
|
|
143
|
+
// 守卫(hasForeignTaskAncestor)只看库内 parent_id:库里还没行的会话,父链只在磁盘
|
|
144
|
+
// header 里——补行时不写父,跨任务祖先对守卫就是盲区(无行会话会被当成顶层行放行)。
|
|
145
|
+
|
|
146
|
+
test("无行会话补行时写 parent_id:磁盘父属别的任务 → 归属被拒绝(与「有行」路径同结论)", async (t) => {
|
|
147
|
+
const db = freshDb();
|
|
148
|
+
const logs = captureConsoleError(t);
|
|
149
|
+
const parentFile = writeSession("alpha", "guard-parent");
|
|
150
|
+
const other = store.createTask("alpha-proj", "别的任务");
|
|
151
|
+
db.prepare(
|
|
152
|
+
"INSERT INTO session_meta (session_id, task_id, updated, pinned, path, cwd, project_key, parent_id, created, modified) VALUES ('guard-parent', ?, 1, 0, ?, '/w/alpha', '/w/alpha', NULL, 1, 1)",
|
|
153
|
+
).run(other.id, parentFile);
|
|
154
|
+
// 子会话:磁盘上有父指针,库里**没有行**(正是守卫此前看不见的那一类)。
|
|
155
|
+
const childFile = writeSession("alpha", "guard-child", parentFile);
|
|
156
|
+
const target = store.createTask("alpha-proj", "目标任务");
|
|
157
|
+
|
|
158
|
+
const assigned = await store.assignSessionSubtreeToTask("guard-child", target.id);
|
|
159
|
+
|
|
160
|
+
assert.equal(assigned, false, "祖先在别的任务 → 拒绝(返回 false 而不是静默归属)");
|
|
161
|
+
assert.equal(store.taskForSession("guard-child"), null, "拒绝 → 不写归属");
|
|
162
|
+
assert.equal(store.taskForSession("guard-parent"), other.id, "祖先归属不动");
|
|
163
|
+
assert.ok(
|
|
164
|
+
logs.some((line) => /祖先属于其它任务/.test(line)),
|
|
165
|
+
"拒绝可见(console.error)",
|
|
166
|
+
);
|
|
167
|
+
const row = rowFor("guard-child");
|
|
168
|
+
assert.ok(row, "补行照常完成(列齐全)");
|
|
169
|
+
assert.equal(row.path, childFile);
|
|
170
|
+
assert.equal(row.parent_id, "guard-parent", "补行时由磁盘 header.parentSession 反查写入父 id");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("无行会话补行写 parent_id 的正向面:磁盘父属同一任务 → 归属成功且父链保持", async () => {
|
|
174
|
+
const db = freshDb();
|
|
175
|
+
const parentFile = writeSession("alpha", "same-parent");
|
|
176
|
+
const task = store.createTask("alpha-proj", "同一个任务");
|
|
177
|
+
db.prepare(
|
|
178
|
+
"INSERT INTO session_meta (session_id, task_id, updated, pinned, path, cwd, project_key, parent_id, created, modified) VALUES ('same-parent', ?, 1, 0, ?, '/w/alpha', '/w/alpha', NULL, 1, 1)",
|
|
179
|
+
).run(task.id, parentFile);
|
|
180
|
+
writeSession("alpha", "same-child", parentFile);
|
|
181
|
+
|
|
182
|
+
assert.equal(await store.assignSessionSubtreeToTask("same-child", task.id), true);
|
|
183
|
+
const row = rowFor("same-child");
|
|
184
|
+
assert.equal(row.parent_id, "same-parent");
|
|
185
|
+
assert.equal(row.task_id, task.id, "同任务祖先不构成冲突");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("无行会话的父反查不到(父行未建/父文件已消失)→ parent_id 留空,不被当成冲突", async () => {
|
|
189
|
+
const db = freshDb();
|
|
190
|
+
writeSession("alpha", "orphan-child", join(sessionsDir, "alpha", "2026-09-18T00-00-00_gone.jsonl"));
|
|
191
|
+
const task = store.createTask("alpha-proj", "任务");
|
|
192
|
+
|
|
193
|
+
assert.equal(await store.assignSessionSubtreeToTask("orphan-child", task.id), true);
|
|
194
|
+
const row = rowFor("orphan-child");
|
|
195
|
+
assert.equal(row.parent_id, null, "反查不到 → undefined(不写悬空指针)");
|
|
196
|
+
assert.equal(row.task_id, task.id);
|
|
197
|
+
assert.equal(db.prepare("SELECT COUNT(*) c FROM session_meta").get().c, 1);
|
|
198
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import test, { after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
// getAgentDir() 每次调用都读环境变量 → 指向临时树,测试绝不碰 ~/.pi/agent。
|
|
10
|
+
// sessions 子目录先建好:无行会话补行时会按 id 在这里找文件,miss 不回落扫开发机真实目录。
|
|
11
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-web-subtree-"));
|
|
12
|
+
mkdirSync(join(agentDir, "sessions"), { recursive: true });
|
|
13
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
14
|
+
after(() => rmSync(agentDir, { recursive: true, force: true }));
|
|
15
|
+
|
|
16
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false, alias: { "@": process.cwd() } });
|
|
17
|
+
const { setDbForTesting } = await jiti.import("./sqlite-db.ts");
|
|
18
|
+
const store = await jiti.import("./task-store.ts");
|
|
19
|
+
|
|
20
|
+
function freshDb() {
|
|
21
|
+
const db = new DatabaseSync(":memory:");
|
|
22
|
+
setDbForTesting(db);
|
|
23
|
+
return db;
|
|
24
|
+
}
|
|
25
|
+
// 造一个「任务 T + 根 A + 子树 A<-B<-C」的库内结构
|
|
26
|
+
function seedTree(db) {
|
|
27
|
+
const task = store.createTask("/p", "T");
|
|
28
|
+
const row = (id, taskId, parentId) => db.prepare(
|
|
29
|
+
"INSERT INTO session_meta (session_id, task_id, updated, pinned, path, cwd, project_key, created, modified, parent_id) VALUES (?,?,?,0,?,?,?,?,?,?)",
|
|
30
|
+
).run(id, taskId, Date.now(), `/s/${id}.jsonl`, "/p", "/p", Date.now(), Date.now(), parentId);
|
|
31
|
+
row("A", null, null); row("B", null, "A"); row("C", null, "B");
|
|
32
|
+
return task;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("listDescendantIds 按 parent_id 递归(不含自身,含多层)", () => {
|
|
36
|
+
const db = freshDb(); seedTree(db);
|
|
37
|
+
assert.deepEqual(store.listDescendantIds("A").sort(), ["B", "C"]);
|
|
38
|
+
assert.deepEqual(store.listDescendantIds("C"), []);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("agent/new 之外的归属:assignSessionSubtreeToTask 连带子树", async () => {
|
|
42
|
+
const db = freshDb(); const task = seedTree(db);
|
|
43
|
+
assert.equal(await store.assignSessionSubtreeToTask("A", task.id), true);
|
|
44
|
+
const ids = db.prepare("SELECT session_id, task_id FROM session_meta ORDER BY session_id").all();
|
|
45
|
+
assert.deepEqual(ids.map((r) => [r.session_id, r.task_id]), [["A", task.id], ["B", task.id], ["C", task.id]]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("归属守卫:祖先属于别的任务 → 拒绝且不写任何行(子树/单节点入口一致)", async () => {
|
|
49
|
+
const db = freshDb(); const task = seedTree(db);
|
|
50
|
+
const other = store.createTask("/p", "Other");
|
|
51
|
+
// A 属于另一个任务 → C 的祖先链(C←B←A)上有非空且 ≠ 目标任务的 task_id
|
|
52
|
+
db.prepare("UPDATE session_meta SET task_id = ? WHERE session_id = 'A'").run(other.id);
|
|
53
|
+
const snapshot = () => db.prepare("SELECT session_id, task_id, updated FROM session_meta ORDER BY session_id").all()
|
|
54
|
+
.map((r) => [r.session_id, r.task_id, r.updated]);
|
|
55
|
+
const before = snapshot();
|
|
56
|
+
const errors = [];
|
|
57
|
+
const original = console.error;
|
|
58
|
+
console.error = (...args) => errors.push(args.map(String).join(" "));
|
|
59
|
+
try {
|
|
60
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), false, "子树入口拒绝");
|
|
61
|
+
assert.equal(await store.assignSessionToTask("C", task.id), false, "单节点入口拒绝");
|
|
62
|
+
} finally {
|
|
63
|
+
console.error = original;
|
|
64
|
+
}
|
|
65
|
+
assert.deepEqual(snapshot(), before, "不写任何行(归属与 updated 都不变)");
|
|
66
|
+
assert.equal(errors.length, 2, "拒绝必须 console.error 可见");
|
|
67
|
+
assert.deepEqual(
|
|
68
|
+
errors.map((e) => (/子树归属被拒绝/.test(e) ? "subtree" : "single")),
|
|
69
|
+
["subtree", "single"],
|
|
70
|
+
"子树入口与单节点入口各自给出可见日志",
|
|
71
|
+
);
|
|
72
|
+
// 悬空祖先(行已不在库)不算冲突:C 就是根
|
|
73
|
+
db.prepare("DELETE FROM session_meta WHERE session_id = 'A'").run();
|
|
74
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), true, "祖先行已不存在 → 允许(C 是根)");
|
|
75
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("归属守卫:祖先只是聊天区会话(task_id NULL)→ 正常归属", async () => {
|
|
79
|
+
const db = freshDb(); const task = seedTree(db);
|
|
80
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), true, "聊天区祖先不拦");
|
|
81
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
82
|
+
assert.equal(store.taskForSession("A"), null, "只归属自身 + 子树,祖先不动");
|
|
83
|
+
assert.equal(store.taskForSession("B"), null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("归属守卫:祖先就在同一任务 → 正常(幂等,不误拦)", async () => {
|
|
87
|
+
const db = freshDb(); const task = seedTree(db);
|
|
88
|
+
db.prepare("UPDATE session_meta SET task_id = ? WHERE session_id IN ('A','B')").run(task.id);
|
|
89
|
+
assert.equal(await store.assignSessionSubtreeToTask("C", task.id), true, "祖先同任务");
|
|
90
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
91
|
+
assert.equal(await store.assignSessionToTask("B", task.id), true, "重复归属幂等");
|
|
92
|
+
assert.equal(store.taskForSession("B"), task.id);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("reparentSessionChildren:子行 parent_id 同请求改写为祖父(或 NULL)", () => {
|
|
96
|
+
const db = freshDb(); seedTree(db);
|
|
97
|
+
store.reparentSessionChildren("B", "A");
|
|
98
|
+
assert.equal(db.prepare("SELECT parent_id FROM session_meta WHERE session_id='C'").get().parent_id, "A", "子行接到祖父");
|
|
99
|
+
store.reparentSessionChildren("A", null);
|
|
100
|
+
assert.equal(db.prepare("SELECT parent_id FROM session_meta WHERE session_id='B'").get().parent_id, null, "祖父拿不到 → NULL");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("updateTask 的 sessionIds 全量替换按子树闭包展开", () => {
|
|
104
|
+
const db = freshDb(); const task = seedTree(db);
|
|
105
|
+
store.updateTask(task.id, { sessionIds: ["A"] }); // 只给根,服务端要连带子树
|
|
106
|
+
assert.equal(store.taskForSession("B"), task.id);
|
|
107
|
+
store.updateTask(task.id, { sessionIds: [] }); // 清空 → 整棵子树回到聊天区
|
|
108
|
+
assert.equal(store.taskForSession("A"), null);
|
|
109
|
+
assert.equal(store.taskForSession("C"), null);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("移出带子树的父会话:前端提交剩余成员列表(含子 id)也整树离开", () => {
|
|
113
|
+
const db = freshDb(); const task = seedTree(db);
|
|
114
|
+
store.updateTask(task.id, { sessionIds: ["A"] }); // A/B/C 全在任务
|
|
115
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
116
|
+
// 前端拖出父 A 时提交的是「剩余成员列表」(flat sessionIds 去掉 A,仍含子 id)
|
|
117
|
+
store.updateTask(task.id, { sessionIds: ["B", "C"] });
|
|
118
|
+
assert.equal(store.taskForSession("A"), null, "父离开任务");
|
|
119
|
+
assert.equal(store.taskForSession("B"), null, "B 的祖先 A 不在成员集合里 → B 一起离开");
|
|
120
|
+
assert.equal(store.taskForSession("C"), null, "C 跟随 B 一起离开(多层)");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("祖先行已不存在(断链)时不剪:子会话仍可留在任务里当根", async () => {
|
|
124
|
+
const db = freshDb(); const task = seedTree(db);
|
|
125
|
+
await store.assignSessionSubtreeToTask("A", task.id);
|
|
126
|
+
// A 已被删(B 的 parent_id 悬空)——此窗口内不得把 B/C 踢出任务(它们就是本任务的根)
|
|
127
|
+
db.prepare("DELETE FROM session_meta WHERE session_id = 'A'").run();
|
|
128
|
+
store.updateTask(task.id, { sessionIds: ["B", "C"] });
|
|
129
|
+
assert.equal(store.taskForSession("B"), task.id);
|
|
130
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("子会话不能单独移出:祖先仍是成员时子会话保持在任务内", () => {
|
|
134
|
+
const db = freshDb(); const task = seedTree(db);
|
|
135
|
+
store.updateTask(task.id, { sessionIds: ["A"] }); // A/B/C 全在任务
|
|
136
|
+
// 前端拖出子会话 B 时提交的是 「flat 剩余成员列表」= filter(≠B) = ["A", "C"](含祖先 A),
|
|
137
|
+
// 闭包再把 B 纳入 → 成员集合不变。这是有意的不变量:子会话不能被单独移出任务。
|
|
138
|
+
store.updateTask(task.id, { sessionIds: ["A", "C"] });
|
|
139
|
+
assert.equal(store.taskForSession("A"), task.id, "祖先仍在任务");
|
|
140
|
+
assert.equal(store.taskForSession("B"), task.id, "B 不能单飞(否则聊天区出现孤儿根)");
|
|
141
|
+
assert.equal(store.taskForSession("C"), task.id);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("ensureSessionMetaRow 传 taskId 时落归属,不传时保持 NULL", () => {
|
|
145
|
+
freshDb();
|
|
146
|
+
store.ensureSessionMetaRow("X", { path: "/s/X.jsonl", cwd: "/p", projectKey: "/p", taskId: "T1" });
|
|
147
|
+
assert.equal(store.taskForSession("X"), "T1");
|
|
148
|
+
store.ensureSessionMetaRow("Y", { path: "/s/Y.jsonl", cwd: "/p", projectKey: "/p" });
|
|
149
|
+
assert.equal(store.taskForSession("Y"), null);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("ensureSessionMetaRow 冲突路径不覆盖已有归属(第二次传别的 taskId 仍保留首个)", () => {
|
|
153
|
+
const db = freshDb();
|
|
154
|
+
store.ensureSessionMetaRow("X", { path: "/s/X.jsonl", cwd: "/p", projectKey: "/p", taskId: "T1" });
|
|
155
|
+
store.ensureSessionMetaRow("X", { path: "/s/X2.jsonl", cwd: "/p2", projectKey: "/p2", taskId: "T2" });
|
|
156
|
+
assert.equal(store.taskForSession("X"), "T1", "归属各自管:upsert 不覆盖");
|
|
157
|
+
assert.equal(
|
|
158
|
+
db.prepare("SELECT path FROM session_meta WHERE session_id = 'X'").get().path,
|
|
159
|
+
"/s/X2.jsonl",
|
|
160
|
+
"索引列照常刷新",
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("ensureSessionMetaRow:不传 firstMessage 的 upsert 不擦已有首条消息(COALESCE)", () => {
|
|
165
|
+
const db = freshDb();
|
|
166
|
+
store.ensureSessionMetaRow("F", { path: "/s/F.jsonl", cwd: "/p", projectKey: "/p", firstMessage: "首条消息" });
|
|
167
|
+
// 同 id 再 upsert(如 fork/重新落盘)但不带 firstMessage:旧写法会把首条消息擦成 NULL
|
|
168
|
+
store.ensureSessionMetaRow("F", { path: "/s/F2.jsonl", cwd: "/p", projectKey: "/p" });
|
|
169
|
+
const row = db.prepare("SELECT first_message, path FROM session_meta WHERE session_id = 'F'").get();
|
|
170
|
+
assert.equal(row.first_message, "首条消息", "已有首条消息保留");
|
|
171
|
+
assert.equal(row.path, "/s/F2.jsonl", "索引列照常刷新");
|
|
172
|
+
});
|