@sanqianx/project-knowledge 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +17 -0
  3. package/CHANGELOG.md +780 -0
  4. package/LICENSE +201 -0
  5. package/NOTICE +5 -0
  6. package/README.md +266 -0
  7. package/_site/README.md +30 -0
  8. package/_site/_test/ai-profile-resolver-test.js +137 -0
  9. package/_site/_test/ai-profile-test.js +75 -0
  10. package/_site/_test/ai-vendor-presets-test.js +91 -0
  11. package/_site/_test/automation-queue-test.js +14 -0
  12. package/_site/_test/automation-ui-test.js +40 -0
  13. package/_site/_test/background-task-registry-test.js +43 -0
  14. package/_site/_test/baseline-schema-test.js +91 -0
  15. package/_site/_test/bin-cli-test.js +218 -0
  16. package/_site/_test/bridge-adapter-test.js +95 -0
  17. package/_site/_test/bridge-consumer-service-test.js +310 -0
  18. package/_site/_test/cdp-browser-failure-test.js +100 -0
  19. package/_site/_test/chat-claudecodeui-match-test.js +92 -0
  20. package/_site/_test/claude-executable-discovery-test.js +90 -0
  21. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
  22. package/_site/_test/claude-workbench-test.js +170 -0
  23. package/_site/_test/client-capture-restart-e2e-test.js +189 -0
  24. package/_site/_test/codex-conversation-projection-test.js +59 -0
  25. package/_site/_test/codex-notify-fanout-test.js +45 -0
  26. package/_site/_test/commit-boundary-freeze-test.js +147 -0
  27. package/_site/_test/commit-conversation-binding-test.js +99 -0
  28. package/_site/_test/commit-evidence-test.js +110 -0
  29. package/_site/_test/context-pack-test.js +198 -0
  30. package/_site/_test/conversation-api-test.js +119 -0
  31. package/_site/_test/conversation-store-test.js +136 -0
  32. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
  33. package/_site/_test/data-dir-migration-test.js +77 -0
  34. package/_site/_test/desktop-browser-compat-test.js +28 -0
  35. package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
  36. package/_site/_test/embedding-config-test.js +32 -0
  37. package/_site/_test/explicit-commit-processor-test.js +37 -0
  38. package/_site/_test/fixtures/fake-browser-exit.js +10 -0
  39. package/_site/_test/fixtures/fake-browser-hang.js +10 -0
  40. package/_site/_test/fixtures/make-git-repos.js +89 -0
  41. package/_site/_test/folder-picker-output-test.js +19 -0
  42. package/_site/_test/full-integration-e2e-test.js +165 -0
  43. package/_site/_test/git-validation-test.js +81 -0
  44. package/_site/_test/hook-runtime-endpoint-test.js +88 -0
  45. package/_site/_test/hook-status-repair-api-test.js +152 -0
  46. package/_site/_test/hook-trigger-test.js +85 -0
  47. package/_site/_test/hook-worktree-test.js +42 -0
  48. package/_site/_test/import-preflight-api-test.js +233 -0
  49. package/_site/_test/import-ui-flow-test.js +171 -0
  50. package/_site/_test/index-builder-test.js +41 -0
  51. package/_site/_test/index-writer-concurrency-test.js +135 -0
  52. package/_site/_test/integration-adapters-test.js +243 -0
  53. package/_site/_test/integration-capture-setup-test.js +108 -0
  54. package/_site/_test/integration-surface-coverage-test.js +165 -0
  55. package/_site/_test/kb-framework-test.js +57 -0
  56. package/_site/_test/kbpath-follow-test.js +63 -0
  57. package/_site/_test/knowledge-db-test.js +144 -0
  58. package/_site/_test/knowledge-language-control-test.js +170 -0
  59. package/_site/_test/knowledge-maintenance-test.js +40 -0
  60. package/_site/_test/knowledge-migration-test.js +75 -0
  61. package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
  62. package/_site/_test/knowledge-query-test.js +54 -0
  63. package/_site/_test/knowledge-retrieval-service-test.js +77 -0
  64. package/_site/_test/knowledge-scopes-test.js +37 -0
  65. package/_site/_test/knowledge-storage-location-test.js +28 -0
  66. package/_site/_test/knowledge-storage-startup-test.js +52 -0
  67. package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
  68. package/_site/_test/legacy-claude-session-alias-test.js +106 -0
  69. package/_site/_test/legacy-forward-compat-test.js +257 -0
  70. package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
  71. package/_site/_test/legacy-routes-removed-test.js +47 -0
  72. package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
  73. package/_site/_test/log-redaction-test.js +58 -0
  74. package/_site/_test/logging-api-test.js +89 -0
  75. package/_site/_test/logging-sse-no-gap-test.js +88 -0
  76. package/_site/_test/logging-ui-test.js +104 -0
  77. package/_site/_test/markdown-delta-overlay-test.js +85 -0
  78. package/_site/_test/markdown-maintenance-api-test.js +75 -0
  79. package/_site/_test/markdown-maintenance-test.js +11 -0
  80. package/_site/_test/mcp-server-test.js +149 -0
  81. package/_site/_test/model-context-windows-test.js +89 -0
  82. package/_site/_test/module-bridge-eventbridge-test.js +193 -0
  83. package/_site/_test/non-release-ci-test.js +34 -0
  84. package/_site/_test/offline-boundary-isolation-test.js +32 -0
  85. package/_site/_test/orphaned-claim-recovery-test.js +80 -0
  86. package/_site/_test/p0-data-migration-characterization-test.js +37 -0
  87. package/_site/_test/p0-e2e-gate-test.js +347 -0
  88. package/_site/_test/package-boundary-test.js +42 -0
  89. package/_site/_test/package-startup-test.js +56 -0
  90. package/_site/_test/packaged-ui-smoke-test.js +79 -0
  91. package/_site/_test/path-consistency-test.js +151 -0
  92. package/_site/_test/pending-sweep-test.js +9 -0
  93. package/_site/_test/post-commit-automation-test.js +87 -0
  94. package/_site/_test/pr-consumer-contract-test.js +55 -0
  95. package/_site/_test/project-control-panel-task14-test.js +63 -0
  96. package/_site/_test/project-delete-recovery-test.js +64 -0
  97. package/_site/_test/project-goal-editor-test.js +145 -0
  98. package/_site/_test/project-layout-v2-migration-test.js +127 -0
  99. package/_site/_test/project-lifecycle-transaction-test.js +106 -0
  100. package/_site/_test/project-remove-running-guard-test.js +79 -0
  101. package/_site/_test/project-store-test.js +90 -0
  102. package/_site/_test/prompt-settings-test.js +115 -0
  103. package/_site/_test/protected-architecture-gate-test.js +123 -0
  104. package/_site/_test/refactor-characterization-test.js +36 -0
  105. package/_site/_test/release-version-sync-test.js +98 -0
  106. package/_site/_test/requirement-binding-test.js +113 -0
  107. package/_site/_test/requirement-recorder-test.js +173 -0
  108. package/_site/_test/run-all-tests.js +156 -0
  109. package/_site/_test/runtime-endpoint-test.js +64 -0
  110. package/_site/_test/scanner-test.js +83 -0
  111. package/_site/_test/server-runtime-migration-safety-test.js +27 -0
  112. package/_site/_test/server-security-test.js +96 -0
  113. package/_site/_test/sessions-stream-test.js +92 -0
  114. package/_site/_test/shared-contracts-test.js +58 -0
  115. package/_site/_test/simple-import-test.js +97 -0
  116. package/_site/_test/startup-analysis-disabled-test.js +16 -0
  117. package/_site/_test/storage-foundation-test.js +78 -0
  118. package/_site/_test/structured-logger-test.js +101 -0
  119. package/_site/_test/task15-20-ui-flow-test.js +148 -0
  120. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
  121. package/_site/_test/team-knowledge-store-test.js +131 -0
  122. package/_site/_test/tracking-start-test.js +143 -0
  123. package/_site/_test/ui-i18n-toggle-test.js +114 -0
  124. package/_site/_test/ui-smoke-test.js +73 -0
  125. package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
  126. package/_site/_test/workbench-permission-test.js +118 -0
  127. package/_site/_test/workspace-ui-contract-test.js +43 -0
  128. package/_site/lib/ai-profile-resolver.js +78 -0
  129. package/_site/lib/ai-vendor-presets.js +83 -0
  130. package/_site/lib/ai-workspace.js +104 -0
  131. package/_site/lib/atomic-file.js +204 -0
  132. package/_site/lib/automation-config.js +140 -0
  133. package/_site/lib/bridge-adapter.js +256 -0
  134. package/_site/lib/bridge-consumer-service.js +402 -0
  135. package/_site/lib/claude-cli-runner.js +1599 -0
  136. package/_site/lib/commit-conversation-binder.js +246 -0
  137. package/_site/lib/commit-processing-ledger.js +27 -0
  138. package/_site/lib/commit-prompt.js +202 -0
  139. package/_site/lib/commit-reconciler.js +444 -0
  140. package/_site/lib/context-pack-builder.js +299 -0
  141. package/_site/lib/contracts.js +279 -0
  142. package/_site/lib/conversation-exclusions.js +204 -0
  143. package/_site/lib/conversation-query-service.js +161 -0
  144. package/_site/lib/conversation-store.js +385 -0
  145. package/_site/lib/data-dir.js +81 -0
  146. package/_site/lib/data-state-classifier.js +40 -0
  147. package/_site/lib/embedding-service.js +85 -0
  148. package/_site/lib/evidence-bundle.js +228 -0
  149. package/_site/lib/folder-picker-output.js +24 -0
  150. package/_site/lib/git-runner.js +62 -0
  151. package/_site/lib/github-team-store.js +1175 -0
  152. package/_site/lib/hook-manager.js +202 -0
  153. package/_site/lib/index-builder.js +157 -0
  154. package/_site/lib/index-service.js +173 -0
  155. package/_site/lib/integration-installer.js +907 -0
  156. package/_site/lib/kb-framework.js +191 -0
  157. package/_site/lib/kb-validator.js +124 -0
  158. package/_site/lib/knowledge-db.js +363 -0
  159. package/_site/lib/knowledge-promotion.js +435 -0
  160. package/_site/lib/knowledge-retrieval-service.js +401 -0
  161. package/_site/lib/knowledge-schema.js +121 -0
  162. package/_site/lib/knowledge-tool-runtime.js +359 -0
  163. package/_site/lib/legacy-data-manifest.js +26 -0
  164. package/_site/lib/llm-client.js +173 -0
  165. package/_site/lib/markdown-knowledge-indexer.js +230 -0
  166. package/_site/lib/migration-service.js +442 -0
  167. package/_site/lib/model-context-windows.js +82 -0
  168. package/_site/lib/module-bridge.js +496 -0
  169. package/_site/lib/post-commit-automation.js +141 -0
  170. package/_site/lib/project-lifecycle-service.js +511 -0
  171. package/_site/lib/project-registry-store.js +129 -0
  172. package/_site/lib/project-store.js +262 -0
  173. package/_site/lib/prompt-registry.js +148 -0
  174. package/_site/lib/requirement-adapters.js +10 -0
  175. package/_site/lib/requirement-binder.js +112 -0
  176. package/_site/lib/requirement-recorder.js +278 -0
  177. package/_site/lib/runtime-endpoint.js +160 -0
  178. package/_site/lib/scanner.js +298 -0
  179. package/_site/lib/server-app.js +1520 -0
  180. package/_site/lib/settings-store.js +113 -0
  181. package/_site/lib/storage-layout.js +162 -0
  182. package/_site/lib/structured-logger.js +574 -0
  183. package/_site/scripts/codex-notify-fanout.cjs +51 -0
  184. package/_site/scripts/folder-picker.ps1 +156 -0
  185. package/_site/scripts/hook-trigger.js +156 -0
  186. package/_site/scripts/sync-release-version.js +158 -0
  187. package/_site/server.js +12 -0
  188. package/_site/start.bat +26 -0
  189. package/_site/stop.bat +11 -0
  190. package/bin/project-knowledge-integrations.js +241 -0
  191. package/bin/project-knowledge-kb.js +65 -0
  192. package/bin/project-knowledge-mcp.js +195 -0
  193. package/bin/project-knowledge.js +439 -0
  194. package/claude-prompts.json +21 -0
  195. package/docs/README.zh-CN.md +175 -0
  196. package/docs/assets/architecture.svg +182 -0
  197. package/docs/assets/dashboard.png +0 -0
  198. package/docs/assets/draft-review.png +0 -0
  199. package/docs/assets/logo.svg +50 -0
  200. package/docs/assets/runs-drafts.png +0 -0
  201. package/docs/assets/settings.png +0 -0
  202. package/docs/project-registry-schema.md +33 -0
  203. package/docs/team-knowledge-mode-a-plan.md +73 -0
  204. package/docs/testing-strategy.md +33 -0
  205. package/package.json +100 -0
  206. package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
  207. package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
  208. package/plugins/project-knowledge/.mcp.json +14 -0
  209. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
  210. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
  211. package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
  212. package/templates/change.md +30 -0
  213. package/templates/project-readme.md +52 -0
  214. package/ui/app.css +58 -0
  215. package/ui/app.js +607 -0
  216. package/ui/favicon.svg +38 -0
  217. package/ui/i18n.js +146 -0
  218. package/ui/index.html +162 -0
@@ -0,0 +1,90 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const runner = require('../lib/claude-cli-runner');
7
+
8
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-discovery-test-'));
9
+
10
+ (async () => {
11
+ try {
12
+ const nativeExe = path.join(temp, 'claude.exe');
13
+ const npmRoot = path.join(temp, 'npm');
14
+ const extensionlessNpmShim = path.join(npmRoot, 'claude');
15
+ const npmShim = path.join(npmRoot, 'claude.cmd');
16
+ const npmCli = path.join(npmRoot, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
17
+ const npmExe = path.join(npmRoot, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe');
18
+ fs.mkdirSync(path.dirname(npmExe), { recursive: true });
19
+ fs.writeFileSync(nativeExe, 'test');
20
+ fs.writeFileSync(extensionlessNpmShim, '#!/bin/sh');
21
+ fs.writeFileSync(npmShim, '@echo off');
22
+ fs.writeFileSync(npmCli, '#!/usr/bin/env node');
23
+ fs.writeFileSync(npmExe, 'native executable');
24
+
25
+ const fromEnv = await runner.findClaudeExecutableForSdk({
26
+ platform: 'win32',
27
+ env: { CLAUDE_CODE_EXECPATH: nativeExe },
28
+ runCommand: () => ({ status: 1, stdout: '' }),
29
+ });
30
+ assert.equal(fromEnv.cmd, nativeExe);
31
+ assert.equal(fromEnv.source, 'CLAUDE_CODE_EXECPATH');
32
+
33
+ const fromPath = await runner.findClaudeExecutableForSdk({
34
+ platform: 'win32',
35
+ env: {},
36
+ runCommand: () => ({ status: 0, stdout: `${nativeExe}\r\n` }),
37
+ });
38
+ assert.equal(fromPath.cmd, nativeExe);
39
+ assert.equal(fromPath.source, 'PATH');
40
+
41
+ const fromExtensionlessNpmShim = await runner.findClaudeExecutableForSdk({
42
+ platform: 'win32',
43
+ env: {},
44
+ runCommand: () => ({ status: 0, stdout: `${extensionlessNpmShim}\r\n${npmShim}\r\n` }),
45
+ });
46
+ assert.equal(fromExtensionlessNpmShim.cmd, npmExe,
47
+ 'extensionless npm shell shim should resolve to the package native executable');
48
+
49
+ const fromCmdNpmShim = await runner.findClaudeExecutableForSdk({
50
+ platform: 'win32',
51
+ env: { CLAUDE_CODE_EXECPATH: npmShim },
52
+ runCommand: () => ({ status: 1, stdout: '' }),
53
+ });
54
+ assert.equal(fromCmdNpmShim.cmd, npmExe,
55
+ 'configured npm .cmd shim should resolve to the package native executable');
56
+ assert.equal(fromCmdNpmShim.runtimeExecutable, null,
57
+ 'native Claude executable should not need a JavaScript runtime launcher');
58
+
59
+ fs.rmSync(npmExe);
60
+ const fromCliScript = await runner.findClaudeExecutableForSdk({
61
+ platform: 'win32',
62
+ env: { CLAUDE_CODE_EXECPATH: npmShim },
63
+ runtimeExecutable: process.execPath,
64
+ runCommand: () => ({ status: 1, stdout: '' }),
65
+ });
66
+ assert.equal(fromCliScript.cmd, npmCli,
67
+ 'npm shim should fall back to the installed Claude cli.js');
68
+ assert.equal(fromCliScript.runtimeExecutable, process.execPath,
69
+ 'Claude cli.js must use the current runtime instead of PATH node');
70
+ const scriptLaunch = spawnSync(fromCliScript.runtimeExecutable, [fromCliScript.cmd], {
71
+ encoding: 'utf8',
72
+ windowsHide: true,
73
+ });
74
+ assert.equal(scriptLaunch.status, 0,
75
+ `resolved Claude cli.js should launch through the selected runtime: ${scriptLaunch.stderr}`);
76
+
77
+ const missing = await runner.findClaudeExecutableForSdk({
78
+ platform: 'win32',
79
+ env: {},
80
+ exists: () => false,
81
+ runCommand: () => ({ status: 1, stdout: '' }),
82
+ });
83
+ assert.equal(missing.cmd, null);
84
+ assert.match(missing.reason, /Claude Code was not found/);
85
+
86
+ console.log('claude-executable-discovery-test: PASS');
87
+ } finally {
88
+ fs.rmSync(temp, { recursive: true, force: true });
89
+ }
90
+ })();
@@ -0,0 +1,252 @@
1
+ // Run: node _site/_test/claude-session-lifecycle-sweep-test.js
2
+ //
3
+ // Regression test for the stale-active-session sweep in claude-cli-runner.
4
+ //
5
+ // Without the sweeper, three failure modes leave sessions stuck in
6
+ // state=running forever after the subprocess is gone:
7
+ // 1. dashboard restart kills the SDK parent query; claude.exe becomes
8
+ // orphaned; on reload the persisted record still says "running"
9
+ // 2. SIGKILL of the dashboard before the SDK for-await loop emits its
10
+ // terminal message
11
+ // 3. child Claude.exe dies under IPC drop; in-memory session has
12
+ // subprocess=null while state=running
13
+ //
14
+ // Symptom in production: a project keeps showing the pulsing "running"
15
+ // badge and the embedded terminal view stays empty because no live
16
+ // subprocess streams into it. Until the sweeper ships, the only manual
17
+ // workaround is a dashboard restart that the user has to remember.
18
+
19
+ const fs = require('fs');
20
+ const os = require('os');
21
+ const path = require('path');
22
+
23
+ const TMP_DATA = fs.mkdtempSync(path.join(os.tmpdir(), `kb-sweep-test-${process.pid}-`));
24
+ process.env.KB_DATA_DIR = TMP_DATA;
25
+ process.env.KB_STALE_ACTIVE_MS = '60000';
26
+ require(path.resolve(__dirname, '..', '..', '_site', 'lib', 'data-dir'))._resetCache();
27
+
28
+ // Seed the v2 registry and per-project config/state before loading the runner.
29
+ const PROJECT_IDS = ['demo-zombie-persisted', 'demo-zombie-memory', 'demo-fresh', 'demo-broadcast', 'demo-callback'];
30
+ const { defaultProjectConfig, defaultProjectState } = require('../lib/project-store');
31
+ const now = new Date().toISOString();
32
+ fs.mkdirSync(path.join(TMP_DATA, 'projects'), { recursive: true });
33
+ for (const projectId of PROJECT_IDS) {
34
+ const knowledgePath = path.join(TMP_DATA, 'knowledge', projectId);
35
+ const metadataPath = path.join(TMP_DATA, 'projects', projectId);
36
+ fs.mkdirSync(knowledgePath, { recursive: true });
37
+ fs.mkdirSync(metadataPath, { recursive: true });
38
+ fs.writeFileSync(path.join(metadataPath, 'config.json'), JSON.stringify(defaultProjectConfig(projectId, {
39
+ displayName: projectId, storageName: projectId, repoPath: TMP_DATA, knowledgePath,
40
+ }), null, 2) + '\n', 'utf8');
41
+ fs.writeFileSync(path.join(metadataPath, 'state.json'), JSON.stringify(defaultProjectState(), null, 2) + '\n', 'utf8');
42
+ }
43
+ fs.writeFileSync(path.join(TMP_DATA, 'projects.json'), JSON.stringify({
44
+ schema: 'project-registry/v2', schemaVersion: 2, projectOrder: PROJECT_IDS,
45
+ projects: Object.fromEntries(PROJECT_IDS.map(projectId => [projectId, { createdAt: now, displayNameSnapshot: projectId }])),
46
+ revision: 1, updatedAt: now,
47
+ }, null, 2) + '\n', 'utf8');
48
+ fs.writeFileSync(path.join(TMP_DATA, 'claude-prompts.json'), '{"schema":"prompts/v1","prompts":{}}', 'utf-8');
49
+
50
+ const runner = require('../lib/claude-cli-runner');
51
+ const {
52
+ createSession,
53
+ demoteStaleActiveSessions,
54
+ ACTIVE_STATES,
55
+ TERMINAL_STATES,
56
+ listSessions,
57
+ subscribeList,
58
+ } = runner;
59
+
60
+ function assert(cond, msg) { if (!cond) throw new Error('ASSERT: ' + msg); }
61
+
62
+ function writePersistedRecord(projectSlug, record) {
63
+ const aiDir = path.join(TMP_DATA, 'runtime', 'claude-sessions', projectSlug);
64
+ fs.mkdirSync(aiDir, { recursive: true });
65
+ const target = path.join(aiDir, `${record.sessionId}.json`);
66
+ // Default updatedAt to the same stale time used for startedAt so the
67
+ // sweeper treats the record as orphan-aged. Callers can override either.
68
+ const longAgo = record.startedAt || new Date(Date.now() - 10 * 60 * 1000).toISOString();
69
+ fs.writeFileSync(target, JSON.stringify({
70
+ schema: 'claude-workbench-session/v1',
71
+ updatedAt: record.updatedAt || longAgo,
72
+ ...record,
73
+ }, null, 2) + '\n', 'utf-8');
74
+ return target;
75
+ }
76
+
77
+ function makeStalePersistedRecord(projectSlug, sessionId, state) {
78
+ const longAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString();
79
+ return writePersistedRecord(projectSlug, {
80
+ sessionId,
81
+ projectSlug,
82
+ projectPath: 'D:/proj/' + projectSlug,
83
+ kbPath: path.join(TMP_DATA, 'knowledge', projectSlug),
84
+ promptKey: 'post-commit-automation',
85
+ runner: 'sdk',
86
+ state,
87
+ startedAt: longAgo,
88
+ endedAt: null,
89
+ exitCode: null,
90
+ turns: 1,
91
+ error: null,
92
+ pendingPermission: null,
93
+ source: 'git-hook',
94
+ automation: true,
95
+ automationRunId: 'auto-test-1',
96
+ metadata: { source: 'git-hook', automation: true, automationRunId: 'auto-test-1' },
97
+ permissionMode: 'bypassPermissions',
98
+ events: [{ type: 'claude/system-prompt', text: 'fake', promptKey: 'post-commit-automation' }],
99
+ });
100
+ }
101
+
102
+ (() => {
103
+ // The sweeper itself is the unit under test.
104
+ assert(typeof demoteStaleActiveSessions === 'function',
105
+ 'demoteStaleActiveSessions must be exported');
106
+ assert(ACTIVE_STATES.has('running'),
107
+ 'ACTIVE_STATES must include running');
108
+ assert(TERMINAL_STATES.has('failed'),
109
+ 'TERMINAL_STATES must include failed');
110
+
111
+ // (1) Persisted record that's been "running" for 10 minutes with no live
112
+ // subprocess — must be demoted. The user's exact scenario: dashboard
113
+ // restart orphaned the SDK parent query and the record on disk still
114
+ // says running.
115
+ {
116
+ const sessionId = 'sess-zombie-persisted-' + Date.now();
117
+ makeStalePersistedRecord('demo-zombie-persisted', sessionId, 'running');
118
+
119
+ const demoted = demoteStaleActiveSessions({ thresholdMs: 60 * 1000 });
120
+ const hit = demoted.find(d => d.sessionId === sessionId);
121
+ assert(hit, 'sweeper should have demoted the persisted zombie');
122
+ assert(hit.source === 'persisted', `expected source=persisted, got ${hit.source}`);
123
+
124
+ // The rehydrated session in memory should now show failed + endedAt.
125
+ const state = runner.getState(sessionId);
126
+ assert(state.state === 'failed',
127
+ `expected state=failed after sweep, got ${state.state}`);
128
+ assert(state.endedAt, 'endedAt should be set after demotion');
129
+ assert(state.error && /subprocess lost/i.test(state.error),
130
+ `error message should explain the orphan, got ${state.error}`);
131
+
132
+ // Subsequent sweeps should be idempotent — already-failed sessions are
133
+ // not in ACTIVE_STATES so they don't get touched again.
134
+ const secondSweep = demoteStaleActiveSessions({ thresholdMs: 60 * 1000 });
135
+ const hitAgain = secondSweep.find(d => d.sessionId === sessionId);
136
+ assert(!hitAgain,
137
+ `sweeper must not re-demote already-failed session (hit again: ${JSON.stringify(secondSweep)})`);
138
+ }
139
+
140
+ // (2) In-memory session with state=running AND subprocess=null AND a
141
+ // stale updatedAt — must be demoted. This is the runtime-orphan case:
142
+ // SDK query was kicked off, but the subprocess ref was cleared without
143
+ // a terminal setState (e.g. the dashboard process died between the
144
+ // subprocess=null assignment and the setState(failed) on the catch path).
145
+ {
146
+ const projectSlug = 'demo-zombie-memory';
147
+ const session = createSession({
148
+ projectSlug,
149
+ projectPath: 'D:/proj/' + projectSlug,
150
+ kbPath: path.join(TMP_DATA, 'knowledge', projectSlug),
151
+ promptKey: 'post-commit-automation',
152
+ source: 'git-hook',
153
+ metadata: { source: 'git-hook', automation: true, automationRunId: 'auto-mem' },
154
+ });
155
+ // Force it into the orphan shape: state=running, subprocess=null,
156
+ // updatedAt way in the past.
157
+ session.state = 'running';
158
+ session.subprocess = null;
159
+ session.updatedAt = new Date(Date.now() - 10 * 60 * 1000).toISOString();
160
+ session.startedAt = session.updatedAt;
161
+
162
+ const demoted = demoteStaleActiveSessions({ thresholdMs: 60 * 1000 });
163
+ const hit = demoted.find(d => d.sessionId === session.sessionId);
164
+ assert(hit, 'sweeper should have demoted the in-memory zombie');
165
+ assert(hit.source === 'memory', `expected source=memory, got ${hit.source}`);
166
+
167
+ const state = runner.getState(session.sessionId);
168
+ assert(state.state === 'failed',
169
+ `expected in-memory session to be failed, got ${state.state}`);
170
+ assert(state.error && /subprocess exited/i.test(state.error),
171
+ `expected subprocess-null error, got ${state.error}`);
172
+ }
173
+
174
+ // (3) Fresh session (started 5s ago) must NOT be demoted even if
175
+ // subprocess happens to be null. Threshold exists exactly to keep
176
+ // legitimately-thinking sessions from being killed prematurely.
177
+ {
178
+ const projectSlug = 'demo-fresh';
179
+ const session = createSession({
180
+ projectSlug,
181
+ projectPath: 'D:/proj/' + projectSlug,
182
+ kbPath: path.join(TMP_DATA, 'knowledge', projectSlug),
183
+ promptKey: 'post-commit-automation',
184
+ source: 'git-hook',
185
+ metadata: { source: 'git-hook', automation: true, automationRunId: 'auto-fresh' },
186
+ });
187
+ session.state = 'running';
188
+ session.subprocess = null;
189
+ // updatedAt defaults to createdAt via createSession, well under 60s old.
190
+
191
+ const demoted = demoteStaleActiveSessions({ thresholdMs: 60 * 1000 });
192
+ const hit = demoted.find(d => d.sessionId === session.sessionId);
193
+ assert(!hit,
194
+ `fresh session (age <60s) must NOT be demoted; demoted list = ${JSON.stringify(demoted)}`);
195
+
196
+ // Cleanup so it doesn't leak into the next test.
197
+ runner.deleteSession(session.sessionId);
198
+ }
199
+
200
+ // (4) Demotion must fire the global SSE broadcast so the UI sees the
201
+ // running→failed transition. Without this the blue light stays on even
202
+ // after the in-memory state is wrong.
203
+ {
204
+ const projectSlug = 'demo-broadcast';
205
+ const broadcastEvents = [];
206
+ subscribeList(ev => broadcastEvents.push(ev));
207
+
208
+ const sessionId = 'sess-broadcast-' + Date.now();
209
+ makeStalePersistedRecord(projectSlug, sessionId, 'running');
210
+
211
+ demoteStaleActiveSessions({ thresholdMs: 60 * 1000 });
212
+ const sawTransition = broadcastEvents.some(
213
+ ev => ev.sessionId === sessionId && ev.state === 'failed' && ev.kind === 'stale-active-sweep'
214
+ );
215
+ assert(sawTransition,
216
+ `expected SSE broadcast for ${sessionId} with state=failed kind=stale-active-sweep; got ${JSON.stringify(broadcastEvents)}`);
217
+ }
218
+
219
+ // (5) The onSessionEnded hook must fire for failed sessions, so the
220
+ // automation queue (post-commit-automation.js) can promote the next
221
+ // queued run. Without this, stranded sessions would also strand any
222
+ // queued successor forever.
223
+ {
224
+ const projectSlug = 'demo-callback';
225
+ const sessionId = 'sess-callback-' + Date.now();
226
+ makeStalePersistedRecord(projectSlug, sessionId, 'running');
227
+
228
+ let endedWithFailed = null;
229
+ const off = runner.onSessionEnded(s => {
230
+ if (s.sessionId === sessionId && s.state === 'failed') {
231
+ endedWithFailed = s;
232
+ }
233
+ });
234
+
235
+ demoteStaleActiveSessions({ thresholdMs: 60 * 1000 });
236
+ // onSessionEnded fires via setImmediate, so wait one tick.
237
+ return Promise.resolve()
238
+ .then(() => new Promise(resolve => setImmediate(resolve)))
239
+ .then(() => {
240
+ off();
241
+ assert(endedWithFailed,
242
+ `onSessionEnded should have fired for the demoted session`);
243
+ assert(endedWithFailed.sessionId === sessionId,
244
+ `wrong sessionId in callback: ${endedWithFailed && endedWithFailed.sessionId}`);
245
+ });
246
+ }
247
+ })().then(() => {
248
+ console.log('claude-session-lifecycle-sweep-test PASS');
249
+ }).catch(e => {
250
+ console.error(e.stack || e.message);
251
+ process.exit(1);
252
+ });
@@ -0,0 +1,170 @@
1
+ // Claude workbench regression test.
2
+ //
3
+ // Verifies session metadata and the SDK-only session lifecycle. The legacy
4
+ // `claude -p` subprocess path was removed in the CC-Switch migration, so
5
+ // the pre-turn `requirePermission` flow no longer exists — the SDK path
6
+ // shows permission prompts via canUseTool only when an individual tool
7
+ // is actually invoked. Tool-level prompts are exercised by the chat
8
+ // panel integration tests.
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const runner = require('../lib/claude-cli-runner');
13
+
14
+ const ROOT = path.resolve(__dirname, '..', '..');
15
+ const TEMP_KB = path.join(ROOT, 'projects', 'claude-workbench-test-temp');
16
+ const TEMP_AI = path.join(ROOT, '_site', '_ai', 'claude-workbench-test-temp');
17
+
18
+ function assert(cond, msg) {
19
+ if (!cond) throw new Error(msg);
20
+ }
21
+
22
+ (async () => {
23
+ fs.rmSync(TEMP_KB, { recursive: true, force: true });
24
+ fs.rmSync(TEMP_AI, { recursive: true, force: true });
25
+ fs.mkdirSync(TEMP_KB, { recursive: true });
26
+
27
+ // 1. Default initial-analysis turn: SDK is the only mode. The session
28
+ // should be created without any pre-turn permission prompt (the
29
+ // SDK surfaces tool approvals lazily through canUseTool).
30
+ const started = runner.startSession({
31
+ slug: 'claude-workbench-test-temp',
32
+ projectPath: ROOT,
33
+ kbPath: TEMP_KB,
34
+ promptKey: 'initial-analysis',
35
+ aiProfile: {
36
+ id: 'test-profile',
37
+ implementation: 'claude-code-agent',
38
+ mainModel: 'test-model',
39
+ },
40
+ vars: {
41
+ SLUG: 'claude-workbench-test-temp',
42
+ PROJECT_PATH: ROOT,
43
+ PRIMARY_LANGUAGE: 'JavaScript',
44
+ KNOWLEDGE_LANGUAGE: 'zh-CN',
45
+ },
46
+ });
47
+
48
+ assert(started.sessionId, 'startSession should return sessionId');
49
+ assert(!started.pendingPermission,
50
+ 'SDK mode should not show a pre-turn UI permission prompt');
51
+ assert(started.runner === 'sdk',
52
+ `startSession should default to runner: 'sdk', got ${started.runner}`);
53
+
54
+ const state = runner.getState(started.sessionId);
55
+ assert(state.runner === 'sdk',
56
+ `session state should record runner: 'sdk', got ${state.runner}`);
57
+
58
+ runner.deleteSession(started.sessionId);
59
+
60
+ // 1b. The embedded terminal should be able to create an idle chat
61
+ // session without reading claude-prompts.json or running an
62
+ // initial-analysis prompt. The user's first typed message starts
63
+ // the actual SDK turn via sendInput.
64
+ const chat = runner.startChatSession({
65
+ slug: 'claude-workbench-test-temp',
66
+ projectPath: ROOT,
67
+ kbPath: TEMP_KB,
68
+ aiProfile: {
69
+ id: 'test-profile',
70
+ implementation: 'claude-code-agent',
71
+ mainModel: 'test-model',
72
+ },
73
+ permissionMode: 'default',
74
+ });
75
+ assert(chat.sessionId, 'startChatSession should return sessionId');
76
+ const chatState = runner.getState(chat.sessionId);
77
+ assert(chatState.promptKey === 'terminal-chat',
78
+ `chat session should use terminal-chat promptKey, got ${chatState.promptKey}`);
79
+ assert(chatState.state === 'idle',
80
+ `chat session should stay idle until user input, got ${chatState.state}`);
81
+ assert(chatState.turns === 0, `chat session should not start a turn, got turns=${chatState.turns}`);
82
+
83
+ // 1c. Switching AI profiles mid-session must not resume the old upstream
84
+ // Claude conversation with the new provider. That used to send the
85
+ // previous model (for example MiniMax-M3) to the new base URL (for
86
+ // example GLM), producing "model not found" API errors.
87
+ const chatSession = runner.getSession(chat.sessionId);
88
+ chatSession.aiProfileId = 'minimax-m3';
89
+ chatSession.model = 'MiniMax-M3';
90
+ chatSession.claudeSessionId = 'old-claude-session-id';
91
+ const switched = runner.applyAiProfileToSession(chatSession, {
92
+ id: 'glm',
93
+ implementation: 'claude-code-agent',
94
+ mainModel: 'glm-5.2',
95
+ });
96
+ const switchedState = runner.getState(chat.sessionId);
97
+ assert(switched.resetConversation === true, 'profile switch should reset the upstream conversation');
98
+ assert(switchedState.claudeSessionId === null, 'profile switch should clear stale claudeSessionId');
99
+ assert(switchedState.aiProfileId === 'glm', `profile switch should update aiProfileId, got ${switchedState.aiProfileId}`);
100
+ assert(switchedState.model === 'glm-5.2', `profile switch should update model, got ${switchedState.model}`);
101
+ runner.deleteSession(chat.sessionId);
102
+
103
+ // Session metadata uses durable atomic replacement, but a disk fault must
104
+ // remain fail-open for the live Workbench session and become observable.
105
+ const AtomicFile = require('../lib/atomic-file');
106
+ const originalAtomicWrite = AtomicFile.writeJsonAtomic;
107
+ const persistenceLogs = [];
108
+ runner.setLogger({ error: async (event, message, payload) => { persistenceLogs.push({ event, message, payload }); } });
109
+ AtomicFile.writeJsonAtomic = () => { throw Object.assign(new Error('simulated disk full'), { code: 'ENOSPC' }); };
110
+ let persistenceFaultSession;
111
+ try {
112
+ persistenceFaultSession = runner.createSession({
113
+ projectSlug: 'claude-workbench-test-temp', projectPath: ROOT, kbPath: TEMP_KB, promptKey: 'terminal-chat',
114
+ });
115
+ assert(persistenceFaultSession.sessionId, 'persistence failure must not interrupt the live session');
116
+ assert(persistenceLogs.some(entry => entry.event === 'claude.session_persist_failed' && entry.payload.error.code === 'ENOSPC'), 'persistence failure must be structured and correlated');
117
+ } finally {
118
+ AtomicFile.writeJsonAtomic = originalAtomicWrite;
119
+ runner.setLogger(null);
120
+ if (persistenceFaultSession) runner.deleteSession(persistenceFaultSession.sessionId);
121
+ }
122
+
123
+ const executable = await runner.findClaudeExecutableForSdk();
124
+ if (process.platform === 'win32' && executable.cmd) {
125
+ assert(!/\.(cmd|bat|ps1)$/i.test(executable.cmd),
126
+ `SDK executable must not be a Windows shell shim: ${executable.cmd}`);
127
+ }
128
+
129
+ // 2. Automation raw prompt sessions must preserve the selected permission
130
+ // mode. This guards against regressing back to bypassPermissions -> default.
131
+ process.env.KB_AUTOMATION_FAKE_CLAUDE = '1';
132
+ const auto = runner.startAutomationSession({
133
+ slug: 'claude-workbench-test-temp',
134
+ projectPath: ROOT,
135
+ kbPath: TEMP_KB,
136
+ userPrompt: 'automation test prompt',
137
+ systemPrompt: 'automation system prompt',
138
+ aiProfile: {
139
+ id: 'test-profile',
140
+ implementation: 'claude-code-agent',
141
+ mainModel: 'test-model',
142
+ },
143
+ permissionMode: 'bypassPermissions',
144
+ allowedTools: ['Read'],
145
+ safetyPolicy: {
146
+ kind: 'kb-automation',
147
+ kbPath: TEMP_KB,
148
+ knowledgeMode: 'requestApproval',
149
+ allowReadOnlyBash: false,
150
+ allowedTools: ['Read'],
151
+ canWriteKb: false,
152
+ },
153
+ metadata: { automation: true, automationRunId: 'auto-test', source: 'test' },
154
+ });
155
+ const autoState = runner.getState(auto.sessionId);
156
+ assert(autoState.permissionMode === 'bypassPermissions',
157
+ `automation session should preserve bypassPermissions, got ${autoState.permissionMode}`);
158
+ assert(autoState.automation === true, 'automation session should be marked automation=true');
159
+ runner.deleteSession(auto.sessionId);
160
+ delete process.env.KB_AUTOMATION_FAKE_CLAUDE;
161
+
162
+ fs.rmSync(TEMP_KB, { recursive: true, force: true });
163
+ fs.rmSync(TEMP_AI, { recursive: true, force: true });
164
+ console.log('Claude workbench test passed');
165
+ })().catch(err => {
166
+ try { fs.rmSync(TEMP_KB, { recursive: true, force: true }); } catch {}
167
+ try { fs.rmSync(TEMP_AI, { recursive: true, force: true }); } catch {}
168
+ console.error(err);
169
+ process.exit(1);
170
+ });