vibepulse 0.2.2 → 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/fallback-build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/routes-manifest.json +8 -0
- package/.next/server/app/_global-error/page.js +1 -1
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.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.js.nft.json +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 +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/server/app/api/node/sessions/route.js +5 -3
- package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
- package/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
- package/.next/server/app/api/profiles/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
- package/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
- package/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
- package/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
- package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
- package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/sessions/route.js +4 -2
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +1 -0
- package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__98073dd6._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__d8e61048._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__f441109e._.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js.map +1 -0
- package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
- package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js.map → [root-of-the-server]__c91a8380._.js.map} +1 -1
- package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
- package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js.map +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +2 -2
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +8 -0
- package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/node/sessions/route.js +5 -3
- package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/profiles/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
- package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
- package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/sessions/route.js +4 -2
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
- package/.next/standalone/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +2 -2
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/b3bc362202331708.css +3 -0
- package/.next/standalone/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
- package/.next/standalone/README.md +29 -5
- package/.next/standalone/docs/session-status-detection.md +36 -0
- package/.next/standalone/docs/superpowers/specs/2026-04-09-claude-capability-alignment-design.md +39 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.test.ts +60 -1
- package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.ts +77 -22
- package/.next/standalone/src/app/api/node/sessions/route.test.ts +282 -0
- package/.next/standalone/src/app/api/node/sessions/route.ts +141 -17
- package/.next/standalone/src/app/api/opencode-events/route.test.ts +3 -1
- package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +101 -0
- package/.next/standalone/src/app/api/sessions/[id]/archive/route.ts +47 -12
- package/.next/standalone/src/app/api/sessions/[id]/delete/route.test.ts +92 -0
- package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +45 -10
- package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.test.ts +74 -0
- package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.ts +22 -2
- package/.next/standalone/src/app/api/sessions/[id]/restore/route.test.ts +186 -0
- package/.next/standalone/src/app/api/sessions/[id]/restore/route.ts +184 -0
- package/.next/standalone/src/app/api/sessions/route.test.ts +1889 -107
- package/.next/standalone/src/app/api/sessions/route.ts +365 -981
- package/.next/standalone/src/components/KanbanBoard.test.tsx +307 -1
- package/.next/standalone/src/components/KanbanBoard.tsx +105 -18
- package/.next/standalone/src/components/ProjectCard.test.tsx +416 -2
- package/.next/standalone/src/components/ProjectCard.tsx +238 -86
- package/.next/standalone/src/components/SessionCard.test.tsx +253 -2
- package/.next/standalone/src/components/SessionCard.tsx +182 -76
- package/.next/standalone/src/hooks/useOpencodeSync.test.ts +321 -1
- package/.next/standalone/src/hooks/useOpencodeSync.ts +16 -12
- package/.next/standalone/src/lib/claudeSessionOverrides.test.ts +75 -0
- package/.next/standalone/src/lib/claudeSessionOverrides.ts +169 -0
- package/.next/standalone/src/lib/session-providers/claudeCode.test.ts +2288 -0
- package/.next/standalone/src/lib/session-providers/claudeCode.ts +1083 -0
- package/.next/standalone/src/lib/session-providers/localAggregator.test.ts +322 -0
- package/.next/standalone/src/lib/session-providers/localAggregator.ts +302 -0
- package/.next/standalone/src/lib/session-providers/opencodeProvider.ts +723 -0
- package/.next/standalone/src/lib/session-providers/providerIds.test.ts +337 -0
- package/.next/standalone/src/lib/session-providers/providerIds.ts +176 -0
- package/.next/standalone/src/lib/session-providers/types.ts +131 -0
- package/.next/standalone/src/lib/transform.test.ts +253 -0
- package/.next/standalone/src/lib/transform.ts +96 -37
- package/.next/standalone/src/types/index.ts +23 -17
- package/.next/static/chunks/b3bc362202331708.css +3 -0
- package/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/.next/types/routes.d.ts +2 -1
- package/.next/types/validator.ts +9 -0
- package/README.md +29 -5
- package/package.json +1 -1
- package/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__2f981540._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__3745b314._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__6c428a24._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__73a00b88._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
- package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +0 -3
- package/.next/static/chunks/f42202943f6742e5.css +0 -3
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,2288 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { chmod, mkdir, mkdtemp, realpath, rm, stat, symlink, utimes, writeFile } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import * as claudeCodeModule from './claudeCode';
|
|
6
|
+
import {
|
|
7
|
+
discoverClaudeCodeSessions,
|
|
8
|
+
sanitizeClaudeProjectPath,
|
|
9
|
+
type ClaudeCodeDiscoveredSession,
|
|
10
|
+
type ClaudeCodeNormalizedSession,
|
|
11
|
+
} from './claudeCode';
|
|
12
|
+
|
|
13
|
+
vi.mock('@/lib/claudeSessionOverrides', () => ({
|
|
14
|
+
listClaudeSessionOverrides: vi.fn(async () => []),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
type ClaudeFixture = {
|
|
18
|
+
rootDir: string;
|
|
19
|
+
homeDir: string;
|
|
20
|
+
claudeDir: string;
|
|
21
|
+
projectsDir: string;
|
|
22
|
+
sessionsDir: string;
|
|
23
|
+
repoDir: string;
|
|
24
|
+
repoLinkPath: string;
|
|
25
|
+
otherRepoDir: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const SESSION_ONE = '550e8400-e29b-41d4-a716-446655440000';
|
|
29
|
+
const SESSION_TWO = '660e8400-e29b-41d4-a716-446655440000';
|
|
30
|
+
|
|
31
|
+
const fixtureRoots: string[] = [];
|
|
32
|
+
|
|
33
|
+
function makeDiscoveredSession(
|
|
34
|
+
overrides: Partial<ClaudeCodeDiscoveredSession> = {}
|
|
35
|
+
): ClaudeCodeDiscoveredSession {
|
|
36
|
+
return {
|
|
37
|
+
sessionId: overrides.sessionId ?? SESSION_ONE,
|
|
38
|
+
...(typeof overrides.title === 'string' ? { title: overrides.title } : {}),
|
|
39
|
+
cwd: overrides.cwd ?? '/tmp/current-worktree',
|
|
40
|
+
projectPath: overrides.projectPath ?? overrides.cwd ?? '/tmp/current-worktree',
|
|
41
|
+
projectName: overrides.projectName ?? 'current-worktree',
|
|
42
|
+
artifactPath: overrides.artifactPath ?? '/tmp/current-worktree/.claude/projects/session.jsonl',
|
|
43
|
+
gitBranch: overrides.gitBranch === undefined ? 'main' : overrides.gitBranch,
|
|
44
|
+
createdAt: overrides.createdAt ?? 1_700_000_000_000,
|
|
45
|
+
updatedAt: overrides.updatedAt ?? 1_700_000_000_500,
|
|
46
|
+
startedAt: overrides.startedAt,
|
|
47
|
+
pid: overrides.pid,
|
|
48
|
+
isRunning: overrides.isRunning ?? false,
|
|
49
|
+
waitingForUser: overrides.waitingForUser ?? false,
|
|
50
|
+
parentSessionId: overrides.parentSessionId,
|
|
51
|
+
topology: overrides.topology,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getNormalizeClaudeCodeSessions():
|
|
56
|
+
| ((sessions: ClaudeCodeDiscoveredSession[]) => ClaudeCodeNormalizedSession[])
|
|
57
|
+
| undefined {
|
|
58
|
+
return (claudeCodeModule as unknown as {
|
|
59
|
+
normalizeClaudeCodeSessions?: (sessions: ClaudeCodeDiscoveredSession[]) => ClaudeCodeNormalizedSession[];
|
|
60
|
+
}).normalizeClaudeCodeSessions;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function createFixture(): Promise<ClaudeFixture> {
|
|
64
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'vibepulse-claude-provider-'));
|
|
65
|
+
fixtureRoots.push(rootDir);
|
|
66
|
+
|
|
67
|
+
const homeDir = join(rootDir, 'home');
|
|
68
|
+
const claudeDir = join(homeDir, '.claude');
|
|
69
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
70
|
+
const sessionsDir = join(claudeDir, 'sessions');
|
|
71
|
+
const repoDir = join(rootDir, 'repos', 'current-worktree');
|
|
72
|
+
const repoLinkPath = join(rootDir, 'repos', 'current-link');
|
|
73
|
+
const otherRepoDir = join(rootDir, 'repos', 'different-worktree');
|
|
74
|
+
|
|
75
|
+
await mkdir(projectsDir, { recursive: true });
|
|
76
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
77
|
+
await mkdir(repoDir, { recursive: true });
|
|
78
|
+
await mkdir(otherRepoDir, { recursive: true });
|
|
79
|
+
await symlink(repoDir, repoLinkPath);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
rootDir,
|
|
83
|
+
homeDir,
|
|
84
|
+
claudeDir,
|
|
85
|
+
projectsDir,
|
|
86
|
+
sessionsDir,
|
|
87
|
+
repoDir,
|
|
88
|
+
repoLinkPath,
|
|
89
|
+
otherRepoDir,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function buildProjectDir(projectsDir: string, repoPath: string): Promise<string> {
|
|
94
|
+
return join(projectsDir, sanitizeClaudeProjectPath(await realpath(repoPath)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createJsonlHead(params: {
|
|
98
|
+
sessionId: string;
|
|
99
|
+
cwd: string;
|
|
100
|
+
gitBranch?: string;
|
|
101
|
+
timestamp?: string;
|
|
102
|
+
parentSessionId?: string;
|
|
103
|
+
}): string {
|
|
104
|
+
return [
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
type: 'file-history-snapshot',
|
|
107
|
+
snapshot: {
|
|
108
|
+
timestamp: params.timestamp ?? '2026-04-09T18:20:00.000Z',
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
cwd: params.cwd,
|
|
113
|
+
sessionId: params.sessionId,
|
|
114
|
+
...(typeof params.parentSessionId === 'string' ? { parentSessionId: params.parentSessionId } : {}),
|
|
115
|
+
gitBranch: params.gitBranch ?? 'main',
|
|
116
|
+
timestamp: params.timestamp ?? '2026-04-09T18:21:00.000Z',
|
|
117
|
+
type: 'user',
|
|
118
|
+
message: { role: 'user', content: 'hello' },
|
|
119
|
+
}),
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function writeProjectArtifact(params: {
|
|
124
|
+
projectsDir: string;
|
|
125
|
+
repoPath: string;
|
|
126
|
+
sessionId: string;
|
|
127
|
+
jsonlContent?: string;
|
|
128
|
+
originalPath?: string;
|
|
129
|
+
}): Promise<string> {
|
|
130
|
+
const projectDir = await buildProjectDir(params.projectsDir, params.repoPath);
|
|
131
|
+
await mkdir(projectDir, { recursive: true });
|
|
132
|
+
await writeFile(join(projectDir, 'sessions-index.json'), JSON.stringify({
|
|
133
|
+
version: 1,
|
|
134
|
+
entries: [],
|
|
135
|
+
originalPath: params.originalPath ?? params.repoPath,
|
|
136
|
+
}, null, 2));
|
|
137
|
+
|
|
138
|
+
const artifactPath = join(projectDir, `${params.sessionId}.jsonl`);
|
|
139
|
+
await writeFile(
|
|
140
|
+
artifactPath,
|
|
141
|
+
params.jsonlContent ?? createJsonlHead({ sessionId: params.sessionId, cwd: params.repoPath })
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return artifactPath;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function writeSubagentArtifact(params: {
|
|
148
|
+
projectsDir: string;
|
|
149
|
+
repoPath: string;
|
|
150
|
+
parentSessionId: string;
|
|
151
|
+
agentId: string;
|
|
152
|
+
timestamp?: string;
|
|
153
|
+
}): Promise<string> {
|
|
154
|
+
const projectDir = await buildProjectDir(params.projectsDir, params.repoPath);
|
|
155
|
+
const subagentsDir = join(projectDir, params.parentSessionId, 'subagents');
|
|
156
|
+
await mkdir(subagentsDir, { recursive: true });
|
|
157
|
+
|
|
158
|
+
const artifactPath = join(subagentsDir, `agent-${params.agentId}.jsonl`);
|
|
159
|
+
const timestamp = params.timestamp ?? new Date().toISOString();
|
|
160
|
+
|
|
161
|
+
await writeFile(
|
|
162
|
+
artifactPath,
|
|
163
|
+
[
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
type: 'user',
|
|
166
|
+
isSidechain: true,
|
|
167
|
+
agentId: params.agentId,
|
|
168
|
+
cwd: params.repoPath,
|
|
169
|
+
sessionId: params.parentSessionId,
|
|
170
|
+
gitBranch: 'main',
|
|
171
|
+
timestamp,
|
|
172
|
+
message: { role: 'user', content: 'delegated data analysis' },
|
|
173
|
+
}),
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
type: 'assistant',
|
|
176
|
+
isSidechain: true,
|
|
177
|
+
agentId: params.agentId,
|
|
178
|
+
cwd: params.repoPath,
|
|
179
|
+
sessionId: params.parentSessionId,
|
|
180
|
+
gitBranch: 'main',
|
|
181
|
+
timestamp,
|
|
182
|
+
message: { role: 'assistant', stop_reason: 'end_turn', content: [{ type: 'text', text: 'done' }] },
|
|
183
|
+
}),
|
|
184
|
+
].join('\n')
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return artifactPath;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function writeSessionIndexEntry(params: {
|
|
191
|
+
sessionsDir: string;
|
|
192
|
+
pid: number;
|
|
193
|
+
sessionId: string;
|
|
194
|
+
cwd: string;
|
|
195
|
+
startedAt?: number;
|
|
196
|
+
}): Promise<void> {
|
|
197
|
+
await writeFile(join(params.sessionsDir, `${params.pid}.json`), JSON.stringify({
|
|
198
|
+
pid: params.pid,
|
|
199
|
+
sessionId: params.sessionId,
|
|
200
|
+
cwd: params.cwd,
|
|
201
|
+
...(typeof params.startedAt === 'number' ? { startedAt: params.startedAt } : {}),
|
|
202
|
+
kind: 'interactive',
|
|
203
|
+
entrypoint: 'cli',
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
afterEach(async () => {
|
|
208
|
+
vi.restoreAllMocks();
|
|
209
|
+
|
|
210
|
+
while (fixtureRoots.length > 0) {
|
|
211
|
+
const rootDir = fixtureRoots.pop();
|
|
212
|
+
if (!rootDir) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('discoverClaudeCodeSessions', () => {
|
|
221
|
+
it('returns an empty result when the Claude artifact tree is missing', async () => {
|
|
222
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'vibepulse-claude-missing-'));
|
|
223
|
+
fixtureRoots.push(rootDir);
|
|
224
|
+
const repoDir = join(rootDir, 'repo');
|
|
225
|
+
const homeDir = join(rootDir, 'home');
|
|
226
|
+
|
|
227
|
+
await mkdir(repoDir, { recursive: true });
|
|
228
|
+
|
|
229
|
+
await expect(discoverClaudeCodeSessions({ repoPath: repoDir, homeDir })).resolves.toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('discovers Claude project artifacts globally across local repos while preserving current-repo discovery and sidecar enrichment', async () => {
|
|
233
|
+
const fixture = await createFixture();
|
|
234
|
+
|
|
235
|
+
await writeProjectArtifact({
|
|
236
|
+
projectsDir: fixture.projectsDir,
|
|
237
|
+
repoPath: fixture.repoDir,
|
|
238
|
+
sessionId: SESSION_ONE,
|
|
239
|
+
jsonlContent: createJsonlHead({
|
|
240
|
+
sessionId: SESSION_ONE,
|
|
241
|
+
cwd: fixture.repoDir,
|
|
242
|
+
gitBranch: 'feature/current',
|
|
243
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await writeProjectArtifact({
|
|
248
|
+
projectsDir: fixture.projectsDir,
|
|
249
|
+
repoPath: fixture.otherRepoDir,
|
|
250
|
+
sessionId: SESSION_TWO,
|
|
251
|
+
jsonlContent: createJsonlHead({
|
|
252
|
+
sessionId: SESSION_TWO,
|
|
253
|
+
cwd: fixture.otherRepoDir,
|
|
254
|
+
gitBranch: 'feature/other',
|
|
255
|
+
timestamp: '2026-04-09T18:23:00.000Z',
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await writeSessionIndexEntry({
|
|
260
|
+
sessionsDir: fixture.sessionsDir,
|
|
261
|
+
pid: 12345,
|
|
262
|
+
sessionId: SESSION_ONE,
|
|
263
|
+
cwd: fixture.repoDir,
|
|
264
|
+
startedAt: 1_700_000_000_123,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await writeSessionIndexEntry({
|
|
268
|
+
sessionsDir: fixture.sessionsDir,
|
|
269
|
+
pid: 23456,
|
|
270
|
+
sessionId: SESSION_TWO,
|
|
271
|
+
cwd: fixture.otherRepoDir,
|
|
272
|
+
startedAt: 1_700_000_000_456,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const isPidAlive = vi.fn((pid: number) => pid === 12345 || pid === 23456);
|
|
276
|
+
|
|
277
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
278
|
+
repoPath: fixture.repoLinkPath,
|
|
279
|
+
homeDir: fixture.homeDir,
|
|
280
|
+
isPidAlive,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(sessions).toHaveLength(2);
|
|
284
|
+
expect(sessions).toEqual(expect.arrayContaining([
|
|
285
|
+
expect.objectContaining({
|
|
286
|
+
sessionId: SESSION_ONE,
|
|
287
|
+
cwd: await realpath(fixture.repoDir),
|
|
288
|
+
projectPath: await realpath(fixture.repoDir),
|
|
289
|
+
projectName: 'current-worktree',
|
|
290
|
+
gitBranch: 'feature/current',
|
|
291
|
+
pid: 12345,
|
|
292
|
+
startedAt: 1_700_000_000_123,
|
|
293
|
+
isRunning: true,
|
|
294
|
+
}),
|
|
295
|
+
expect.objectContaining({
|
|
296
|
+
sessionId: SESSION_TWO,
|
|
297
|
+
cwd: await realpath(fixture.otherRepoDir),
|
|
298
|
+
projectPath: await realpath(fixture.otherRepoDir),
|
|
299
|
+
projectName: 'different-worktree',
|
|
300
|
+
gitBranch: 'feature/other',
|
|
301
|
+
pid: 23456,
|
|
302
|
+
startedAt: 1_700_000_000_456,
|
|
303
|
+
isRunning: true,
|
|
304
|
+
}),
|
|
305
|
+
]));
|
|
306
|
+
const currentRepoSession = sessions.find((session) => session.sessionId === SESSION_ONE);
|
|
307
|
+
const externalRepoSession = sessions.find((session) => session.sessionId === SESSION_TWO);
|
|
308
|
+
|
|
309
|
+
expect(currentRepoSession).toMatchObject({
|
|
310
|
+
sessionId: SESSION_ONE,
|
|
311
|
+
cwd: await realpath(fixture.repoDir),
|
|
312
|
+
projectPath: await realpath(fixture.repoDir),
|
|
313
|
+
projectName: 'current-worktree',
|
|
314
|
+
gitBranch: 'feature/current',
|
|
315
|
+
pid: 12345,
|
|
316
|
+
startedAt: 1_700_000_000_123,
|
|
317
|
+
isRunning: true,
|
|
318
|
+
});
|
|
319
|
+
expect(currentRepoSession?.artifactPath).toBe(
|
|
320
|
+
join(await buildProjectDir(fixture.projectsDir, fixture.repoDir), `${SESSION_ONE}.jsonl`)
|
|
321
|
+
);
|
|
322
|
+
expect(externalRepoSession?.artifactPath).toBe(
|
|
323
|
+
join(await buildProjectDir(fixture.projectsDir, fixture.otherRepoDir), `${SESSION_TWO}.jsonl`)
|
|
324
|
+
);
|
|
325
|
+
expect(isPidAlive).toHaveBeenCalledWith(12345);
|
|
326
|
+
expect(isPidAlive).toHaveBeenCalledWith(23456);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('discovers a current-repo Claude artifact without a matching sidecar and still uses sidecars for enrichment when present', async () => {
|
|
330
|
+
const fixture = await createFixture();
|
|
331
|
+
|
|
332
|
+
await writeProjectArtifact({
|
|
333
|
+
projectsDir: fixture.projectsDir,
|
|
334
|
+
repoPath: fixture.repoDir,
|
|
335
|
+
sessionId: SESSION_ONE,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const sessionsWithoutCandidateIndex = await discoverClaudeCodeSessions({
|
|
339
|
+
repoPath: fixture.repoDir,
|
|
340
|
+
homeDir: fixture.homeDir,
|
|
341
|
+
isPidAlive: () => false,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(sessionsWithoutCandidateIndex).toHaveLength(1);
|
|
345
|
+
expect(sessionsWithoutCandidateIndex[0]).toMatchObject({
|
|
346
|
+
sessionId: SESSION_ONE,
|
|
347
|
+
cwd: await realpath(fixture.repoDir),
|
|
348
|
+
projectPath: await realpath(fixture.repoDir),
|
|
349
|
+
projectName: 'current-worktree',
|
|
350
|
+
gitBranch: 'main',
|
|
351
|
+
isRunning: false,
|
|
352
|
+
});
|
|
353
|
+
expect(sessionsWithoutCandidateIndex[0]?.pid).toBeUndefined();
|
|
354
|
+
expect(sessionsWithoutCandidateIndex[0]?.startedAt).toBeUndefined();
|
|
355
|
+
|
|
356
|
+
await rm(join(await buildProjectDir(fixture.projectsDir, fixture.repoDir), `${SESSION_ONE}.jsonl`), { force: true });
|
|
357
|
+
|
|
358
|
+
await writeSessionIndexEntry({
|
|
359
|
+
sessionsDir: fixture.sessionsDir,
|
|
360
|
+
pid: 45678,
|
|
361
|
+
sessionId: SESSION_ONE,
|
|
362
|
+
cwd: fixture.repoDir,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const sessionsWithoutProjectArtifact = await discoverClaudeCodeSessions({
|
|
366
|
+
repoPath: fixture.repoDir,
|
|
367
|
+
homeDir: fixture.homeDir,
|
|
368
|
+
isPidAlive: () => false,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(sessionsWithoutProjectArtifact).toEqual([]);
|
|
372
|
+
|
|
373
|
+
await writeProjectArtifact({
|
|
374
|
+
projectsDir: fixture.projectsDir,
|
|
375
|
+
repoPath: fixture.repoDir,
|
|
376
|
+
sessionId: SESSION_ONE,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const sessionsWithDeadPid = await discoverClaudeCodeSessions({
|
|
380
|
+
repoPath: fixture.repoDir,
|
|
381
|
+
homeDir: fixture.homeDir,
|
|
382
|
+
isPidAlive: () => false,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(sessionsWithDeadPid).toHaveLength(1);
|
|
386
|
+
expect(sessionsWithDeadPid[0]).toMatchObject({
|
|
387
|
+
sessionId: SESSION_ONE,
|
|
388
|
+
isRunning: false,
|
|
389
|
+
});
|
|
390
|
+
expect(sessionsWithDeadPid[0]?.pid).toBeUndefined();
|
|
391
|
+
expect(sessionsWithDeadPid[0]?.startedAt).toBeUndefined();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('skips noisy IDE-opened-file narration when deriving Claude session titles', async () => {
|
|
395
|
+
const fixture = await createFixture();
|
|
396
|
+
|
|
397
|
+
await writeProjectArtifact({
|
|
398
|
+
projectsDir: fixture.projectsDir,
|
|
399
|
+
repoPath: fixture.repoDir,
|
|
400
|
+
sessionId: SESSION_ONE,
|
|
401
|
+
jsonlContent: [
|
|
402
|
+
JSON.stringify({
|
|
403
|
+
type: 'user',
|
|
404
|
+
cwd: fixture.repoDir,
|
|
405
|
+
sessionId: SESSION_ONE,
|
|
406
|
+
gitBranch: 'feature/current',
|
|
407
|
+
timestamp: '2026-04-09T18:21:00.000Z',
|
|
408
|
+
message: {
|
|
409
|
+
role: 'user',
|
|
410
|
+
content: '<ide_opened_file>The user opened /repo/project-one/ga4_analysis.py</ide_opened_file>',
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
type: 'user',
|
|
415
|
+
cwd: fixture.repoDir,
|
|
416
|
+
sessionId: SESSION_ONE,
|
|
417
|
+
gitBranch: 'feature/current',
|
|
418
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
419
|
+
message: {
|
|
420
|
+
role: 'user',
|
|
421
|
+
content: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况',
|
|
422
|
+
},
|
|
423
|
+
}),
|
|
424
|
+
].join('\n'),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
428
|
+
repoPath: fixture.repoDir,
|
|
429
|
+
homeDir: fixture.homeDir,
|
|
430
|
+
isPidAlive: () => false,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(sessions).toHaveLength(1);
|
|
434
|
+
expect(sessions[0]).toMatchObject({
|
|
435
|
+
sessionId: SESSION_ONE,
|
|
436
|
+
title: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况',
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('skips noisy IDE-opened-file narration when user content is emitted as text parts array', async () => {
|
|
441
|
+
const fixture = await createFixture();
|
|
442
|
+
|
|
443
|
+
await writeProjectArtifact({
|
|
444
|
+
projectsDir: fixture.projectsDir,
|
|
445
|
+
repoPath: fixture.repoDir,
|
|
446
|
+
sessionId: SESSION_ONE,
|
|
447
|
+
jsonlContent: [
|
|
448
|
+
JSON.stringify({
|
|
449
|
+
type: 'user',
|
|
450
|
+
cwd: fixture.repoDir,
|
|
451
|
+
sessionId: SESSION_ONE,
|
|
452
|
+
gitBranch: 'feature/current',
|
|
453
|
+
timestamp: '2026-04-09T18:21:00.000Z',
|
|
454
|
+
message: {
|
|
455
|
+
role: 'user',
|
|
456
|
+
content: [
|
|
457
|
+
{ type: 'text', text: '<ide_opened_file>The user opened /repo/project-one/ga4_analysis.py</ide_opened_file>' },
|
|
458
|
+
],
|
|
459
|
+
},
|
|
460
|
+
}),
|
|
461
|
+
JSON.stringify({
|
|
462
|
+
type: 'user',
|
|
463
|
+
cwd: fixture.repoDir,
|
|
464
|
+
sessionId: SESSION_ONE,
|
|
465
|
+
gitBranch: 'feature/current',
|
|
466
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
467
|
+
message: {
|
|
468
|
+
role: 'user',
|
|
469
|
+
content: [
|
|
470
|
+
{ type: 'text', text: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(array)' },
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
}),
|
|
474
|
+
].join('\n'),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
478
|
+
repoPath: fixture.repoDir,
|
|
479
|
+
homeDir: fixture.homeDir,
|
|
480
|
+
isPidAlive: () => false,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(sessions).toHaveLength(1);
|
|
484
|
+
expect(sessions[0]).toMatchObject({
|
|
485
|
+
sessionId: SESSION_ONE,
|
|
486
|
+
title: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(array)',
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('uses meaningful text from the same first user event when that event also contains noisy ide_opened_file text', async () => {
|
|
491
|
+
const fixture = await createFixture();
|
|
492
|
+
|
|
493
|
+
await writeProjectArtifact({
|
|
494
|
+
projectsDir: fixture.projectsDir,
|
|
495
|
+
repoPath: fixture.repoDir,
|
|
496
|
+
sessionId: SESSION_ONE,
|
|
497
|
+
jsonlContent: [
|
|
498
|
+
JSON.stringify({
|
|
499
|
+
type: 'user',
|
|
500
|
+
cwd: fixture.repoDir,
|
|
501
|
+
sessionId: SESSION_ONE,
|
|
502
|
+
gitBranch: 'feature/current',
|
|
503
|
+
timestamp: '2026-04-09T18:21:00.000Z',
|
|
504
|
+
message: {
|
|
505
|
+
role: 'user',
|
|
506
|
+
content: [
|
|
507
|
+
{
|
|
508
|
+
type: 'text',
|
|
509
|
+
text: '<ide_opened_file>The user opened the file /Users/admin/code/labs/ai-workers/data-analysis/ga4_analysis.py in the IDE. This may or may not be related to the current task.</ide_opened_file>',
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
type: 'text',
|
|
513
|
+
text: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(same-event-part)',
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
JSON.stringify({
|
|
519
|
+
type: 'user',
|
|
520
|
+
cwd: fixture.repoDir,
|
|
521
|
+
sessionId: SESSION_ONE,
|
|
522
|
+
gitBranch: 'feature/current',
|
|
523
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
524
|
+
message: {
|
|
525
|
+
role: 'user',
|
|
526
|
+
content: [{ type: 'text', text: '营收情况呢' }],
|
|
527
|
+
},
|
|
528
|
+
}),
|
|
529
|
+
].join('\n'),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
533
|
+
repoPath: fixture.repoDir,
|
|
534
|
+
homeDir: fixture.homeDir,
|
|
535
|
+
isPidAlive: () => false,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
expect(sessions).toHaveLength(1);
|
|
539
|
+
expect(sessions[0]).toMatchObject({
|
|
540
|
+
sessionId: SESSION_ONE,
|
|
541
|
+
title: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(same-event-part)',
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('skips multiple consecutive noisy IDE narration events before using first meaningful user title', async () => {
|
|
546
|
+
const fixture = await createFixture();
|
|
547
|
+
|
|
548
|
+
await writeProjectArtifact({
|
|
549
|
+
projectsDir: fixture.projectsDir,
|
|
550
|
+
repoPath: fixture.repoDir,
|
|
551
|
+
sessionId: SESSION_ONE,
|
|
552
|
+
jsonlContent: [
|
|
553
|
+
JSON.stringify({
|
|
554
|
+
type: 'user',
|
|
555
|
+
cwd: fixture.repoDir,
|
|
556
|
+
sessionId: SESSION_ONE,
|
|
557
|
+
gitBranch: 'feature/current',
|
|
558
|
+
timestamp: '2026-04-09T18:21:00.000Z',
|
|
559
|
+
message: {
|
|
560
|
+
role: 'user',
|
|
561
|
+
content: '<ide_opened_file>The user opened /repo/project-one/ga4_analysis.py</ide_opened_file>',
|
|
562
|
+
},
|
|
563
|
+
}),
|
|
564
|
+
JSON.stringify({
|
|
565
|
+
type: 'user',
|
|
566
|
+
cwd: fixture.repoDir,
|
|
567
|
+
sessionId: SESSION_ONE,
|
|
568
|
+
gitBranch: 'feature/current',
|
|
569
|
+
timestamp: '2026-04-09T18:21:30.000Z',
|
|
570
|
+
message: {
|
|
571
|
+
role: 'user',
|
|
572
|
+
content: '<ide_opened_file>The user opened /repo/project-one/config.yaml</ide_opened_file>',
|
|
573
|
+
},
|
|
574
|
+
}),
|
|
575
|
+
JSON.stringify({
|
|
576
|
+
type: 'user',
|
|
577
|
+
cwd: fixture.repoDir,
|
|
578
|
+
sessionId: SESSION_ONE,
|
|
579
|
+
gitBranch: 'feature/current',
|
|
580
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
581
|
+
message: {
|
|
582
|
+
role: 'user',
|
|
583
|
+
content: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(real-title)',
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
].join('\n'),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
590
|
+
repoPath: fixture.repoDir,
|
|
591
|
+
homeDir: fixture.homeDir,
|
|
592
|
+
isPidAlive: () => false,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(sessions).toHaveLength(1);
|
|
596
|
+
expect(sessions[0]).toMatchObject({
|
|
597
|
+
sessionId: SESSION_ONE,
|
|
598
|
+
title: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(real-title)',
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('keeps the first meaningful user input as Claude session title', async () => {
|
|
603
|
+
const fixture = await createFixture();
|
|
604
|
+
|
|
605
|
+
await writeProjectArtifact({
|
|
606
|
+
projectsDir: fixture.projectsDir,
|
|
607
|
+
repoPath: fixture.repoDir,
|
|
608
|
+
sessionId: SESSION_ONE,
|
|
609
|
+
jsonlContent: [
|
|
610
|
+
JSON.stringify({
|
|
611
|
+
type: 'user',
|
|
612
|
+
cwd: fixture.repoDir,
|
|
613
|
+
sessionId: SESSION_ONE,
|
|
614
|
+
gitBranch: 'feature/current',
|
|
615
|
+
timestamp: '2026-04-09T18:20:00.000Z',
|
|
616
|
+
message: {
|
|
617
|
+
role: 'user',
|
|
618
|
+
content: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况',
|
|
619
|
+
},
|
|
620
|
+
}),
|
|
621
|
+
JSON.stringify({
|
|
622
|
+
type: 'assistant',
|
|
623
|
+
cwd: fixture.repoDir,
|
|
624
|
+
sessionId: SESSION_ONE,
|
|
625
|
+
gitBranch: 'feature/current',
|
|
626
|
+
timestamp: '2026-04-09T18:21:00.000Z',
|
|
627
|
+
message: {
|
|
628
|
+
role: 'assistant',
|
|
629
|
+
stop_reason: 'end_turn',
|
|
630
|
+
content: [{ type: 'text', text: '这是阶段性分析结果' }],
|
|
631
|
+
},
|
|
632
|
+
}),
|
|
633
|
+
JSON.stringify({
|
|
634
|
+
type: 'user',
|
|
635
|
+
cwd: fixture.repoDir,
|
|
636
|
+
sessionId: SESSION_ONE,
|
|
637
|
+
gitBranch: 'feature/current',
|
|
638
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
639
|
+
message: {
|
|
640
|
+
role: 'user',
|
|
641
|
+
content: '继续',
|
|
642
|
+
},
|
|
643
|
+
}),
|
|
644
|
+
].join('\n'),
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
648
|
+
repoPath: fixture.repoDir,
|
|
649
|
+
homeDir: fixture.homeDir,
|
|
650
|
+
isPidAlive: () => false,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(sessions).toHaveLength(1);
|
|
654
|
+
expect(sessions[0]).toMatchObject({
|
|
655
|
+
sessionId: SESSION_ONE,
|
|
656
|
+
title: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况',
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('ignores a noisy latest user event and keeps the previous meaningful user title', async () => {
|
|
661
|
+
const fixture = await createFixture();
|
|
662
|
+
|
|
663
|
+
await writeProjectArtifact({
|
|
664
|
+
projectsDir: fixture.projectsDir,
|
|
665
|
+
repoPath: fixture.repoDir,
|
|
666
|
+
sessionId: SESSION_ONE,
|
|
667
|
+
jsonlContent: [
|
|
668
|
+
JSON.stringify({
|
|
669
|
+
type: 'user',
|
|
670
|
+
cwd: fixture.repoDir,
|
|
671
|
+
sessionId: SESSION_ONE,
|
|
672
|
+
gitBranch: 'feature/current',
|
|
673
|
+
timestamp: '2026-04-09T18:20:00.000Z',
|
|
674
|
+
message: {
|
|
675
|
+
role: 'user',
|
|
676
|
+
content: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(fallback-previous)',
|
|
677
|
+
},
|
|
678
|
+
}),
|
|
679
|
+
JSON.stringify({
|
|
680
|
+
type: 'user',
|
|
681
|
+
cwd: fixture.repoDir,
|
|
682
|
+
sessionId: SESSION_ONE,
|
|
683
|
+
gitBranch: 'feature/current',
|
|
684
|
+
timestamp: '2026-04-09T18:21:00.000Z',
|
|
685
|
+
message: {
|
|
686
|
+
role: 'user',
|
|
687
|
+
content: '<ide_opened_file>The user opened /repo/project-one/ga4_analysis.py</ide_opened_file>',
|
|
688
|
+
},
|
|
689
|
+
}),
|
|
690
|
+
].join('\n'),
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
694
|
+
repoPath: fixture.repoDir,
|
|
695
|
+
homeDir: fixture.homeDir,
|
|
696
|
+
isPidAlive: () => false,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(sessions).toHaveLength(1);
|
|
700
|
+
expect(sessions[0]).toMatchObject({
|
|
701
|
+
sessionId: SESSION_ONE,
|
|
702
|
+
title: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(fallback-previous)',
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('keeps legitimate prompts that start with "The user opened" when they are not path narration', async () => {
|
|
707
|
+
const fixture = await createFixture();
|
|
708
|
+
|
|
709
|
+
await writeProjectArtifact({
|
|
710
|
+
projectsDir: fixture.projectsDir,
|
|
711
|
+
repoPath: fixture.repoDir,
|
|
712
|
+
sessionId: SESSION_ONE,
|
|
713
|
+
jsonlContent: [
|
|
714
|
+
JSON.stringify({
|
|
715
|
+
type: 'user',
|
|
716
|
+
cwd: fixture.repoDir,
|
|
717
|
+
sessionId: SESSION_ONE,
|
|
718
|
+
gitBranch: 'feature/current',
|
|
719
|
+
timestamp: '2026-04-09T18:20:00.000Z',
|
|
720
|
+
message: {
|
|
721
|
+
role: 'user',
|
|
722
|
+
content: 'The user opened discussion about GA4 revenue anomalies this month',
|
|
723
|
+
},
|
|
724
|
+
}),
|
|
725
|
+
].join('\n'),
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
729
|
+
repoPath: fixture.repoDir,
|
|
730
|
+
homeDir: fixture.homeDir,
|
|
731
|
+
isPidAlive: () => false,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
expect(sessions).toHaveLength(1);
|
|
735
|
+
expect(sessions[0]).toMatchObject({
|
|
736
|
+
sessionId: SESSION_ONE,
|
|
737
|
+
title: 'The user opened discussion about GA4 revenue anomalies this month',
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('recovers title from expanded head scan when default head window only contains noisy IDE narration', async () => {
|
|
742
|
+
const fixture = await createFixture();
|
|
743
|
+
|
|
744
|
+
const fillerEvents = Array.from({ length: 120 }, (_, index) => JSON.stringify({
|
|
745
|
+
type: 'assistant',
|
|
746
|
+
cwd: fixture.repoDir,
|
|
747
|
+
sessionId: SESSION_ONE,
|
|
748
|
+
gitBranch: 'feature/current',
|
|
749
|
+
timestamp: `2026-04-09T18:21:${String(index % 60).padStart(2, '0')}.000Z`,
|
|
750
|
+
message: {
|
|
751
|
+
role: 'assistant',
|
|
752
|
+
stop_reason: 'end_turn',
|
|
753
|
+
content: [{ type: 'text', text: `filler-${index}` }],
|
|
754
|
+
},
|
|
755
|
+
}));
|
|
756
|
+
|
|
757
|
+
await writeProjectArtifact({
|
|
758
|
+
projectsDir: fixture.projectsDir,
|
|
759
|
+
repoPath: fixture.repoDir,
|
|
760
|
+
sessionId: SESSION_ONE,
|
|
761
|
+
jsonlContent: [
|
|
762
|
+
JSON.stringify({
|
|
763
|
+
type: 'user',
|
|
764
|
+
cwd: fixture.repoDir,
|
|
765
|
+
sessionId: SESSION_ONE,
|
|
766
|
+
gitBranch: 'feature/current',
|
|
767
|
+
timestamp: '2026-04-09T18:20:00.000Z',
|
|
768
|
+
message: {
|
|
769
|
+
role: 'user',
|
|
770
|
+
content: '<ide_opened_file>The user opened /repo/project-one/ga4_analysis.py</ide_opened_file>',
|
|
771
|
+
},
|
|
772
|
+
}),
|
|
773
|
+
...fillerEvents,
|
|
774
|
+
JSON.stringify({
|
|
775
|
+
type: 'user',
|
|
776
|
+
cwd: fixture.repoDir,
|
|
777
|
+
sessionId: SESSION_ONE,
|
|
778
|
+
gitBranch: 'feature/current',
|
|
779
|
+
timestamp: '2026-04-09T18:23:00.000Z',
|
|
780
|
+
message: {
|
|
781
|
+
role: 'user',
|
|
782
|
+
content: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(head-recovered)',
|
|
783
|
+
},
|
|
784
|
+
}),
|
|
785
|
+
].join('\n'),
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
789
|
+
repoPath: fixture.repoDir,
|
|
790
|
+
homeDir: fixture.homeDir,
|
|
791
|
+
isPidAlive: () => false,
|
|
792
|
+
jsonlHeadLimitBytes: 256,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
expect(sessions).toHaveLength(1);
|
|
796
|
+
expect(sessions[0]).toMatchObject({
|
|
797
|
+
sessionId: SESSION_ONE,
|
|
798
|
+
title: '研究近 90 天 chatree 话树趣聊(非 mix)的趋势情况(head-recovered)',
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('ignores malformed or unreadable Claude artifacts without throwing', async () => {
|
|
803
|
+
const fixture = await createFixture();
|
|
804
|
+
|
|
805
|
+
await writeProjectArtifact({
|
|
806
|
+
projectsDir: fixture.projectsDir,
|
|
807
|
+
repoPath: fixture.repoDir,
|
|
808
|
+
sessionId: SESSION_ONE,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
await writeSessionIndexEntry({
|
|
812
|
+
sessionsDir: fixture.sessionsDir,
|
|
813
|
+
pid: 12345,
|
|
814
|
+
sessionId: SESSION_ONE,
|
|
815
|
+
cwd: fixture.repoDir,
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const unreadableArtifactPath = await writeProjectArtifact({
|
|
819
|
+
projectsDir: fixture.projectsDir,
|
|
820
|
+
repoPath: fixture.repoDir,
|
|
821
|
+
sessionId: SESSION_TWO,
|
|
822
|
+
jsonlContent: createJsonlHead({ sessionId: SESSION_TWO, cwd: fixture.repoDir }),
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
await chmod(unreadableArtifactPath, 0o000);
|
|
826
|
+
|
|
827
|
+
await writeFile(join(fixture.sessionsDir, 'bad.json'), '{not-json');
|
|
828
|
+
const otherProjectDir = await buildProjectDir(fixture.projectsDir, fixture.otherRepoDir);
|
|
829
|
+
await mkdir(otherProjectDir, { recursive: true });
|
|
830
|
+
await writeFile(join(otherProjectDir, 'sessions-index.json'), '{bad-json');
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
await expect(discoverClaudeCodeSessions({
|
|
834
|
+
repoPath: fixture.repoDir,
|
|
835
|
+
homeDir: fixture.homeDir,
|
|
836
|
+
isPidAlive: () => false,
|
|
837
|
+
})).resolves.toMatchObject([
|
|
838
|
+
{
|
|
839
|
+
sessionId: SESSION_ONE,
|
|
840
|
+
isRunning: false,
|
|
841
|
+
},
|
|
842
|
+
]);
|
|
843
|
+
} finally {
|
|
844
|
+
await chmod(unreadableArtifactPath, 0o644);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('keeps stale transcript-only Claude artifacts visible as idle sessions', async () => {
|
|
849
|
+
const fixture = await createFixture();
|
|
850
|
+
|
|
851
|
+
const artifactPath = await writeProjectArtifact({
|
|
852
|
+
projectsDir: fixture.projectsDir,
|
|
853
|
+
repoPath: fixture.repoDir,
|
|
854
|
+
sessionId: SESSION_ONE,
|
|
855
|
+
jsonlContent: createJsonlHead({
|
|
856
|
+
sessionId: SESSION_ONE,
|
|
857
|
+
cwd: fixture.repoDir,
|
|
858
|
+
timestamp: '2025-01-01T00:00:00.000Z',
|
|
859
|
+
}),
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
await writeSessionIndexEntry({
|
|
863
|
+
sessionsDir: fixture.sessionsDir,
|
|
864
|
+
pid: 45678,
|
|
865
|
+
sessionId: SESSION_ONE,
|
|
866
|
+
cwd: fixture.repoDir,
|
|
867
|
+
startedAt: 1_700_000_000_456,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
const staleDate = new Date('2025-01-01T00:00:00.000Z');
|
|
871
|
+
await utimes(artifactPath, staleDate, staleDate);
|
|
872
|
+
|
|
873
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
874
|
+
repoPath: fixture.repoDir,
|
|
875
|
+
homeDir: fixture.homeDir,
|
|
876
|
+
isPidAlive: () => true,
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
expect(sessions).toHaveLength(1);
|
|
880
|
+
expect(sessions[0]).toMatchObject({
|
|
881
|
+
sessionId: SESSION_ONE,
|
|
882
|
+
isRunning: false,
|
|
883
|
+
waitingForUser: false,
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('keeps recent artifact-backed Claude sessions idle when sidecar liveness is only a bare pid check', async () => {
|
|
888
|
+
const fixture = await createFixture();
|
|
889
|
+
|
|
890
|
+
await writeProjectArtifact({
|
|
891
|
+
projectsDir: fixture.projectsDir,
|
|
892
|
+
repoPath: fixture.repoDir,
|
|
893
|
+
sessionId: SESSION_ONE,
|
|
894
|
+
jsonlContent: createJsonlHead({
|
|
895
|
+
sessionId: SESSION_ONE,
|
|
896
|
+
cwd: fixture.repoDir,
|
|
897
|
+
gitBranch: 'feature/current',
|
|
898
|
+
timestamp: '2026-04-09T18:24:00.000Z',
|
|
899
|
+
}),
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
await writeSessionIndexEntry({
|
|
903
|
+
sessionsDir: fixture.sessionsDir,
|
|
904
|
+
pid: 56789,
|
|
905
|
+
sessionId: SESSION_ONE,
|
|
906
|
+
cwd: fixture.repoDir,
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
910
|
+
repoPath: fixture.repoDir,
|
|
911
|
+
homeDir: fixture.homeDir,
|
|
912
|
+
isPidAlive: () => true,
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
expect(sessions).toHaveLength(1);
|
|
916
|
+
expect(sessions[0]).toMatchObject({
|
|
917
|
+
sessionId: SESSION_ONE,
|
|
918
|
+
cwd: await realpath(fixture.repoDir),
|
|
919
|
+
projectPath: await realpath(fixture.repoDir),
|
|
920
|
+
projectName: 'current-worktree',
|
|
921
|
+
gitBranch: 'feature/current',
|
|
922
|
+
isRunning: false,
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('prefers live sidecar metadata when duplicate session sidecars exist', async () => {
|
|
927
|
+
const fixture = await createFixture();
|
|
928
|
+
|
|
929
|
+
const artifactPath = await writeProjectArtifact({
|
|
930
|
+
projectsDir: fixture.projectsDir,
|
|
931
|
+
repoPath: fixture.repoDir,
|
|
932
|
+
sessionId: SESSION_ONE,
|
|
933
|
+
jsonlContent: createJsonlHead({
|
|
934
|
+
sessionId: SESSION_ONE,
|
|
935
|
+
cwd: fixture.repoDir,
|
|
936
|
+
gitBranch: 'feature/current',
|
|
937
|
+
timestamp: new Date().toISOString(),
|
|
938
|
+
}),
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
await writeSessionIndexEntry({
|
|
942
|
+
sessionsDir: fixture.sessionsDir,
|
|
943
|
+
pid: 12345,
|
|
944
|
+
sessionId: SESSION_ONE,
|
|
945
|
+
cwd: fixture.repoDir,
|
|
946
|
+
startedAt: Date.now() - 5_000,
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
await writeSessionIndexEntry({
|
|
950
|
+
sessionsDir: fixture.sessionsDir,
|
|
951
|
+
pid: 99999,
|
|
952
|
+
sessionId: SESSION_ONE,
|
|
953
|
+
cwd: fixture.repoDir,
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const nowDate = new Date();
|
|
957
|
+
await utimes(artifactPath, nowDate, nowDate);
|
|
958
|
+
|
|
959
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
960
|
+
repoPath: fixture.repoDir,
|
|
961
|
+
homeDir: fixture.homeDir,
|
|
962
|
+
isPidAlive: (pid) => pid === 12345,
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
expect(sessions).toHaveLength(1);
|
|
966
|
+
expect(sessions[0]).toMatchObject({
|
|
967
|
+
sessionId: SESSION_ONE,
|
|
968
|
+
pid: 12345,
|
|
969
|
+
isRunning: true,
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('treats an alive Claude process with stale-enough artifact activity as idle instead of busy', async () => {
|
|
974
|
+
const fixture = await createFixture();
|
|
975
|
+
|
|
976
|
+
const artifactPath = await writeProjectArtifact({
|
|
977
|
+
projectsDir: fixture.projectsDir,
|
|
978
|
+
repoPath: fixture.repoDir,
|
|
979
|
+
sessionId: SESSION_ONE,
|
|
980
|
+
jsonlContent: createJsonlHead({
|
|
981
|
+
sessionId: SESSION_ONE,
|
|
982
|
+
cwd: fixture.repoDir,
|
|
983
|
+
gitBranch: 'feature/current',
|
|
984
|
+
timestamp: new Date(Date.now() - 90_000).toISOString(),
|
|
985
|
+
}),
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
await writeSessionIndexEntry({
|
|
989
|
+
sessionsDir: fixture.sessionsDir,
|
|
990
|
+
pid: 56789,
|
|
991
|
+
sessionId: SESSION_ONE,
|
|
992
|
+
cwd: fixture.repoDir,
|
|
993
|
+
startedAt: Date.now() - 5_000_000,
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
const staleRecentDate = new Date(Date.now() - 90_000);
|
|
997
|
+
await utimes(artifactPath, staleRecentDate, staleRecentDate);
|
|
998
|
+
|
|
999
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1000
|
+
repoPath: fixture.repoDir,
|
|
1001
|
+
homeDir: fixture.homeDir,
|
|
1002
|
+
isPidAlive: () => true,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
expect(sessions).toHaveLength(1);
|
|
1006
|
+
expect(sessions[0]).toMatchObject({
|
|
1007
|
+
sessionId: SESSION_ONE,
|
|
1008
|
+
isRunning: false,
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('drops Claude busy to idle shortly after activity becomes older than the short busy window', async () => {
|
|
1013
|
+
const fixture = await createFixture();
|
|
1014
|
+
|
|
1015
|
+
const artifactPath = await writeProjectArtifact({
|
|
1016
|
+
projectsDir: fixture.projectsDir,
|
|
1017
|
+
repoPath: fixture.repoDir,
|
|
1018
|
+
sessionId: SESSION_ONE,
|
|
1019
|
+
jsonlContent: createJsonlHead({
|
|
1020
|
+
sessionId: SESSION_ONE,
|
|
1021
|
+
cwd: fixture.repoDir,
|
|
1022
|
+
gitBranch: 'feature/current',
|
|
1023
|
+
timestamp: new Date(Date.now() - 12_000).toISOString(),
|
|
1024
|
+
}),
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
await writeSessionIndexEntry({
|
|
1028
|
+
sessionsDir: fixture.sessionsDir,
|
|
1029
|
+
pid: 56789,
|
|
1030
|
+
sessionId: SESSION_ONE,
|
|
1031
|
+
cwd: fixture.repoDir,
|
|
1032
|
+
startedAt: Date.now() - 5_000_000,
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const slightlyStaleDate = new Date(Date.now() - 12_000);
|
|
1036
|
+
await utimes(artifactPath, slightlyStaleDate, slightlyStaleDate);
|
|
1037
|
+
|
|
1038
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1039
|
+
repoPath: fixture.repoDir,
|
|
1040
|
+
homeDir: fixture.homeDir,
|
|
1041
|
+
isPidAlive: () => true,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
expect(sessions).toHaveLength(1);
|
|
1045
|
+
expect(sessions[0]).toMatchObject({
|
|
1046
|
+
sessionId: SESSION_ONE,
|
|
1047
|
+
isRunning: false,
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('does not keep waiting-for-user when the latest assistant turn is a question but no live process evidence exists', async () => {
|
|
1052
|
+
const fixture = await createFixture();
|
|
1053
|
+
|
|
1054
|
+
await writeProjectArtifact({
|
|
1055
|
+
projectsDir: fixture.projectsDir,
|
|
1056
|
+
repoPath: fixture.repoDir,
|
|
1057
|
+
sessionId: SESSION_ONE,
|
|
1058
|
+
jsonlContent: [
|
|
1059
|
+
JSON.stringify({
|
|
1060
|
+
type: 'user',
|
|
1061
|
+
message: { role: 'user', content: 'Should I continue?' },
|
|
1062
|
+
cwd: fixture.repoDir,
|
|
1063
|
+
sessionId: SESSION_ONE,
|
|
1064
|
+
timestamp: new Date(Date.now() - 30_000).toISOString(),
|
|
1065
|
+
gitBranch: 'feature/current',
|
|
1066
|
+
}),
|
|
1067
|
+
JSON.stringify({
|
|
1068
|
+
type: 'assistant',
|
|
1069
|
+
message: {
|
|
1070
|
+
role: 'assistant',
|
|
1071
|
+
stop_reason: 'end_turn',
|
|
1072
|
+
content: [{ type: 'text', text: 'I found two approaches. Which one do you want?' }],
|
|
1073
|
+
},
|
|
1074
|
+
cwd: fixture.repoDir,
|
|
1075
|
+
sessionId: SESSION_ONE,
|
|
1076
|
+
timestamp: new Date(Date.now() - 10_000).toISOString(),
|
|
1077
|
+
gitBranch: 'feature/current',
|
|
1078
|
+
}),
|
|
1079
|
+
].join('\n'),
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1083
|
+
repoPath: fixture.repoDir,
|
|
1084
|
+
homeDir: fixture.homeDir,
|
|
1085
|
+
isPidAlive: () => false,
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
expect(sessions).toHaveLength(1);
|
|
1089
|
+
expect(sessions[0]).toMatchObject({
|
|
1090
|
+
sessionId: SESSION_ONE,
|
|
1091
|
+
waitingForUser: false,
|
|
1092
|
+
isRunning: false,
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it('does not keep waiting-for-user for pending tool_use approval when no live process evidence exists', async () => {
|
|
1097
|
+
const fixture = await createFixture();
|
|
1098
|
+
|
|
1099
|
+
await writeProjectArtifact({
|
|
1100
|
+
projectsDir: fixture.projectsDir,
|
|
1101
|
+
repoPath: fixture.repoDir,
|
|
1102
|
+
sessionId: SESSION_ONE,
|
|
1103
|
+
jsonlContent: [
|
|
1104
|
+
JSON.stringify({
|
|
1105
|
+
type: 'user',
|
|
1106
|
+
message: { role: 'user', content: '头条热点呢' },
|
|
1107
|
+
cwd: fixture.repoDir,
|
|
1108
|
+
sessionId: SESSION_ONE,
|
|
1109
|
+
timestamp: new Date(Date.now() - 30_000).toISOString(),
|
|
1110
|
+
gitBranch: 'feature/current',
|
|
1111
|
+
permissionMode: 'default',
|
|
1112
|
+
}),
|
|
1113
|
+
JSON.stringify({
|
|
1114
|
+
type: 'assistant',
|
|
1115
|
+
message: {
|
|
1116
|
+
role: 'assistant',
|
|
1117
|
+
stop_reason: 'tool_use',
|
|
1118
|
+
content: [{ type: 'tool_use', id: 'tool_1', name: 'Fetch', input: { url: 'https://example.com' } }],
|
|
1119
|
+
},
|
|
1120
|
+
cwd: fixture.repoDir,
|
|
1121
|
+
sessionId: SESSION_ONE,
|
|
1122
|
+
timestamp: new Date(Date.now() - 10_000).toISOString(),
|
|
1123
|
+
gitBranch: 'feature/current',
|
|
1124
|
+
}),
|
|
1125
|
+
].join('\n'),
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1129
|
+
repoPath: fixture.repoDir,
|
|
1130
|
+
homeDir: fixture.homeDir,
|
|
1131
|
+
isPidAlive: () => false,
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
expect(sessions).toHaveLength(1);
|
|
1135
|
+
expect(sessions[0]).toMatchObject({
|
|
1136
|
+
sessionId: SESSION_ONE,
|
|
1137
|
+
waitingForUser: false,
|
|
1138
|
+
isRunning: false,
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('keeps waiting-for-user when transcript is valid even with live process evidence', async () => {
|
|
1143
|
+
const fixture = await createFixture();
|
|
1144
|
+
|
|
1145
|
+
await writeProjectArtifact({
|
|
1146
|
+
projectsDir: fixture.projectsDir,
|
|
1147
|
+
repoPath: fixture.repoDir,
|
|
1148
|
+
sessionId: SESSION_ONE,
|
|
1149
|
+
jsonlContent: [
|
|
1150
|
+
JSON.stringify({
|
|
1151
|
+
type: 'user',
|
|
1152
|
+
message: { role: 'user', content: 'Need a network fetch' },
|
|
1153
|
+
cwd: fixture.repoDir,
|
|
1154
|
+
sessionId: SESSION_ONE,
|
|
1155
|
+
timestamp: new Date(Date.now() - 30_000).toISOString(),
|
|
1156
|
+
gitBranch: 'feature/current',
|
|
1157
|
+
}),
|
|
1158
|
+
JSON.stringify({
|
|
1159
|
+
type: 'assistant',
|
|
1160
|
+
message: {
|
|
1161
|
+
role: 'assistant',
|
|
1162
|
+
stop_reason: 'tool_use',
|
|
1163
|
+
content: [{ type: 'tool_use', id: 'tool_1', name: 'Fetch', input: { url: 'https://example.com' } }],
|
|
1164
|
+
},
|
|
1165
|
+
cwd: fixture.repoDir,
|
|
1166
|
+
sessionId: SESSION_ONE,
|
|
1167
|
+
timestamp: new Date(Date.now() - 5_000).toISOString(),
|
|
1168
|
+
gitBranch: 'feature/current',
|
|
1169
|
+
}),
|
|
1170
|
+
].join('\n'),
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
await writeSessionIndexEntry({
|
|
1174
|
+
sessionsDir: fixture.sessionsDir,
|
|
1175
|
+
pid: 77777,
|
|
1176
|
+
sessionId: SESSION_ONE,
|
|
1177
|
+
cwd: fixture.repoDir,
|
|
1178
|
+
startedAt: Date.now() - 100_000,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1182
|
+
repoPath: fixture.repoDir,
|
|
1183
|
+
homeDir: fixture.homeDir,
|
|
1184
|
+
isPidAlive: (pid) => pid === 77777,
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
expect(sessions).toHaveLength(1);
|
|
1188
|
+
expect(sessions[0]).toMatchObject({
|
|
1189
|
+
sessionId: SESSION_ONE,
|
|
1190
|
+
pid: 77777,
|
|
1191
|
+
waitingForUser: true,
|
|
1192
|
+
isRunning: false,
|
|
1193
|
+
});
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it('does not mark Claude sessions as waiting when a tool_use has already completed with a later tool_result', async () => {
|
|
1197
|
+
const fixture = await createFixture();
|
|
1198
|
+
|
|
1199
|
+
await writeProjectArtifact({
|
|
1200
|
+
projectsDir: fixture.projectsDir,
|
|
1201
|
+
repoPath: fixture.repoDir,
|
|
1202
|
+
sessionId: SESSION_ONE,
|
|
1203
|
+
jsonlContent: [
|
|
1204
|
+
JSON.stringify({
|
|
1205
|
+
type: 'assistant',
|
|
1206
|
+
message: {
|
|
1207
|
+
role: 'assistant',
|
|
1208
|
+
stop_reason: 'tool_use',
|
|
1209
|
+
content: [{ type: 'tool_use', id: 'tool_1', name: 'Fetch', input: { url: 'https://example.com' } }],
|
|
1210
|
+
},
|
|
1211
|
+
cwd: fixture.repoDir,
|
|
1212
|
+
sessionId: SESSION_ONE,
|
|
1213
|
+
timestamp: new Date(Date.now() - 20_000).toISOString(),
|
|
1214
|
+
gitBranch: 'feature/current',
|
|
1215
|
+
}),
|
|
1216
|
+
JSON.stringify({
|
|
1217
|
+
type: 'user',
|
|
1218
|
+
message: { role: 'user', content: [{ type: 'tool_result', content: 'done' }] },
|
|
1219
|
+
cwd: fixture.repoDir,
|
|
1220
|
+
sessionId: SESSION_ONE,
|
|
1221
|
+
timestamp: new Date(Date.now() - 10_000).toISOString(),
|
|
1222
|
+
gitBranch: 'feature/current',
|
|
1223
|
+
}),
|
|
1224
|
+
].join('\n'),
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1228
|
+
repoPath: fixture.repoDir,
|
|
1229
|
+
homeDir: fixture.homeDir,
|
|
1230
|
+
isPidAlive: () => false,
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
expect(sessions).toHaveLength(1);
|
|
1234
|
+
expect(sessions[0]).toMatchObject({
|
|
1235
|
+
sessionId: SESSION_ONE,
|
|
1236
|
+
waitingForUser: false,
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
it('clears stale waiting markers when transcript tail has an in-progress write and live process evidence', async () => {
|
|
1241
|
+
const fixture = await createFixture();
|
|
1242
|
+
|
|
1243
|
+
await writeProjectArtifact({
|
|
1244
|
+
projectsDir: fixture.projectsDir,
|
|
1245
|
+
repoPath: fixture.repoDir,
|
|
1246
|
+
sessionId: SESSION_ONE,
|
|
1247
|
+
jsonlContent: [
|
|
1248
|
+
JSON.stringify({
|
|
1249
|
+
type: 'user',
|
|
1250
|
+
message: { role: 'user', content: 'Continue the task' },
|
|
1251
|
+
cwd: fixture.repoDir,
|
|
1252
|
+
sessionId: SESSION_ONE,
|
|
1253
|
+
timestamp: new Date(Date.now() - 40_000).toISOString(),
|
|
1254
|
+
gitBranch: 'feature/current',
|
|
1255
|
+
}),
|
|
1256
|
+
JSON.stringify({
|
|
1257
|
+
type: 'assistant',
|
|
1258
|
+
message: {
|
|
1259
|
+
role: 'assistant',
|
|
1260
|
+
stop_reason: 'tool_use',
|
|
1261
|
+
content: [{ type: 'tool_use', id: 'tool_1', name: 'Fetch', input: { url: 'https://example.com' } }],
|
|
1262
|
+
},
|
|
1263
|
+
cwd: fixture.repoDir,
|
|
1264
|
+
sessionId: SESSION_ONE,
|
|
1265
|
+
timestamp: new Date(Date.now() - 20_000).toISOString(),
|
|
1266
|
+
gitBranch: 'feature/current',
|
|
1267
|
+
}),
|
|
1268
|
+
'{"type":"assistant","message":',
|
|
1269
|
+
].join('\n'),
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
await writeSessionIndexEntry({
|
|
1273
|
+
sessionsDir: fixture.sessionsDir,
|
|
1274
|
+
pid: 56789,
|
|
1275
|
+
sessionId: SESSION_ONE,
|
|
1276
|
+
cwd: fixture.repoDir,
|
|
1277
|
+
startedAt: Date.now() - 5_000_000,
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1281
|
+
repoPath: fixture.repoDir,
|
|
1282
|
+
homeDir: fixture.homeDir,
|
|
1283
|
+
isPidAlive: (pid) => pid === 56789,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
expect(sessions).toHaveLength(1);
|
|
1287
|
+
expect(sessions[0]).toMatchObject({
|
|
1288
|
+
sessionId: SESSION_ONE,
|
|
1289
|
+
pid: 56789,
|
|
1290
|
+
waitingForUser: false,
|
|
1291
|
+
isRunning: true,
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
it('clears waiting markers when transcript tail is partial but no live process evidence exists', async () => {
|
|
1296
|
+
const fixture = await createFixture();
|
|
1297
|
+
|
|
1298
|
+
await writeProjectArtifact({
|
|
1299
|
+
projectsDir: fixture.projectsDir,
|
|
1300
|
+
repoPath: fixture.repoDir,
|
|
1301
|
+
sessionId: SESSION_ONE,
|
|
1302
|
+
jsonlContent: [
|
|
1303
|
+
JSON.stringify({
|
|
1304
|
+
type: 'user',
|
|
1305
|
+
message: { role: 'user', content: 'Continue the task' },
|
|
1306
|
+
cwd: fixture.repoDir,
|
|
1307
|
+
sessionId: SESSION_ONE,
|
|
1308
|
+
timestamp: new Date(Date.now() - 40_000).toISOString(),
|
|
1309
|
+
gitBranch: 'feature/current',
|
|
1310
|
+
}),
|
|
1311
|
+
JSON.stringify({
|
|
1312
|
+
type: 'assistant',
|
|
1313
|
+
message: {
|
|
1314
|
+
role: 'assistant',
|
|
1315
|
+
stop_reason: 'tool_use',
|
|
1316
|
+
content: [{ type: 'tool_use', id: 'tool_1', name: 'Fetch', input: { url: 'https://example.com' } }],
|
|
1317
|
+
},
|
|
1318
|
+
cwd: fixture.repoDir,
|
|
1319
|
+
sessionId: SESSION_ONE,
|
|
1320
|
+
timestamp: new Date(Date.now() - 20_000).toISOString(),
|
|
1321
|
+
gitBranch: 'feature/current',
|
|
1322
|
+
}),
|
|
1323
|
+
'{"type":"assistant","message":',
|
|
1324
|
+
].join('\n'),
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1328
|
+
repoPath: fixture.repoDir,
|
|
1329
|
+
homeDir: fixture.homeDir,
|
|
1330
|
+
isPidAlive: () => false,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
expect(sessions).toHaveLength(1);
|
|
1334
|
+
expect(sessions[0]).toMatchObject({
|
|
1335
|
+
sessionId: SESSION_ONE,
|
|
1336
|
+
waitingForUser: false,
|
|
1337
|
+
isRunning: false,
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
it('keeps waiting markers when transcript tail is partial but artifact activity is no longer recent', async () => {
|
|
1342
|
+
const fixture = await createFixture();
|
|
1343
|
+
|
|
1344
|
+
const artifactPath = await writeProjectArtifact({
|
|
1345
|
+
projectsDir: fixture.projectsDir,
|
|
1346
|
+
repoPath: fixture.repoDir,
|
|
1347
|
+
sessionId: SESSION_ONE,
|
|
1348
|
+
jsonlContent: [
|
|
1349
|
+
JSON.stringify({
|
|
1350
|
+
type: 'user',
|
|
1351
|
+
message: { role: 'user', content: 'Continue the task' },
|
|
1352
|
+
cwd: fixture.repoDir,
|
|
1353
|
+
sessionId: SESSION_ONE,
|
|
1354
|
+
timestamp: new Date(Date.now() - 40_000).toISOString(),
|
|
1355
|
+
gitBranch: 'feature/current',
|
|
1356
|
+
}),
|
|
1357
|
+
JSON.stringify({
|
|
1358
|
+
type: 'assistant',
|
|
1359
|
+
message: {
|
|
1360
|
+
role: 'assistant',
|
|
1361
|
+
stop_reason: 'tool_use',
|
|
1362
|
+
content: [{ type: 'tool_use', id: 'tool_1', name: 'Fetch', input: { url: 'https://example.com' } }],
|
|
1363
|
+
},
|
|
1364
|
+
cwd: fixture.repoDir,
|
|
1365
|
+
sessionId: SESSION_ONE,
|
|
1366
|
+
timestamp: new Date(Date.now() - 20_000).toISOString(),
|
|
1367
|
+
gitBranch: 'feature/current',
|
|
1368
|
+
}),
|
|
1369
|
+
'{"type":"assistant","message":',
|
|
1370
|
+
].join('\n'),
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
await writeSessionIndexEntry({
|
|
1374
|
+
sessionsDir: fixture.sessionsDir,
|
|
1375
|
+
pid: 88888,
|
|
1376
|
+
sessionId: SESSION_ONE,
|
|
1377
|
+
cwd: fixture.repoDir,
|
|
1378
|
+
startedAt: Date.now() - 100_000,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
const staleDate = new Date(Date.now() - 20_000);
|
|
1382
|
+
await utimes(artifactPath, staleDate, staleDate);
|
|
1383
|
+
|
|
1384
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1385
|
+
repoPath: fixture.repoDir,
|
|
1386
|
+
homeDir: fixture.homeDir,
|
|
1387
|
+
isPidAlive: (pid) => pid === 88888,
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
expect(sessions).toHaveLength(1);
|
|
1391
|
+
expect(sessions[0]).toMatchObject({
|
|
1392
|
+
sessionId: SESSION_ONE,
|
|
1393
|
+
pid: 88888,
|
|
1394
|
+
waitingForUser: true,
|
|
1395
|
+
isRunning: false,
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
it('suppresses stale waiting-for-user immediately after a Claude session restore until new transcript activity occurs', async () => {
|
|
1400
|
+
const fixture = await createFixture();
|
|
1401
|
+
const overridesModule = await import('@/lib/claudeSessionOverrides');
|
|
1402
|
+
const mockList = vi.mocked(overridesModule.listClaudeSessionOverrides);
|
|
1403
|
+
|
|
1404
|
+
const artifactPath = await writeProjectArtifact({
|
|
1405
|
+
projectsDir: fixture.projectsDir,
|
|
1406
|
+
repoPath: fixture.repoDir,
|
|
1407
|
+
sessionId: SESSION_ONE,
|
|
1408
|
+
jsonlContent: [
|
|
1409
|
+
JSON.stringify({
|
|
1410
|
+
type: 'assistant',
|
|
1411
|
+
message: {
|
|
1412
|
+
role: 'assistant',
|
|
1413
|
+
stop_reason: 'tool_use',
|
|
1414
|
+
content: [{ type: 'tool_use', id: 'tool_1', name: 'Fetch', input: { url: 'https://example.com' } }],
|
|
1415
|
+
},
|
|
1416
|
+
cwd: fixture.repoDir,
|
|
1417
|
+
sessionId: SESSION_ONE,
|
|
1418
|
+
timestamp: new Date(Date.now() - 20_000).toISOString(),
|
|
1419
|
+
gitBranch: 'feature/current',
|
|
1420
|
+
}),
|
|
1421
|
+
].join('\n'),
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
const artifactStat = await stat(artifactPath);
|
|
1425
|
+
mockList.mockResolvedValueOnce([
|
|
1426
|
+
{ sessionId: SESSION_ONE, restoredAt: artifactStat.mtimeMs + 1_000, updatedAt: artifactStat.mtimeMs + 1_000 },
|
|
1427
|
+
]);
|
|
1428
|
+
|
|
1429
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1430
|
+
repoPath: fixture.repoDir,
|
|
1431
|
+
homeDir: fixture.homeDir,
|
|
1432
|
+
isPidAlive: () => false,
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
expect(sessions).toHaveLength(1);
|
|
1436
|
+
expect(sessions[0]).toMatchObject({
|
|
1437
|
+
sessionId: SESSION_ONE,
|
|
1438
|
+
waitingForUser: false,
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
it('emits authoritative child linkage only when a local artifact declares an explicit parent session id', async () => {
|
|
1443
|
+
const fixture = await createFixture();
|
|
1444
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
1445
|
+
|
|
1446
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
1447
|
+
|
|
1448
|
+
await writeProjectArtifact({
|
|
1449
|
+
projectsDir: fixture.projectsDir,
|
|
1450
|
+
repoPath: fixture.repoDir,
|
|
1451
|
+
sessionId: SESSION_ONE,
|
|
1452
|
+
jsonlContent: createJsonlHead({
|
|
1453
|
+
sessionId: SESSION_ONE,
|
|
1454
|
+
cwd: fixture.repoDir,
|
|
1455
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
1456
|
+
}),
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
await writeProjectArtifact({
|
|
1460
|
+
projectsDir: fixture.projectsDir,
|
|
1461
|
+
repoPath: fixture.repoDir,
|
|
1462
|
+
sessionId: SESSION_TWO,
|
|
1463
|
+
jsonlContent: createJsonlHead({
|
|
1464
|
+
sessionId: SESSION_TWO,
|
|
1465
|
+
cwd: fixture.repoDir,
|
|
1466
|
+
parentSessionId: SESSION_ONE,
|
|
1467
|
+
timestamp: '2026-04-09T18:23:00.000Z',
|
|
1468
|
+
}),
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
const discovered = await discoverClaudeCodeSessions({
|
|
1472
|
+
repoPath: fixture.repoDir,
|
|
1473
|
+
homeDir: fixture.homeDir,
|
|
1474
|
+
isPidAlive: () => false,
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
expect(discovered).toHaveLength(2);
|
|
1478
|
+
expect(discovered.find((session) => session.sessionId === SESSION_ONE)).toMatchObject({
|
|
1479
|
+
topology: { childSessions: 'authoritative' },
|
|
1480
|
+
});
|
|
1481
|
+
expect(discovered.find((session) => session.sessionId === SESSION_TWO)).toMatchObject({
|
|
1482
|
+
parentSessionId: SESSION_ONE,
|
|
1483
|
+
topology: { childSessions: 'authoritative' },
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
const normalized = normalizeClaudeCodeSessions?.(discovered) ?? [];
|
|
1487
|
+
|
|
1488
|
+
expect(normalized).toHaveLength(1);
|
|
1489
|
+
expect(normalized[0]).toMatchObject({
|
|
1490
|
+
id: `claude~${SESSION_ONE}`,
|
|
1491
|
+
topology: { childSessions: 'authoritative' },
|
|
1492
|
+
children: [
|
|
1493
|
+
{
|
|
1494
|
+
id: `claude~${SESSION_TWO}`,
|
|
1495
|
+
parentID: `claude~${SESSION_ONE}`,
|
|
1496
|
+
topology: { childSessions: 'authoritative' },
|
|
1497
|
+
},
|
|
1498
|
+
],
|
|
1499
|
+
});
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
it('discovers nested subagent artifacts and links them to parent sessions', async () => {
|
|
1503
|
+
const fixture = await createFixture();
|
|
1504
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
1505
|
+
|
|
1506
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
1507
|
+
|
|
1508
|
+
await writeProjectArtifact({
|
|
1509
|
+
projectsDir: fixture.projectsDir,
|
|
1510
|
+
repoPath: fixture.repoDir,
|
|
1511
|
+
sessionId: SESSION_ONE,
|
|
1512
|
+
jsonlContent: createJsonlHead({
|
|
1513
|
+
sessionId: SESSION_ONE,
|
|
1514
|
+
cwd: fixture.repoDir,
|
|
1515
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
1516
|
+
}),
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
await writeSubagentArtifact({
|
|
1520
|
+
projectsDir: fixture.projectsDir,
|
|
1521
|
+
repoPath: fixture.repoDir,
|
|
1522
|
+
parentSessionId: SESSION_ONE,
|
|
1523
|
+
agentId: 'a1234567890',
|
|
1524
|
+
timestamp: new Date().toISOString(),
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
const discovered = await discoverClaudeCodeSessions({
|
|
1528
|
+
repoPath: fixture.repoDir,
|
|
1529
|
+
homeDir: fixture.homeDir,
|
|
1530
|
+
isPidAlive: () => false,
|
|
1531
|
+
});
|
|
1532
|
+
const scopedSubagentSessionId = `${SESSION_ONE}__agent-a1234567890`;
|
|
1533
|
+
|
|
1534
|
+
expect(discovered).toEqual(
|
|
1535
|
+
expect.arrayContaining([
|
|
1536
|
+
expect.objectContaining({
|
|
1537
|
+
sessionId: SESSION_ONE,
|
|
1538
|
+
topology: { childSessions: 'authoritative' },
|
|
1539
|
+
}),
|
|
1540
|
+
expect.objectContaining({
|
|
1541
|
+
sessionId: scopedSubagentSessionId,
|
|
1542
|
+
parentSessionId: SESSION_ONE,
|
|
1543
|
+
topology: { childSessions: 'authoritative' },
|
|
1544
|
+
}),
|
|
1545
|
+
])
|
|
1546
|
+
);
|
|
1547
|
+
expect(discovered.find((session) => session.sessionId === scopedSubagentSessionId)?.isRunning).toBe(true);
|
|
1548
|
+
|
|
1549
|
+
const normalized = normalizeClaudeCodeSessions?.(discovered) ?? [];
|
|
1550
|
+
|
|
1551
|
+
expect(normalized).toHaveLength(1);
|
|
1552
|
+
expect(normalized[0]).toMatchObject({
|
|
1553
|
+
id: `claude~${SESSION_ONE}`,
|
|
1554
|
+
children: [
|
|
1555
|
+
expect.objectContaining({
|
|
1556
|
+
id: scopedSubagentSessionId,
|
|
1557
|
+
parentID: `claude~${SESSION_ONE}`,
|
|
1558
|
+
topology: { childSessions: 'authoritative' },
|
|
1559
|
+
}),
|
|
1560
|
+
],
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
it('keeps sidechain child sessions distinct when different parents share the same agent artifact id', async () => {
|
|
1565
|
+
const fixture = await createFixture();
|
|
1566
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
1567
|
+
|
|
1568
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
1569
|
+
|
|
1570
|
+
const secondParentSessionId = '770e8400-e29b-41d4-a716-446655440000';
|
|
1571
|
+
|
|
1572
|
+
await writeProjectArtifact({
|
|
1573
|
+
projectsDir: fixture.projectsDir,
|
|
1574
|
+
repoPath: fixture.repoDir,
|
|
1575
|
+
sessionId: SESSION_ONE,
|
|
1576
|
+
jsonlContent: createJsonlHead({
|
|
1577
|
+
sessionId: SESSION_ONE,
|
|
1578
|
+
cwd: fixture.repoDir,
|
|
1579
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
1580
|
+
}),
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
await writeProjectArtifact({
|
|
1584
|
+
projectsDir: fixture.projectsDir,
|
|
1585
|
+
repoPath: fixture.repoDir,
|
|
1586
|
+
sessionId: secondParentSessionId,
|
|
1587
|
+
jsonlContent: createJsonlHead({
|
|
1588
|
+
sessionId: secondParentSessionId,
|
|
1589
|
+
cwd: fixture.repoDir,
|
|
1590
|
+
timestamp: '2026-04-09T18:24:00.000Z',
|
|
1591
|
+
}),
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
await writeSubagentArtifact({
|
|
1595
|
+
projectsDir: fixture.projectsDir,
|
|
1596
|
+
repoPath: fixture.repoDir,
|
|
1597
|
+
parentSessionId: SESSION_ONE,
|
|
1598
|
+
agentId: 'shared-agent',
|
|
1599
|
+
timestamp: new Date().toISOString(),
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
await writeSubagentArtifact({
|
|
1603
|
+
projectsDir: fixture.projectsDir,
|
|
1604
|
+
repoPath: fixture.repoDir,
|
|
1605
|
+
parentSessionId: secondParentSessionId,
|
|
1606
|
+
agentId: 'shared-agent',
|
|
1607
|
+
timestamp: new Date().toISOString(),
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
const discovered = await discoverClaudeCodeSessions({
|
|
1611
|
+
repoPath: fixture.repoDir,
|
|
1612
|
+
homeDir: fixture.homeDir,
|
|
1613
|
+
isPidAlive: () => false,
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
const firstScopedSubagentSessionId = `${SESSION_ONE}__agent-shared-agent`;
|
|
1617
|
+
const secondScopedSubagentSessionId = `${secondParentSessionId}__agent-shared-agent`;
|
|
1618
|
+
|
|
1619
|
+
expect(discovered).toEqual(
|
|
1620
|
+
expect.arrayContaining([
|
|
1621
|
+
expect.objectContaining({
|
|
1622
|
+
sessionId: firstScopedSubagentSessionId,
|
|
1623
|
+
parentSessionId: SESSION_ONE,
|
|
1624
|
+
topology: { childSessions: 'authoritative' },
|
|
1625
|
+
}),
|
|
1626
|
+
expect.objectContaining({
|
|
1627
|
+
sessionId: secondScopedSubagentSessionId,
|
|
1628
|
+
parentSessionId: secondParentSessionId,
|
|
1629
|
+
topology: { childSessions: 'authoritative' },
|
|
1630
|
+
}),
|
|
1631
|
+
])
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1634
|
+
const normalized = normalizeClaudeCodeSessions?.(discovered) ?? [];
|
|
1635
|
+
|
|
1636
|
+
expect(normalized).toEqual(
|
|
1637
|
+
expect.arrayContaining([
|
|
1638
|
+
expect.objectContaining({
|
|
1639
|
+
id: `claude~${SESSION_ONE}`,
|
|
1640
|
+
children: [
|
|
1641
|
+
expect.objectContaining({
|
|
1642
|
+
id: firstScopedSubagentSessionId,
|
|
1643
|
+
parentID: `claude~${SESSION_ONE}`,
|
|
1644
|
+
}),
|
|
1645
|
+
],
|
|
1646
|
+
}),
|
|
1647
|
+
expect.objectContaining({
|
|
1648
|
+
id: `claude~${secondParentSessionId}`,
|
|
1649
|
+
children: [
|
|
1650
|
+
expect.objectContaining({
|
|
1651
|
+
id: secondScopedSubagentSessionId,
|
|
1652
|
+
parentID: `claude~${secondParentSessionId}`,
|
|
1653
|
+
}),
|
|
1654
|
+
],
|
|
1655
|
+
}),
|
|
1656
|
+
])
|
|
1657
|
+
);
|
|
1658
|
+
expect(normalized.find((session) => session.id === firstScopedSubagentSessionId)).toBeUndefined();
|
|
1659
|
+
expect(normalized.find((session) => session.id === secondScopedSubagentSessionId)).toBeUndefined();
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
it('does not apply legacy bare subagent overrides across scoped sidechain children', async () => {
|
|
1663
|
+
const fixture = await createFixture();
|
|
1664
|
+
const overridesModule = await import('@/lib/claudeSessionOverrides');
|
|
1665
|
+
const mockList = vi.mocked(overridesModule.listClaudeSessionOverrides);
|
|
1666
|
+
|
|
1667
|
+
const secondParentSessionId = '880e8400-e29b-41d4-a716-446655440000';
|
|
1668
|
+
|
|
1669
|
+
await writeProjectArtifact({
|
|
1670
|
+
projectsDir: fixture.projectsDir,
|
|
1671
|
+
repoPath: fixture.repoDir,
|
|
1672
|
+
sessionId: SESSION_ONE,
|
|
1673
|
+
jsonlContent: createJsonlHead({
|
|
1674
|
+
sessionId: SESSION_ONE,
|
|
1675
|
+
cwd: fixture.repoDir,
|
|
1676
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
1677
|
+
}),
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
await writeProjectArtifact({
|
|
1681
|
+
projectsDir: fixture.projectsDir,
|
|
1682
|
+
repoPath: fixture.repoDir,
|
|
1683
|
+
sessionId: secondParentSessionId,
|
|
1684
|
+
jsonlContent: createJsonlHead({
|
|
1685
|
+
sessionId: secondParentSessionId,
|
|
1686
|
+
cwd: fixture.repoDir,
|
|
1687
|
+
timestamp: '2026-04-09T18:24:00.000Z',
|
|
1688
|
+
}),
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
await writeSubagentArtifact({
|
|
1692
|
+
projectsDir: fixture.projectsDir,
|
|
1693
|
+
repoPath: fixture.repoDir,
|
|
1694
|
+
parentSessionId: SESSION_ONE,
|
|
1695
|
+
agentId: 'shared-agent',
|
|
1696
|
+
timestamp: new Date().toISOString(),
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
await writeSubagentArtifact({
|
|
1700
|
+
projectsDir: fixture.projectsDir,
|
|
1701
|
+
repoPath: fixture.repoDir,
|
|
1702
|
+
parentSessionId: secondParentSessionId,
|
|
1703
|
+
agentId: 'shared-agent',
|
|
1704
|
+
timestamp: new Date().toISOString(),
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
mockList.mockResolvedValueOnce([
|
|
1708
|
+
{
|
|
1709
|
+
sessionId: 'agent-shared-agent',
|
|
1710
|
+
archivedAt: 123,
|
|
1711
|
+
updatedAt: 123,
|
|
1712
|
+
},
|
|
1713
|
+
]);
|
|
1714
|
+
|
|
1715
|
+
const discovered = await discoverClaudeCodeSessions({
|
|
1716
|
+
repoPath: fixture.repoDir,
|
|
1717
|
+
homeDir: fixture.homeDir,
|
|
1718
|
+
isPidAlive: () => false,
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
expect(
|
|
1722
|
+
discovered.find((session) => session.sessionId === `${SESSION_ONE}__agent-shared-agent`)?.archivedAt
|
|
1723
|
+
).toBeUndefined();
|
|
1724
|
+
expect(
|
|
1725
|
+
discovered.find((session) => session.sessionId === `${secondParentSessionId}__agent-shared-agent`)?.archivedAt
|
|
1726
|
+
).toBeUndefined();
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
it('cascades parent deleted overrides to scoped sidechain sessions', async () => {
|
|
1730
|
+
const fixture = await createFixture();
|
|
1731
|
+
const overridesModule = await import('@/lib/claudeSessionOverrides');
|
|
1732
|
+
const mockList = vi.mocked(overridesModule.listClaudeSessionOverrides);
|
|
1733
|
+
|
|
1734
|
+
await writeProjectArtifact({
|
|
1735
|
+
projectsDir: fixture.projectsDir,
|
|
1736
|
+
repoPath: fixture.repoDir,
|
|
1737
|
+
sessionId: SESSION_ONE,
|
|
1738
|
+
jsonlContent: createJsonlHead({
|
|
1739
|
+
sessionId: SESSION_ONE,
|
|
1740
|
+
cwd: fixture.repoDir,
|
|
1741
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
1742
|
+
}),
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
await writeProjectArtifact({
|
|
1746
|
+
projectsDir: fixture.projectsDir,
|
|
1747
|
+
repoPath: fixture.repoDir,
|
|
1748
|
+
sessionId: SESSION_TWO,
|
|
1749
|
+
jsonlContent: createJsonlHead({
|
|
1750
|
+
sessionId: SESSION_TWO,
|
|
1751
|
+
cwd: fixture.repoDir,
|
|
1752
|
+
timestamp: '2026-04-09T18:24:00.000Z',
|
|
1753
|
+
}),
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
await writeSubagentArtifact({
|
|
1757
|
+
projectsDir: fixture.projectsDir,
|
|
1758
|
+
repoPath: fixture.repoDir,
|
|
1759
|
+
parentSessionId: SESSION_ONE,
|
|
1760
|
+
agentId: 'cascade-a',
|
|
1761
|
+
timestamp: new Date().toISOString(),
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
await writeSubagentArtifact({
|
|
1765
|
+
projectsDir: fixture.projectsDir,
|
|
1766
|
+
repoPath: fixture.repoDir,
|
|
1767
|
+
parentSessionId: SESSION_TWO,
|
|
1768
|
+
agentId: 'cascade-b',
|
|
1769
|
+
timestamp: new Date().toISOString(),
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
mockList.mockResolvedValueOnce([
|
|
1773
|
+
{
|
|
1774
|
+
sessionId: SESSION_ONE,
|
|
1775
|
+
deletedAt: 123,
|
|
1776
|
+
updatedAt: 123,
|
|
1777
|
+
},
|
|
1778
|
+
]);
|
|
1779
|
+
|
|
1780
|
+
const discovered = await discoverClaudeCodeSessions({
|
|
1781
|
+
repoPath: fixture.repoDir,
|
|
1782
|
+
homeDir: fixture.homeDir,
|
|
1783
|
+
isPidAlive: () => false,
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
expect(discovered.find((session) => session.sessionId === SESSION_ONE)).toBeUndefined();
|
|
1787
|
+
expect(discovered.find((session) => session.sessionId === `${SESSION_ONE}__agent-cascade-a`)).toBeUndefined();
|
|
1788
|
+
|
|
1789
|
+
expect(discovered).toEqual(
|
|
1790
|
+
expect.arrayContaining([
|
|
1791
|
+
expect.objectContaining({ sessionId: SESSION_TWO }),
|
|
1792
|
+
expect.objectContaining({
|
|
1793
|
+
sessionId: `${SESSION_TWO}__agent-cascade-b`,
|
|
1794
|
+
parentSessionId: SESSION_TWO,
|
|
1795
|
+
}),
|
|
1796
|
+
])
|
|
1797
|
+
);
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
it('keeps Claude discovery flat when parent linkage is malformed, missing locally, or only nested in non-authoritative transcript data', async () => {
|
|
1801
|
+
const fixture = await createFixture();
|
|
1802
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
1803
|
+
|
|
1804
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
1805
|
+
|
|
1806
|
+
await writeProjectArtifact({
|
|
1807
|
+
projectsDir: fixture.projectsDir,
|
|
1808
|
+
repoPath: fixture.repoDir,
|
|
1809
|
+
sessionId: SESSION_ONE,
|
|
1810
|
+
jsonlContent: createJsonlHead({
|
|
1811
|
+
sessionId: SESSION_ONE,
|
|
1812
|
+
cwd: fixture.repoDir,
|
|
1813
|
+
timestamp: '2026-04-09T18:22:00.000Z',
|
|
1814
|
+
}),
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
await writeProjectArtifact({
|
|
1818
|
+
projectsDir: fixture.projectsDir,
|
|
1819
|
+
repoPath: fixture.repoDir,
|
|
1820
|
+
sessionId: SESSION_TWO,
|
|
1821
|
+
jsonlContent: [
|
|
1822
|
+
JSON.stringify({
|
|
1823
|
+
cwd: fixture.repoDir,
|
|
1824
|
+
sessionId: SESSION_TWO,
|
|
1825
|
+
gitBranch: 'main',
|
|
1826
|
+
timestamp: '2026-04-09T18:23:00.000Z',
|
|
1827
|
+
type: 'user',
|
|
1828
|
+
message: { role: 'user', content: 'hello' },
|
|
1829
|
+
parentSessionId: 123,
|
|
1830
|
+
}),
|
|
1831
|
+
].join('\n'),
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
await writeProjectArtifact({
|
|
1835
|
+
projectsDir: fixture.projectsDir,
|
|
1836
|
+
repoPath: fixture.repoDir,
|
|
1837
|
+
sessionId: '770e8400-e29b-41d4-a716-446655440000',
|
|
1838
|
+
jsonlContent: createJsonlHead({
|
|
1839
|
+
sessionId: '770e8400-e29b-41d4-a716-446655440000',
|
|
1840
|
+
cwd: fixture.repoDir,
|
|
1841
|
+
parentSessionId: '880e8400-e29b-41d4-a716-446655440000',
|
|
1842
|
+
timestamp: '2026-04-09T18:24:00.000Z',
|
|
1843
|
+
}),
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
await writeProjectArtifact({
|
|
1847
|
+
projectsDir: fixture.projectsDir,
|
|
1848
|
+
repoPath: fixture.repoDir,
|
|
1849
|
+
sessionId: '990e8400-e29b-41d4-a716-446655440000',
|
|
1850
|
+
jsonlContent: [
|
|
1851
|
+
JSON.stringify({
|
|
1852
|
+
cwd: fixture.repoDir,
|
|
1853
|
+
sessionId: '990e8400-e29b-41d4-a716-446655440000',
|
|
1854
|
+
gitBranch: 'main',
|
|
1855
|
+
timestamp: '2026-04-09T18:25:00.000Z',
|
|
1856
|
+
type: 'user',
|
|
1857
|
+
message: {
|
|
1858
|
+
role: 'user',
|
|
1859
|
+
content: {
|
|
1860
|
+
parentSessionId: SESSION_ONE,
|
|
1861
|
+
note: 'nested tool payload should stay non-authoritative',
|
|
1862
|
+
},
|
|
1863
|
+
},
|
|
1864
|
+
}),
|
|
1865
|
+
].join('\n'),
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
const discovered = await discoverClaudeCodeSessions({
|
|
1869
|
+
repoPath: fixture.repoDir,
|
|
1870
|
+
homeDir: fixture.homeDir,
|
|
1871
|
+
isPidAlive: () => false,
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
expect(discovered).toHaveLength(4);
|
|
1875
|
+
expect(discovered.every((session) => session.topology === undefined)).toBe(true);
|
|
1876
|
+
expect(discovered.every((session) => session.parentSessionId === undefined)).toBe(true);
|
|
1877
|
+
|
|
1878
|
+
const normalized = normalizeClaudeCodeSessions?.(discovered) ?? [];
|
|
1879
|
+
|
|
1880
|
+
expect(normalized).toHaveLength(4);
|
|
1881
|
+
expect(normalized.every((session) => session.children.length === 0)).toBe(true);
|
|
1882
|
+
expect(normalized.every((session) => session.parentID === undefined)).toBe(true);
|
|
1883
|
+
expect(normalized.every((session) => session.topology?.childSessions === 'flat')).toBe(true);
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
it('applies Claude archived overrides and filters deleted overrides', async () => {
|
|
1887
|
+
const fixture = await createFixture();
|
|
1888
|
+
const overridesModule = await import('@/lib/claudeSessionOverrides');
|
|
1889
|
+
const mockList = vi.mocked(overridesModule.listClaudeSessionOverrides);
|
|
1890
|
+
|
|
1891
|
+
await writeProjectArtifact({
|
|
1892
|
+
projectsDir: fixture.projectsDir,
|
|
1893
|
+
repoPath: fixture.repoDir,
|
|
1894
|
+
sessionId: SESSION_ONE,
|
|
1895
|
+
});
|
|
1896
|
+
await writeProjectArtifact({
|
|
1897
|
+
projectsDir: fixture.projectsDir,
|
|
1898
|
+
repoPath: fixture.otherRepoDir,
|
|
1899
|
+
sessionId: SESSION_TWO,
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
mockList.mockResolvedValueOnce([
|
|
1903
|
+
{ sessionId: SESSION_ONE, archivedAt: 123, updatedAt: 123 },
|
|
1904
|
+
{ sessionId: SESSION_TWO, deletedAt: 456, updatedAt: 456 },
|
|
1905
|
+
]);
|
|
1906
|
+
|
|
1907
|
+
const sessions = await discoverClaudeCodeSessions({
|
|
1908
|
+
repoPath: fixture.repoDir,
|
|
1909
|
+
homeDir: fixture.homeDir,
|
|
1910
|
+
isPidAlive: () => false,
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
expect(sessions).toHaveLength(1);
|
|
1914
|
+
expect(sessions[0]).toMatchObject({
|
|
1915
|
+
sessionId: SESSION_ONE,
|
|
1916
|
+
archivedAt: 123,
|
|
1917
|
+
});
|
|
1918
|
+
});
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
describe('normalizeClaudeCodeSessions', () => {
|
|
1922
|
+
it('normalizes a live Claude session as busy and read-only', () => {
|
|
1923
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
1924
|
+
|
|
1925
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
1926
|
+
|
|
1927
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
1928
|
+
makeDiscoveredSession({
|
|
1929
|
+
sessionId: SESSION_ONE,
|
|
1930
|
+
cwd: '/tmp/current-worktree',
|
|
1931
|
+
projectPath: '/tmp/current-worktree',
|
|
1932
|
+
projectName: 'current-worktree',
|
|
1933
|
+
gitBranch: 'feature/current',
|
|
1934
|
+
createdAt: 100,
|
|
1935
|
+
updatedAt: 200,
|
|
1936
|
+
startedAt: 150,
|
|
1937
|
+
pid: 12345,
|
|
1938
|
+
isRunning: true,
|
|
1939
|
+
}),
|
|
1940
|
+
]);
|
|
1941
|
+
|
|
1942
|
+
expect(sessions).toMatchObject([
|
|
1943
|
+
{
|
|
1944
|
+
id: `claude~${SESSION_ONE}`,
|
|
1945
|
+
slug: SESSION_ONE,
|
|
1946
|
+
title: SESSION_ONE.slice(0, 8),
|
|
1947
|
+
directory: '/tmp/current-worktree',
|
|
1948
|
+
projectName: 'current-worktree',
|
|
1949
|
+
branch: 'feature/current',
|
|
1950
|
+
time: {
|
|
1951
|
+
created: 100,
|
|
1952
|
+
updated: 200,
|
|
1953
|
+
},
|
|
1954
|
+
rawSessionId: SESSION_ONE,
|
|
1955
|
+
providerRawId: SESSION_ONE,
|
|
1956
|
+
provider: 'claude-code',
|
|
1957
|
+
readOnly: true,
|
|
1958
|
+
realTimeStatus: 'busy',
|
|
1959
|
+
waitingForUser: false,
|
|
1960
|
+
children: [],
|
|
1961
|
+
},
|
|
1962
|
+
]);
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
it('treats recent transcript-only Claude sessions as idle without retry semantics', () => {
|
|
1966
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
1967
|
+
|
|
1968
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
1969
|
+
|
|
1970
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
1971
|
+
makeDiscoveredSession({
|
|
1972
|
+
sessionId: SESSION_TWO,
|
|
1973
|
+
cwd: '/tmp/fallback-worktree',
|
|
1974
|
+
projectPath: '/tmp/fallback-worktree',
|
|
1975
|
+
projectName: 'fallback-worktree',
|
|
1976
|
+
createdAt: 300,
|
|
1977
|
+
updatedAt: 450,
|
|
1978
|
+
gitBranch: null,
|
|
1979
|
+
isRunning: false,
|
|
1980
|
+
}),
|
|
1981
|
+
]);
|
|
1982
|
+
|
|
1983
|
+
expect(sessions).toMatchObject([
|
|
1984
|
+
{
|
|
1985
|
+
id: `claude~${SESSION_TWO}`,
|
|
1986
|
+
slug: SESSION_TWO,
|
|
1987
|
+
title: SESSION_TWO.slice(0, 8),
|
|
1988
|
+
directory: '/tmp/fallback-worktree',
|
|
1989
|
+
projectName: 'fallback-worktree',
|
|
1990
|
+
time: {
|
|
1991
|
+
created: 300,
|
|
1992
|
+
updated: 450,
|
|
1993
|
+
},
|
|
1994
|
+
rawSessionId: SESSION_TWO,
|
|
1995
|
+
providerRawId: SESSION_TWO,
|
|
1996
|
+
provider: 'claude-code',
|
|
1997
|
+
readOnly: true,
|
|
1998
|
+
realTimeStatus: 'idle',
|
|
1999
|
+
waitingForUser: false,
|
|
2000
|
+
children: [],
|
|
2001
|
+
},
|
|
2002
|
+
]);
|
|
2003
|
+
expect(sessions?.[0]?.branch).toBeUndefined();
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
it('can normalize Claude sessions into a waiting-for-user state without emitting retry', () => {
|
|
2007
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2008
|
+
|
|
2009
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2010
|
+
|
|
2011
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2012
|
+
makeDiscoveredSession({
|
|
2013
|
+
sessionId: SESSION_ONE,
|
|
2014
|
+
isRunning: true,
|
|
2015
|
+
pid: 12345,
|
|
2016
|
+
}),
|
|
2017
|
+
makeDiscoveredSession({
|
|
2018
|
+
sessionId: SESSION_TWO,
|
|
2019
|
+
cwd: '/tmp/transcript-only',
|
|
2020
|
+
projectPath: '/tmp/transcript-only',
|
|
2021
|
+
projectName: 'transcript-only',
|
|
2022
|
+
isRunning: false,
|
|
2023
|
+
waitingForUser: true,
|
|
2024
|
+
}),
|
|
2025
|
+
]) ?? [];
|
|
2026
|
+
|
|
2027
|
+
expect(sessions).toHaveLength(2);
|
|
2028
|
+
expect(sessions.every((session) => ['idle', 'busy'].includes(session.realTimeStatus))).toBe(true);
|
|
2029
|
+
expect(sessions.some((session) => session.waitingForUser === true)).toBe(true);
|
|
2030
|
+
expect(sessions.every((session) => Array.isArray(session.children) && session.children.length === 0)).toBe(true);
|
|
2031
|
+
expect(sessions.every((session) => session.topology?.childSessions === 'flat')).toBe(true);
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
it('nests verified Claude child sessions only when authoritative linkage is explicit', () => {
|
|
2035
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2036
|
+
|
|
2037
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2038
|
+
|
|
2039
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2040
|
+
makeDiscoveredSession({
|
|
2041
|
+
sessionId: SESSION_ONE,
|
|
2042
|
+
cwd: '/tmp/parent-worktree',
|
|
2043
|
+
projectPath: '/tmp/parent-worktree',
|
|
2044
|
+
projectName: 'parent-worktree',
|
|
2045
|
+
isRunning: true,
|
|
2046
|
+
topology: { childSessions: 'authoritative' },
|
|
2047
|
+
}),
|
|
2048
|
+
makeDiscoveredSession({
|
|
2049
|
+
sessionId: SESSION_TWO,
|
|
2050
|
+
cwd: '/tmp/child-worktree',
|
|
2051
|
+
projectPath: '/tmp/child-worktree',
|
|
2052
|
+
projectName: 'child-worktree',
|
|
2053
|
+
isRunning: false,
|
|
2054
|
+
waitingForUser: true,
|
|
2055
|
+
parentSessionId: SESSION_ONE,
|
|
2056
|
+
topology: { childSessions: 'authoritative' },
|
|
2057
|
+
}),
|
|
2058
|
+
]) ?? [];
|
|
2059
|
+
|
|
2060
|
+
expect(sessions).toHaveLength(1);
|
|
2061
|
+
expect(sessions[0]).toMatchObject({
|
|
2062
|
+
id: `claude~${SESSION_ONE}`,
|
|
2063
|
+
topology: { childSessions: 'authoritative' },
|
|
2064
|
+
children: [
|
|
2065
|
+
{
|
|
2066
|
+
id: `claude~${SESSION_TWO}`,
|
|
2067
|
+
parentID: `claude~${SESSION_ONE}`,
|
|
2068
|
+
topology: { childSessions: 'authoritative' },
|
|
2069
|
+
realTimeStatus: 'idle',
|
|
2070
|
+
waitingForUser: true,
|
|
2071
|
+
provider: 'claude-code',
|
|
2072
|
+
providerRawId: SESSION_TWO,
|
|
2073
|
+
rawSessionId: SESSION_TWO,
|
|
2074
|
+
readOnly: true,
|
|
2075
|
+
},
|
|
2076
|
+
],
|
|
2077
|
+
});
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
it('preserves deeper authoritative descendants by flattening them under the root parent session', () => {
|
|
2081
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2082
|
+
|
|
2083
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2084
|
+
|
|
2085
|
+
const grandchildSessionId = '770e8400-e29b-41d4-a716-446655440000';
|
|
2086
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2087
|
+
makeDiscoveredSession({
|
|
2088
|
+
sessionId: SESSION_ONE,
|
|
2089
|
+
cwd: '/tmp/root-parent',
|
|
2090
|
+
projectPath: '/tmp/root-parent',
|
|
2091
|
+
projectName: 'root-parent',
|
|
2092
|
+
isRunning: true,
|
|
2093
|
+
topology: { childSessions: 'authoritative' },
|
|
2094
|
+
}),
|
|
2095
|
+
makeDiscoveredSession({
|
|
2096
|
+
sessionId: SESSION_TWO,
|
|
2097
|
+
cwd: '/tmp/root-parent',
|
|
2098
|
+
projectPath: '/tmp/root-parent',
|
|
2099
|
+
projectName: 'root-parent',
|
|
2100
|
+
parentSessionId: SESSION_ONE,
|
|
2101
|
+
isRunning: false,
|
|
2102
|
+
waitingForUser: true,
|
|
2103
|
+
topology: { childSessions: 'authoritative' },
|
|
2104
|
+
}),
|
|
2105
|
+
makeDiscoveredSession({
|
|
2106
|
+
sessionId: grandchildSessionId,
|
|
2107
|
+
cwd: '/tmp/root-parent',
|
|
2108
|
+
projectPath: '/tmp/root-parent',
|
|
2109
|
+
projectName: 'root-parent',
|
|
2110
|
+
parentSessionId: SESSION_TWO,
|
|
2111
|
+
isRunning: false,
|
|
2112
|
+
waitingForUser: false,
|
|
2113
|
+
topology: { childSessions: 'authoritative' },
|
|
2114
|
+
}),
|
|
2115
|
+
]) ?? [];
|
|
2116
|
+
|
|
2117
|
+
expect(sessions).toHaveLength(1);
|
|
2118
|
+
expect(sessions[0]).toMatchObject({
|
|
2119
|
+
id: `claude~${SESSION_ONE}`,
|
|
2120
|
+
topology: { childSessions: 'authoritative' },
|
|
2121
|
+
children: expect.arrayContaining([
|
|
2122
|
+
expect.objectContaining({
|
|
2123
|
+
id: `claude~${SESSION_TWO}`,
|
|
2124
|
+
parentID: `claude~${SESSION_ONE}`,
|
|
2125
|
+
providerRawId: SESSION_TWO,
|
|
2126
|
+
}),
|
|
2127
|
+
expect.objectContaining({
|
|
2128
|
+
id: `claude~${grandchildSessionId}`,
|
|
2129
|
+
parentID: `claude~${SESSION_ONE}`,
|
|
2130
|
+
providerRawId: grandchildSessionId,
|
|
2131
|
+
}),
|
|
2132
|
+
]),
|
|
2133
|
+
});
|
|
2134
|
+
expect(sessions[0].children).toHaveLength(2);
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
it('keeps Claude sessions flat when linkage is missing or topology is not authoritative', () => {
|
|
2138
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2139
|
+
|
|
2140
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2141
|
+
|
|
2142
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2143
|
+
makeDiscoveredSession({
|
|
2144
|
+
sessionId: SESSION_ONE,
|
|
2145
|
+
cwd: '/tmp/claimed-parent',
|
|
2146
|
+
projectPath: '/tmp/claimed-parent',
|
|
2147
|
+
projectName: 'claimed-parent',
|
|
2148
|
+
topology: { childSessions: 'authoritative' },
|
|
2149
|
+
}),
|
|
2150
|
+
makeDiscoveredSession({
|
|
2151
|
+
sessionId: SESSION_TWO,
|
|
2152
|
+
cwd: '/tmp/non-authoritative-child',
|
|
2153
|
+
projectPath: '/tmp/non-authoritative-child',
|
|
2154
|
+
projectName: 'non-authoritative-child',
|
|
2155
|
+
parentSessionId: SESSION_ONE,
|
|
2156
|
+
}),
|
|
2157
|
+
makeDiscoveredSession({
|
|
2158
|
+
sessionId: '770e8400-e29b-41d4-a716-446655440000',
|
|
2159
|
+
cwd: '/tmp/missing-parent-link',
|
|
2160
|
+
projectPath: '/tmp/missing-parent-link',
|
|
2161
|
+
projectName: 'missing-parent-link',
|
|
2162
|
+
parentSessionId: '880e8400-e29b-41d4-a716-446655440000',
|
|
2163
|
+
topology: { childSessions: 'authoritative' },
|
|
2164
|
+
}),
|
|
2165
|
+
]) ?? [];
|
|
2166
|
+
|
|
2167
|
+
expect(sessions).toHaveLength(3);
|
|
2168
|
+
expect(sessions.every((session) => session.children.length === 0)).toBe(true);
|
|
2169
|
+
expect(sessions.find((session) => session.id === `claude~${SESSION_ONE}`)?.topology).toEqual({
|
|
2170
|
+
childSessions: 'authoritative',
|
|
2171
|
+
});
|
|
2172
|
+
expect(sessions.find((session) => session.id === `claude~${SESSION_TWO}`)?.topology).toEqual({
|
|
2173
|
+
childSessions: 'flat',
|
|
2174
|
+
});
|
|
2175
|
+
expect(sessions.find((session) => session.id === `claude~${SESSION_TWO}`)?.parentID).toBeUndefined();
|
|
2176
|
+
expect(sessions.find((session) => session.id === 'claude~770e8400-e29b-41d4-a716-446655440000')?.topology).toEqual({
|
|
2177
|
+
childSessions: 'authoritative',
|
|
2178
|
+
});
|
|
2179
|
+
expect(sessions.find((session) => session.id === 'claude~770e8400-e29b-41d4-a716-446655440000')?.parentID).toBeUndefined();
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
it('prefers transcript-derived discovered titles for Claude sessions', () => {
|
|
2183
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2184
|
+
|
|
2185
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2186
|
+
|
|
2187
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2188
|
+
makeDiscoveredSession({
|
|
2189
|
+
sessionId: SESSION_ONE,
|
|
2190
|
+
title: 'Investigate Docs as a Service concept and integration options',
|
|
2191
|
+
}),
|
|
2192
|
+
]);
|
|
2193
|
+
|
|
2194
|
+
expect(sessions).toMatchObject([
|
|
2195
|
+
{
|
|
2196
|
+
id: `claude~${SESSION_ONE}`,
|
|
2197
|
+
title: 'Investigate Docs as a Service concept and integration options',
|
|
2198
|
+
},
|
|
2199
|
+
]);
|
|
2200
|
+
});
|
|
2201
|
+
|
|
2202
|
+
it('strips only known Claude wrapper tags while preserving legitimate angle-bracket titles', () => {
|
|
2203
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2204
|
+
|
|
2205
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2206
|
+
|
|
2207
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2208
|
+
makeDiscoveredSession({
|
|
2209
|
+
sessionId: SESSION_ONE,
|
|
2210
|
+
title: '<command-message>graphify notes into clusters</command-message>',
|
|
2211
|
+
}),
|
|
2212
|
+
makeDiscoveredSession({
|
|
2213
|
+
sessionId: SESSION_TWO,
|
|
2214
|
+
title: '<local-command-caveat>Caveat about local command execution</local-command-caveat>',
|
|
2215
|
+
}),
|
|
2216
|
+
makeDiscoveredSession({
|
|
2217
|
+
sessionId: `${SESSION_ONE}-open-only`,
|
|
2218
|
+
title: '<command-message>graphify roadmap next',
|
|
2219
|
+
}),
|
|
2220
|
+
makeDiscoveredSession({
|
|
2221
|
+
sessionId: `${SESSION_TWO}-close-only`,
|
|
2222
|
+
title: 'graphify roadmap next</command-message>',
|
|
2223
|
+
}),
|
|
2224
|
+
makeDiscoveredSession({
|
|
2225
|
+
sessionId: `${SESSION_ONE}-malformed-close`,
|
|
2226
|
+
title: 'graphify roadmap next</command-message',
|
|
2227
|
+
}),
|
|
2228
|
+
makeDiscoveredSession({
|
|
2229
|
+
sessionId: `${SESSION_TWO}-malformed-inline-close`,
|
|
2230
|
+
title: 'graphify</command-message claude title 显示没有过滤好',
|
|
2231
|
+
}),
|
|
2232
|
+
makeDiscoveredSession({
|
|
2233
|
+
sessionId: `${SESSION_ONE}-malformed-inline-local-caveat`,
|
|
2234
|
+
title: 'graphify</local-command-caveat claude title caveat 没过滤好',
|
|
2235
|
+
}),
|
|
2236
|
+
makeDiscoveredSession({
|
|
2237
|
+
sessionId: `${SESSION_ONE}-jsx`,
|
|
2238
|
+
title: 'Investigate <Button /> inside <Card> rendering behavior',
|
|
2239
|
+
}),
|
|
2240
|
+
makeDiscoveredSession({
|
|
2241
|
+
sessionId: `${SESSION_TWO}-generic`,
|
|
2242
|
+
title: '<T> generic helper with boundary checks',
|
|
2243
|
+
}),
|
|
2244
|
+
]) ?? [];
|
|
2245
|
+
|
|
2246
|
+
expect(sessions[0]?.title).toBe('graphify notes into clusters');
|
|
2247
|
+
expect(sessions[1]?.title).toBe('Caveat about local command execution');
|
|
2248
|
+
expect(sessions[2]?.title).toBe('graphify roadmap next');
|
|
2249
|
+
expect(sessions[3]?.title).toBe('graphify roadmap next');
|
|
2250
|
+
expect(sessions[4]?.title).toBe('graphify roadmap next');
|
|
2251
|
+
expect(sessions[5]?.title).toBe('graphify claude title 显示没有过滤好');
|
|
2252
|
+
expect(sessions[6]?.title).toBe('graphify claude title caveat 没过滤好');
|
|
2253
|
+
expect(sessions[7]?.title).toBe('Investigate <Button /> inside <Card> rendering behavior');
|
|
2254
|
+
expect(sessions[8]?.title).toBe('<T> generic helper with boundary checks');
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
it('applies whitespace compaction and truncation after wrapper stripping', () => {
|
|
2258
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2259
|
+
|
|
2260
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2261
|
+
|
|
2262
|
+
const coreTitle = 'This is a deliberately long Claude title that should truncate after wrapper cleanup and normalization';
|
|
2263
|
+
const expectedTitle = `${coreTitle.slice(0, 69)}...`;
|
|
2264
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2265
|
+
makeDiscoveredSession({
|
|
2266
|
+
sessionId: `${SESSION_ONE}-truncation`,
|
|
2267
|
+
title: `<command-message> ${coreTitle} </command-message>`,
|
|
2268
|
+
}),
|
|
2269
|
+
]) ?? [];
|
|
2270
|
+
|
|
2271
|
+
expect(sessions[0]?.title).toBe(expectedTitle);
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
it('documents the boundary-wrapper tradeoff for literal user titles', () => {
|
|
2275
|
+
const normalizeClaudeCodeSessions = getNormalizeClaudeCodeSessions();
|
|
2276
|
+
|
|
2277
|
+
expect(normalizeClaudeCodeSessions).toBeTypeOf('function');
|
|
2278
|
+
|
|
2279
|
+
const sessions = normalizeClaudeCodeSessions?.([
|
|
2280
|
+
makeDiscoveredSession({
|
|
2281
|
+
sessionId: `${SESSION_TWO}-literal-wrapper`,
|
|
2282
|
+
title: '<command-message> literal user content',
|
|
2283
|
+
}),
|
|
2284
|
+
]) ?? [];
|
|
2285
|
+
|
|
2286
|
+
expect(sessions[0]?.title).toBe('literal user content');
|
|
2287
|
+
});
|
|
2288
|
+
});
|