@sanqianx/project-knowledge 5.0.19 → 5.0.20-rc.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 (200) hide show
  1. package/.agents/plugins/marketplace.json +3 -3
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +5 -0
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +349 -349
  7. package/_modules/knowledge-engine/README.md +4 -0
  8. package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
  9. package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
  10. package/_modules/knowledge-engine/src/coordinator.js +7 -0
  11. package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
  12. package/_modules/knowledge-engine/src/server.js +1 -1
  13. package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
  14. package/_site/_test/ai-profile-test.js +75 -75
  15. package/_site/_test/ai-vendor-presets-test.js +91 -91
  16. package/_site/_test/automatic-context-test.js +70 -70
  17. package/_site/_test/bin-cli-test.js +230 -230
  18. package/_site/_test/bridge-adapter-test.js +95 -95
  19. package/_site/_test/bridge-consumer-service-test.js +310 -310
  20. package/_site/_test/candidate-bin-content-test.js +35 -35
  21. package/_site/_test/candidate-product-assets-test.js +13 -13
  22. package/_site/_test/candidate-release-contract-test.js +14 -14
  23. package/_site/_test/capture-hook-dedup-test.js +158 -158
  24. package/_site/_test/claude-executable-discovery-test.js +28 -28
  25. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
  26. package/_site/_test/cli-instance-management-test.js +62 -62
  27. package/_site/_test/client-capture-restart-e2e-test.js +189 -189
  28. package/_site/_test/codex-conversation-projection-test.js +60 -60
  29. package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
  30. package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
  31. package/_site/_test/codex-notify-fanout-test.js +134 -134
  32. package/_site/_test/codex-notify-upgrade-test.js +253 -253
  33. package/_site/_test/commit-boundary-freeze-test.js +147 -147
  34. package/_site/_test/commit-conversation-binding-test.js +4 -4
  35. package/_site/_test/configuration-generation-test.js +22 -22
  36. package/_site/_test/context-pack-test.js +200 -200
  37. package/_site/_test/conversation-api-test.js +10 -10
  38. package/_site/_test/conversation-origin-test.js +15 -15
  39. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
  40. package/_site/_test/embedding-config-test.js +32 -32
  41. package/_site/_test/fixtures/fake-browser-exit.js +10 -10
  42. package/_site/_test/fixtures/fake-browser-hang.js +10 -10
  43. package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
  44. package/_site/_test/fixtures/make-git-repos.js +92 -92
  45. package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
  46. package/_site/_test/fresh-hook-reimport-test.js +53 -53
  47. package/_site/_test/fresh-product-data-test.js +56 -56
  48. package/_site/_test/git-path-compatibility-test.js +80 -80
  49. package/_site/_test/git-validation-test.js +81 -81
  50. package/_site/_test/hook-worktree-test.js +42 -42
  51. package/_site/_test/index-builder-test.js +41 -41
  52. package/_site/_test/index-error-display-contract-test.js +10 -10
  53. package/_site/_test/integration-adapters-test.js +67 -67
  54. package/_site/_test/integration-capture-setup-test.js +118 -118
  55. package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
  56. package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
  57. package/_site/_test/integration-runtime-binding-test.js +473 -473
  58. package/_site/_test/integration-status-scan-test.js +29 -29
  59. package/_site/_test/kb-framework-test.js +57 -57
  60. package/_site/_test/kbpath-follow-test.js +63 -63
  61. package/_site/_test/knowledge-db-test.js +144 -144
  62. package/_site/_test/knowledge-engine-client-test.js +109 -109
  63. package/_site/_test/knowledge-maintenance-test.js +40 -40
  64. package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
  65. package/_site/_test/knowledge-scopes-test.js +37 -37
  66. package/_site/_test/knowledge-storage-location-test.js +28 -28
  67. package/_site/_test/launcher-readiness-test.js +80 -80
  68. package/_site/_test/legacy-claude-session-alias-test.js +106 -106
  69. package/_site/_test/legacy-routes-removed-test.js +47 -47
  70. package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
  71. package/_site/_test/live-gateway-test.js +48 -48
  72. package/_site/_test/local-marketplace-test.js +16 -16
  73. package/_site/_test/log-redaction-test.js +58 -58
  74. package/_site/_test/markdown-maintenance-test.js +11 -11
  75. package/_site/_test/model-context-windows-test.js +89 -89
  76. package/_site/_test/module-agent-start-proxy-test.js +65 -65
  77. package/_site/_test/module-artifact-integrity-test.js +27 -27
  78. package/_site/_test/module-boundary-test.js +30 -30
  79. package/_site/_test/module-bridge-eventbridge-test.js +202 -202
  80. package/_site/_test/module-dynamic-port-test.js +118 -118
  81. package/_site/_test/module-index-proxy-test.js +33 -33
  82. package/_site/_test/module-model-configuration-test.js +44 -44
  83. package/_site/_test/module-process-lifecycle-test.js +35 -35
  84. package/_site/_test/module-stream-proxy-test.js +34 -34
  85. package/_site/_test/module-terminal-restart-test.js +63 -63
  86. package/_site/_test/module-wiring-unbind-race-test.js +59 -59
  87. package/_site/_test/native-context-integrations-test.js +86 -86
  88. package/_site/_test/orphaned-claim-recovery-test.js +80 -80
  89. package/_site/_test/package-boundary-test.js +43 -42
  90. package/_site/_test/package-startup-test.js +56 -56
  91. package/_site/_test/packaged-ui-smoke-test.js +88 -88
  92. package/_site/_test/pr-consumer-contract-test.js +55 -55
  93. package/_site/_test/product-brand-contract-test.js +25 -25
  94. package/_site/_test/product-diagnostics-ui-test.js +93 -93
  95. package/_site/_test/product-import-ui-test.js +104 -104
  96. package/_site/_test/product-progress-status-test.js +12 -1
  97. package/_site/_test/project-remove-running-guard-test.js +79 -79
  98. package/_site/_test/project-store-test.js +90 -90
  99. package/_site/_test/registry-events-test.js +38 -38
  100. package/_site/_test/release-version-sync-test.js +115 -115
  101. package/_site/_test/requirement-binding-test.js +113 -113
  102. package/_site/_test/runtime-endpoint-test.js +64 -64
  103. package/_site/_test/scanner-test.js +83 -83
  104. package/_site/_test/server-security-test.js +96 -96
  105. package/_site/_test/sessions-stream-test.js +93 -93
  106. package/_site/_test/shared-scope-root-binding-test.js +67 -67
  107. package/_site/_test/simple-import-test.js +98 -98
  108. package/_site/_test/startup-analysis-disabled-test.js +6 -6
  109. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
  110. package/_site/_test/team-knowledge-store-test.js +132 -132
  111. package/_site/_test/test-server-cleanup-test.js +35 -35
  112. package/_site/_test/test-server-module-isolation-test.js +37 -37
  113. package/_site/_test/tracking-start-test.js +35 -35
  114. package/_site/_test/workbench-context-receipts-test.js +22 -22
  115. package/_site/_test/workspace-ui-contract-test.js +31 -31
  116. package/_site/_test/zcode-capture-integration-test.js +108 -108
  117. package/_site/lib/ai-vendor-presets.js +83 -83
  118. package/_site/lib/ai-workspace.js +3 -3
  119. package/_site/lib/atomic-file.js +204 -204
  120. package/_site/lib/bridge-adapter.js +260 -260
  121. package/_site/lib/capture-hook-config.js +108 -108
  122. package/_site/lib/claude-cli-runner.js +89 -89
  123. package/_site/lib/cli-instances.js +43 -43
  124. package/_site/lib/codex-context-status.js +17 -17
  125. package/_site/lib/codex-notify-config.js +193 -193
  126. package/_site/lib/commit-conversation-binder.js +12 -12
  127. package/_site/lib/commit-reconciler.js +38 -38
  128. package/_site/lib/context-pack-builder.js +299 -299
  129. package/_site/lib/conversation-exclusions.js +204 -204
  130. package/_site/lib/conversation-query-service.js +17 -17
  131. package/_site/lib/data-dir.js +13 -13
  132. package/_site/lib/embedding-service.js +85 -85
  133. package/_site/lib/engine-connection.js +93 -93
  134. package/_site/lib/engine-knowledge-client.js +72 -72
  135. package/_site/lib/folder-picker-output.js +3 -3
  136. package/_site/lib/git-path.js +14 -14
  137. package/_site/lib/github-team-store.js +3 -3
  138. package/_site/lib/index-builder.js +157 -157
  139. package/_site/lib/integration-status-scan.js +99 -99
  140. package/_site/lib/integration-status-worker.js +14 -14
  141. package/_site/lib/kb-framework.js +15 -15
  142. package/_site/lib/knowledge-db.js +363 -363
  143. package/_site/lib/knowledge-schema.js +121 -121
  144. package/_site/lib/live-gateway.js +78 -78
  145. package/_site/lib/llm-client.js +12 -12
  146. package/_site/lib/model-context-windows.js +82 -82
  147. package/_site/lib/module-bridge.js +874 -874
  148. package/_site/lib/module-port-resolver.js +57 -57
  149. package/_site/lib/post-commit-automation.js +54 -54
  150. package/_site/lib/product-data.js +35 -35
  151. package/_site/lib/project-lifecycle-service.js +14 -14
  152. package/_site/lib/project-registry-store.js +129 -129
  153. package/_site/lib/prompt-registry.js +148 -148
  154. package/_site/lib/registry-events.js +16 -16
  155. package/_site/lib/requirement-adapters.js +10 -10
  156. package/_site/lib/requirement-binder.js +112 -112
  157. package/_site/lib/runtime-control.js +89 -89
  158. package/_site/lib/runtime-endpoint.js +5 -5
  159. package/_site/lib/zcode-context-integration.js +60 -60
  160. package/_site/scripts/codex-notify-fanout.cjs +95 -95
  161. package/_site/scripts/install-module-candidates.js +69 -69
  162. package/_site/scripts/pack-module-candidates.js +89 -89
  163. package/_site/scripts/sync-product-content.js +28 -28
  164. package/_site/scripts/sync-release-version.js +164 -164
  165. package/_site/scripts/vendor-modules.js +49 -49
  166. package/_site/scripts/verify-module-runtime.js +50 -50
  167. package/_site/scripts/verify-product-runtime.js +36 -36
  168. package/_site/scripts/verify-retrieval-gold.js +83 -83
  169. package/_site/server.js +15 -15
  170. package/_site/start.bat +26 -26
  171. package/_site/stop.bat +11 -11
  172. package/bin/project-knowledge-integrations.js +254 -254
  173. package/bin/project-knowledge-kb.js +67 -67
  174. package/bin/project-knowledge-mcp.js +8 -8
  175. package/bin/project-knowledge.js +170 -170
  176. package/claude-prompts.json +21 -21
  177. package/docs/README.zh-CN.md +205 -205
  178. package/docs/assets/architecture.svg +181 -181
  179. package/docs/assets/logo.svg +49 -49
  180. package/docs/fresh-v5-start.md +42 -42
  181. package/docs/leader-glm-workbench-playbook.md +122 -122
  182. package/docs/project-registry-schema.md +11 -11
  183. package/docs/release-5.0.16-acceptance.md +176 -176
  184. package/docs/release-5.0.17-acceptance.md +117 -117
  185. package/docs/release-5.0.19-acceptance.md +39 -25
  186. package/docs/team-knowledge-mode-a-plan.md +73 -73
  187. package/module-runtime-manifest.json +53 -33
  188. package/package.json +3 -2
  189. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  190. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  191. package/plugins/project-knowledge/.mcp.json +1 -1
  192. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  193. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  194. package/templates/change.md +30 -30
  195. package/templates/project-readme.md +18 -18
  196. package/ui/index.html +1 -1
  197. package/ui/knowledge-panel.html +118 -0
  198. package/ui/product.css +8 -0
  199. package/ui/product.js +146 -4
  200. package/vendor-manifest.json +55 -54
@@ -1,132 +1,132 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { execFileSync } = require('child_process');
6
- const { spawnServer } = require('./helpers/spawn-server');
7
- const freePort = require('./helpers/free-port');
8
- const githubTeamStore = require('../lib/github-team-store');
9
-
10
- const ROOT = path.resolve(__dirname, '..', '..');
11
- let PORT, BASE_URL;
12
- const TEMP = fs.mkdtempSync(path.join(os.tmpdir(), `kb-team-v2-${process.pid}-`));
13
- const DATA_DIR = path.join(TEMP, 'data');
14
- const SOURCE_ONE = path.join(TEMP, 'source-one');
15
- const SOURCE_TWO = path.join(TEMP, 'source-two');
16
- const SOURCE_BAD = path.join(TEMP, 'source-bad');
17
- const TEAM_STORE = path.join(TEMP, 'team-store');
18
- const TEAM_REMOTE = path.join(TEMP, 'team-remote');
19
- const TEAM_CLONE = path.join(TEMP, 'team-clone');
20
-
21
- function git(cwd, args) {
22
- return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
23
- }
24
-
25
- function initializeRepo(cwd, title) {
26
- fs.mkdirSync(cwd, { recursive: true });
27
- git(cwd, ['init', '--initial-branch=main']);
28
- git(cwd, ['config', 'user.name', 'Team Test']);
29
- git(cwd, ['config', 'user.email', 'team@example.test']);
30
- fs.writeFileSync(path.join(cwd, 'README.md'), `# ${title}\n`, 'utf8');
31
- git(cwd, ['add', '.']);
32
- git(cwd, ['commit', '-m', 'baseline']);
33
- }
34
-
35
- async function waitForServer() {
36
- for (let attempt = 0; attempt < 100; attempt += 1) {
37
- try { if ((await fetch(`${BASE_URL}/api/health`)).ok) return; } catch {}
38
- await new Promise(resolve => setTimeout(resolve, 100));
39
- }
40
- throw new Error('server did not start');
41
- }
42
-
43
- async function json(method, route, body) {
44
- const response = await fetch(`${BASE_URL}${route}`, {
45
- method,
46
- headers: body ? { 'Content-Type': 'application/json' } : undefined,
47
- body: body ? JSON.stringify(body) : undefined,
48
- });
49
- const text = await response.text();
50
- return { response, body: text ? JSON.parse(text) : {} };
51
- }
52
-
53
- (async () => {
54
- PORT = process.env.KB_TEAM_KNOWLEDGE_PORT ? Number(process.env.KB_TEAM_KNOWLEDGE_PORT) : await freePort(); BASE_URL = `http://127.0.0.1:${PORT}`;
55
- let spawned;
56
- try {
57
- initializeRepo(SOURCE_ONE, 'Source one');
58
- initializeRepo(SOURCE_TWO, 'Source two');
59
- initializeRepo(SOURCE_BAD, 'Source bad');
60
- initializeRepo(TEAM_STORE, 'Team store');
61
- fs.mkdirSync(path.join(TEAM_STORE, 'acc'), { recursive: true });
62
- fs.writeFileSync(path.join(TEAM_STORE, 'acc', 'README.md'), '# ACC shared knowledge\n', 'utf8');
63
- git(TEAM_STORE, ['add', '.']);
64
- git(TEAM_STORE, ['commit', '-m', 'add team knowledge']);
65
- git(TEAM_STORE, ['remote', 'add', 'origin', TEAM_STORE]);
66
-
67
- initializeRepo(TEAM_REMOTE, 'Remote team store');
68
- fs.mkdirSync(path.join(TEAM_REMOTE, 'acc'), { recursive: true });
69
- fs.writeFileSync(path.join(TEAM_REMOTE, 'acc', 'README.md'), '# ACC from remote\n', 'utf8');
70
- git(TEAM_REMOTE, ['add', '.']);
71
- git(TEAM_REMOTE, ['commit', '-m', 'add remote knowledge']);
72
-
73
- const providerConfigPath = path.join(TEMP, 'providers.json');
74
- fs.writeFileSync(providerConfigPath, `\uFEFF${JSON.stringify({ gitea: { webBaseUrl: 'https://gitea.example.test', oauthClientId: 'client', oauthClientSecret: 'secret' } })}`, 'utf8');
75
- const providerFile = githubTeamStore.readProviderFileConfig(providerConfigPath);
76
- const publicProviders = githubTeamStore.providerPublicConfig(githubTeamStore.defaultConfig(), {}, providerFile);
77
- assert.strictEqual(publicProviders.gitea.configured, true);
78
- assert.strictEqual(publicProviders.gitea.oauthClientSecret, undefined);
79
- assert.strictEqual(publicProviders.gitea.oauthClientSecretConfigured, true);
80
- assert.strictEqual(githubTeamStore.proxyUrlForTarget('http://127.0.0.1:3000/api/v1/user', { KB_GIT_PROXY: 'http://127.0.0.1:7890' }), '');
81
-
82
- const scanned = await githubTeamStore.scanLocalStore({ localPath: TEAM_STORE });
83
- assert(scanned.ok && scanned.store.knowledgeBases.some(item => item.path === 'acc'), JSON.stringify(scanned));
84
- const checkedOut = await githubTeamStore.checkoutStore({ cloneUrl: TEAM_REMOTE, branch: 'main', localPath: TEAM_CLONE });
85
- assert(checkedOut.ok, JSON.stringify(checkedOut));
86
- assert.match(fs.readFileSync(path.join(TEAM_CLONE, 'acc', 'README.md'), 'utf8'), /from remote/);
87
-
88
- spawned = spawnServer({ root: ROOT, port: PORT, dataDir: DATA_DIR, tag: 'team-kb-v2' });
89
- let output = '';
90
- spawned.child.stdout.on('data', chunk => { output += chunk; });
91
- spawned.child.stderr.on('data', chunk => { output += chunk; });
92
- await waitForServer();
93
- let result = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(TEMP, 'personal-knowledge') } });
94
- assert(result.response.ok, JSON.stringify(result.body));
95
-
96
- result = await json('POST', '/api/projects/import', {
97
- projectId: 'team-project-one', localPath: SOURCE_ONE,
98
- teamKnowledgeBase: {
99
- storeLocalPath: TEAM_STORE, storeRemoteUrl: 'https://github.com/org/knowledge.git',
100
- storeFullName: 'org/knowledge', storeId: 'team-store', kbId: 'kb-acc', kbSlug: 'acc', kbSubdir: 'acc', displayName: 'ACC',
101
- },
102
- });
103
- assert.strictEqual(result.response.status, 201, `${JSON.stringify(result.body)}\n${output}`);
104
- assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_STORE, 'acc'));
105
- assert.strictEqual(result.body.config.teamBinding.provider, 'github');
106
- assert.strictEqual(result.body.config.teamBinding.kbId, 'kb-acc');
107
- assert.strictEqual(result.body.config.teamBinding.kbSubdir, 'acc');
108
- const registryText = fs.readFileSync(path.join(DATA_DIR, 'projects.json'), 'utf8');
109
- assert(!registryText.includes(TEAM_STORE), 'minimal v2 registry must not contain the external team path');
110
-
111
- result = await json('POST', '/api/projects/import', {
112
- projectId: 'team-project-bad', localPath: SOURCE_BAD,
113
- teamKnowledgeBase: { storeLocalPath: TEAM_STORE, kbSubdir: '../escape' },
114
- });
115
- assert.strictEqual(result.response.status, 403, 'team binding traversal must be rejected');
116
-
117
- result = await json('POST', '/api/projects/import', {
118
- projectId: 'team-project-two', localPath: SOURCE_TWO,
119
- teamKnowledgeBase: { storeLocalPath: TEAM_CLONE, storeRemoteUrl: TEAM_REMOTE, storeId: 'clone', kbId: 'kb-acc', kbSubdir: 'acc' },
120
- });
121
- assert.strictEqual(result.response.status, 201, JSON.stringify(result.body));
122
- assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_CLONE, 'acc'));
123
-
124
- result = await json('DELETE', '/api/projects/team-project-one', { deleteKnowledge: true, confirmationToken: 'team-project-one' });
125
- assert(result.response.ok, JSON.stringify(result.body));
126
- assert.strictEqual(result.body.removedKnowledge, false, 'team knowledge is externally owned');
127
- assert(fs.existsSync(path.join(TEAM_STORE, 'acc', 'README.md')), 'team knowledge must survive project deletion');
128
- console.log('team knowledge store test passed');
129
- } finally {
130
- if (spawned) spawned.child.kill();
131
- }
132
- })().catch(error => { console.error(error); process.exitCode = 1; });
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+ const { spawnServer } = require('./helpers/spawn-server');
7
+ const freePort = require('./helpers/free-port');
8
+ const githubTeamStore = require('../lib/github-team-store');
9
+
10
+ const ROOT = path.resolve(__dirname, '..', '..');
11
+ let PORT, BASE_URL;
12
+ const TEMP = fs.mkdtempSync(path.join(os.tmpdir(), `kb-team-v2-${process.pid}-`));
13
+ const DATA_DIR = path.join(TEMP, 'data');
14
+ const SOURCE_ONE = path.join(TEMP, 'source-one');
15
+ const SOURCE_TWO = path.join(TEMP, 'source-two');
16
+ const SOURCE_BAD = path.join(TEMP, 'source-bad');
17
+ const TEAM_STORE = path.join(TEMP, 'team-store');
18
+ const TEAM_REMOTE = path.join(TEMP, 'team-remote');
19
+ const TEAM_CLONE = path.join(TEMP, 'team-clone');
20
+
21
+ function git(cwd, args) {
22
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
23
+ }
24
+
25
+ function initializeRepo(cwd, title) {
26
+ fs.mkdirSync(cwd, { recursive: true });
27
+ git(cwd, ['init', '--initial-branch=main']);
28
+ git(cwd, ['config', 'user.name', 'Team Test']);
29
+ git(cwd, ['config', 'user.email', 'team@example.test']);
30
+ fs.writeFileSync(path.join(cwd, 'README.md'), `# ${title}\n`, 'utf8');
31
+ git(cwd, ['add', '.']);
32
+ git(cwd, ['commit', '-m', 'baseline']);
33
+ }
34
+
35
+ async function waitForServer() {
36
+ for (let attempt = 0; attempt < 100; attempt += 1) {
37
+ try { if ((await fetch(`${BASE_URL}/api/health`)).ok) return; } catch {}
38
+ await new Promise(resolve => setTimeout(resolve, 100));
39
+ }
40
+ throw new Error('server did not start');
41
+ }
42
+
43
+ async function json(method, route, body) {
44
+ const response = await fetch(`${BASE_URL}${route}`, {
45
+ method,
46
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
47
+ body: body ? JSON.stringify(body) : undefined,
48
+ });
49
+ const text = await response.text();
50
+ return { response, body: text ? JSON.parse(text) : {} };
51
+ }
52
+
53
+ (async () => {
54
+ PORT = process.env.KB_TEAM_KNOWLEDGE_PORT ? Number(process.env.KB_TEAM_KNOWLEDGE_PORT) : await freePort(); BASE_URL = `http://127.0.0.1:${PORT}`;
55
+ let spawned;
56
+ try {
57
+ initializeRepo(SOURCE_ONE, 'Source one');
58
+ initializeRepo(SOURCE_TWO, 'Source two');
59
+ initializeRepo(SOURCE_BAD, 'Source bad');
60
+ initializeRepo(TEAM_STORE, 'Team store');
61
+ fs.mkdirSync(path.join(TEAM_STORE, 'acc'), { recursive: true });
62
+ fs.writeFileSync(path.join(TEAM_STORE, 'acc', 'README.md'), '# ACC shared knowledge\n', 'utf8');
63
+ git(TEAM_STORE, ['add', '.']);
64
+ git(TEAM_STORE, ['commit', '-m', 'add team knowledge']);
65
+ git(TEAM_STORE, ['remote', 'add', 'origin', TEAM_STORE]);
66
+
67
+ initializeRepo(TEAM_REMOTE, 'Remote team store');
68
+ fs.mkdirSync(path.join(TEAM_REMOTE, 'acc'), { recursive: true });
69
+ fs.writeFileSync(path.join(TEAM_REMOTE, 'acc', 'README.md'), '# ACC from remote\n', 'utf8');
70
+ git(TEAM_REMOTE, ['add', '.']);
71
+ git(TEAM_REMOTE, ['commit', '-m', 'add remote knowledge']);
72
+
73
+ const providerConfigPath = path.join(TEMP, 'providers.json');
74
+ fs.writeFileSync(providerConfigPath, `\uFEFF${JSON.stringify({ gitea: { webBaseUrl: 'https://gitea.example.test', oauthClientId: 'client', oauthClientSecret: 'secret' } })}`, 'utf8');
75
+ const providerFile = githubTeamStore.readProviderFileConfig(providerConfigPath);
76
+ const publicProviders = githubTeamStore.providerPublicConfig(githubTeamStore.defaultConfig(), {}, providerFile);
77
+ assert.strictEqual(publicProviders.gitea.configured, true);
78
+ assert.strictEqual(publicProviders.gitea.oauthClientSecret, undefined);
79
+ assert.strictEqual(publicProviders.gitea.oauthClientSecretConfigured, true);
80
+ assert.strictEqual(githubTeamStore.proxyUrlForTarget('http://127.0.0.1:3000/api/v1/user', { KB_GIT_PROXY: 'http://127.0.0.1:7890' }), '');
81
+
82
+ const scanned = await githubTeamStore.scanLocalStore({ localPath: TEAM_STORE });
83
+ assert(scanned.ok && scanned.store.knowledgeBases.some(item => item.path === 'acc'), JSON.stringify(scanned));
84
+ const checkedOut = await githubTeamStore.checkoutStore({ cloneUrl: TEAM_REMOTE, branch: 'main', localPath: TEAM_CLONE });
85
+ assert(checkedOut.ok, JSON.stringify(checkedOut));
86
+ assert.match(fs.readFileSync(path.join(TEAM_CLONE, 'acc', 'README.md'), 'utf8'), /from remote/);
87
+
88
+ spawned = spawnServer({ root: ROOT, port: PORT, dataDir: DATA_DIR, tag: 'team-kb-v2' });
89
+ let output = '';
90
+ spawned.child.stdout.on('data', chunk => { output += chunk; });
91
+ spawned.child.stderr.on('data', chunk => { output += chunk; });
92
+ await waitForServer();
93
+ let result = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(TEMP, 'personal-knowledge') } });
94
+ assert(result.response.ok, JSON.stringify(result.body));
95
+
96
+ result = await json('POST', '/api/projects/import', {
97
+ projectId: 'team-project-one', localPath: SOURCE_ONE,
98
+ teamKnowledgeBase: {
99
+ storeLocalPath: TEAM_STORE, storeRemoteUrl: 'https://github.com/org/knowledge.git',
100
+ storeFullName: 'org/knowledge', storeId: 'team-store', kbId: 'kb-acc', kbSlug: 'acc', kbSubdir: 'acc', displayName: 'ACC',
101
+ },
102
+ });
103
+ assert.strictEqual(result.response.status, 201, `${JSON.stringify(result.body)}\n${output}`);
104
+ assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_STORE, 'acc'));
105
+ assert.strictEqual(result.body.config.teamBinding.provider, 'github');
106
+ assert.strictEqual(result.body.config.teamBinding.kbId, 'kb-acc');
107
+ assert.strictEqual(result.body.config.teamBinding.kbSubdir, 'acc');
108
+ const registryText = fs.readFileSync(path.join(DATA_DIR, 'projects.json'), 'utf8');
109
+ assert(!registryText.includes(TEAM_STORE), 'minimal v2 registry must not contain the external team path');
110
+
111
+ result = await json('POST', '/api/projects/import', {
112
+ projectId: 'team-project-bad', localPath: SOURCE_BAD,
113
+ teamKnowledgeBase: { storeLocalPath: TEAM_STORE, kbSubdir: '../escape' },
114
+ });
115
+ assert.strictEqual(result.response.status, 403, 'team binding traversal must be rejected');
116
+
117
+ result = await json('POST', '/api/projects/import', {
118
+ projectId: 'team-project-two', localPath: SOURCE_TWO,
119
+ teamKnowledgeBase: { storeLocalPath: TEAM_CLONE, storeRemoteUrl: TEAM_REMOTE, storeId: 'clone', kbId: 'kb-acc', kbSubdir: 'acc' },
120
+ });
121
+ assert.strictEqual(result.response.status, 201, JSON.stringify(result.body));
122
+ assert.strictEqual(path.resolve(result.body.config.knowledgePath), path.resolve(TEAM_CLONE, 'acc'));
123
+
124
+ result = await json('DELETE', '/api/projects/team-project-one', { deleteKnowledge: true, confirmationToken: 'team-project-one' });
125
+ assert(result.response.ok, JSON.stringify(result.body));
126
+ assert.strictEqual(result.body.removedKnowledge, false, 'team knowledge is externally owned');
127
+ assert(fs.existsSync(path.join(TEAM_STORE, 'acc', 'README.md')), 'team knowledge must survive project deletion');
128
+ console.log('team knowledge store test passed');
129
+ } finally {
130
+ if (spawned) spawned.child.kill();
131
+ }
132
+ })().catch(error => { console.error(error); process.exitCode = 1; });
@@ -1,35 +1,35 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { spawnServer } = require('./helpers/spawn-server');
6
- const freePort = require('./helpers/free-port');
7
- const { readEndpoint } = require('../lib/runtime-endpoint');
8
- const { spawn } = require('node:child_process');
9
-
10
- (async () => {
11
- const port = await freePort();
12
- const spawned = spawnServer({ root: path.resolve(__dirname, '../..'), port, tag: 'graceful-cleanup' });
13
- let stopped = false;
14
- try {
15
- for (let attempt = 0; attempt < 100; attempt++) {
16
- const endpoint = readEndpoint(spawned.dataDir);
17
- if (endpoint && (await fetch(`http://127.0.0.1:${port}/api/health`).catch(() => null))?.ok) break;
18
- await new Promise(resolve => setTimeout(resolve, 100));
19
- }
20
- assert.equal(readEndpoint(spawned.dataDir)?.pid, spawned.child.pid);
21
- let output = '';
22
- spawned.child.stderr.on('data', bytes => { output += bytes; });
23
- const locker = process.platform === 'win32' ? spawn(process.execPath, ['-e', 'process.stdout.write("holding-cwd\\n");setTimeout(()=>process.exit(0),2500)'], { cwd: spawned.dataDir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) : null;
24
- if (locker) await new Promise((resolve, reject) => { locker.once('error', reject); locker.stdout.once('data', resolve); });
25
- try { await spawned.cleanup(); stopped = true; }
26
- finally { if (locker?.exitCode === null) { locker.kill(); await new Promise(resolve => locker.once('close', resolve)); } }
27
- assert.notEqual(spawned.child.exitCode, null, output);
28
- assert.equal(spawned.child.signalCode, null, 'normal cleanup must drain the server instead of force killing it');
29
- assert.equal(spawned.child.exitCode, 0, output);
30
- assert.ok(!fs.existsSync(spawned.dataDir));
31
- assert.ok(!fs.existsSync(spawned.bridgeHome));
32
- await assert.rejects(fetch(`http://127.0.0.1:${port}/api/health`));
33
- console.log('test-server-cleanup-test PASS (verified graceful shutdown before fixture removal)');
34
- } finally { if (!stopped) await spawned.cleanup(); }
35
- })().catch(error => { console.error(error); process.exitCode = 1; });
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnServer } = require('./helpers/spawn-server');
6
+ const freePort = require('./helpers/free-port');
7
+ const { readEndpoint } = require('../lib/runtime-endpoint');
8
+ const { spawn } = require('node:child_process');
9
+
10
+ (async () => {
11
+ const port = await freePort();
12
+ const spawned = spawnServer({ root: path.resolve(__dirname, '../..'), port, tag: 'graceful-cleanup' });
13
+ let stopped = false;
14
+ try {
15
+ for (let attempt = 0; attempt < 100; attempt++) {
16
+ const endpoint = readEndpoint(spawned.dataDir);
17
+ if (endpoint && (await fetch(`http://127.0.0.1:${port}/api/health`).catch(() => null))?.ok) break;
18
+ await new Promise(resolve => setTimeout(resolve, 100));
19
+ }
20
+ assert.equal(readEndpoint(spawned.dataDir)?.pid, spawned.child.pid);
21
+ let output = '';
22
+ spawned.child.stderr.on('data', bytes => { output += bytes; });
23
+ const locker = process.platform === 'win32' ? spawn(process.execPath, ['-e', 'process.stdout.write("holding-cwd\\n");setTimeout(()=>process.exit(0),2500)'], { cwd: spawned.dataDir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) : null;
24
+ if (locker) await new Promise((resolve, reject) => { locker.once('error', reject); locker.stdout.once('data', resolve); });
25
+ try { await spawned.cleanup(); stopped = true; }
26
+ finally { if (locker?.exitCode === null) { locker.kill(); await new Promise(resolve => locker.once('close', resolve)); } }
27
+ assert.notEqual(spawned.child.exitCode, null, output);
28
+ assert.equal(spawned.child.signalCode, null, 'normal cleanup must drain the server instead of force killing it');
29
+ assert.equal(spawned.child.exitCode, 0, output);
30
+ assert.ok(!fs.existsSync(spawned.dataDir));
31
+ assert.ok(!fs.existsSync(spawned.bridgeHome));
32
+ await assert.rejects(fetch(`http://127.0.0.1:${port}/api/health`));
33
+ console.log('test-server-cleanup-test PASS (verified graceful shutdown before fixture removal)');
34
+ } finally { if (!stopped) await spawned.cleanup(); }
35
+ })().catch(error => { console.error(error); process.exitCode = 1; });
@@ -1,37 +1,37 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const vm = require('node:vm');
6
-
7
- const helper = path.join(__dirname, 'helpers/spawn-server.js');
8
- const calls = [];
9
- const moduleObject = { exports: {} };
10
- const inherited = {
11
- KB_TERMINAL_URL: 'http://127.0.0.1:5760',
12
- KB_VECTORHUB_URL: 'http://127.0.0.1:8787',
13
- KB_EVENTBRIDGE_URL: 'http://127.0.0.1:8790',
14
- KB_KNOWLEDGE_ENGINE_URL: 'http://127.0.0.1:5790',
15
- };
16
- vm.runInNewContext(fs.readFileSync(helper, 'utf8'), {
17
- module: moduleObject,
18
- require(id) {
19
- if (id === 'child_process') return { spawn: (...args) => { calls.push(args); return { pid: 1 }; } };
20
- if (id === 'fs') return { mkdtempSync: prefix => prefix + 'owned' };
21
- if (id === '../../lib/runtime-control' || id === '../../lib/runtime-endpoint') return {};
22
- return require(id);
23
- },
24
- process: { execPath: process.execPath, pid: process.pid, env: inherited },
25
- });
26
- const spawnServer = moduleObject.exports.spawnServer;
27
- spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data' });
28
- const defaults = calls[0][2].env;
29
- for (const key of Object.keys(inherited)) assert.equal(defaults[key], 'http://127.0.0.1:0', key + ' must not consult a live user module');
30
- assert.equal(defaults.KB_DATA_DIR, 'isolated-data');
31
- assert.equal(defaults.KB_MODULES_AUTOSTART, '0');
32
- assert.equal(defaults.KB_MODULES_REGISTER, '0');
33
- assert.notEqual(defaults.AI_CODING_EVENT_BRIDGE_HOME, process.env.AI_CODING_EVENT_BRIDGE_HOME);
34
- const ownUrls = Object.fromEntries(Object.keys(inherited).map(key => [key, 'http://127.0.0.1:12345']));
35
- spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data', extraEnv: ownUrls });
36
- for (const key of Object.keys(inherited)) assert.equal(calls[1][2].env[key], ownUrls[key], 'explicit owned fixture remains supported');
37
- console.log('test-server-module-isolation-test PASS');
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const vm = require('node:vm');
6
+
7
+ const helper = path.join(__dirname, 'helpers/spawn-server.js');
8
+ const calls = [];
9
+ const moduleObject = { exports: {} };
10
+ const inherited = {
11
+ KB_TERMINAL_URL: 'http://127.0.0.1:5760',
12
+ KB_VECTORHUB_URL: 'http://127.0.0.1:8787',
13
+ KB_EVENTBRIDGE_URL: 'http://127.0.0.1:8790',
14
+ KB_KNOWLEDGE_ENGINE_URL: 'http://127.0.0.1:5790',
15
+ };
16
+ vm.runInNewContext(fs.readFileSync(helper, 'utf8'), {
17
+ module: moduleObject,
18
+ require(id) {
19
+ if (id === 'child_process') return { spawn: (...args) => { calls.push(args); return { pid: 1 }; } };
20
+ if (id === 'fs') return { mkdtempSync: prefix => prefix + 'owned' };
21
+ if (id === '../../lib/runtime-control' || id === '../../lib/runtime-endpoint') return {};
22
+ return require(id);
23
+ },
24
+ process: { execPath: process.execPath, pid: process.pid, env: inherited },
25
+ });
26
+ const spawnServer = moduleObject.exports.spawnServer;
27
+ spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data' });
28
+ const defaults = calls[0][2].env;
29
+ for (const key of Object.keys(inherited)) assert.equal(defaults[key], 'http://127.0.0.1:0', key + ' must not consult a live user module');
30
+ assert.equal(defaults.KB_DATA_DIR, 'isolated-data');
31
+ assert.equal(defaults.KB_MODULES_AUTOSTART, '0');
32
+ assert.equal(defaults.KB_MODULES_REGISTER, '0');
33
+ assert.notEqual(defaults.AI_CODING_EVENT_BRIDGE_HOME, process.env.AI_CODING_EVENT_BRIDGE_HOME);
34
+ const ownUrls = Object.fromEntries(Object.keys(inherited).map(key => [key, 'http://127.0.0.1:12345']));
35
+ spawnServer({ root: path.resolve(__dirname, '../..'), port: 0, dataDir: 'isolated-data', extraEnv: ownUrls });
36
+ for (const key of Object.keys(inherited)) assert.equal(calls[1][2].env[key], ownUrls[key], 'explicit owned fixture remains supported');
37
+ console.log('test-server-module-isolation-test PASS');
@@ -93,47 +93,47 @@ function commit(target, name) {
93
93
  await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: nextCommit, branch: 'main' });
94
94
  assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, nextCommit);
95
95
  assert.strictEqual(processorCalls, 1);
96
- await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: laterCommit, branch: 'main' });
97
- assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, laterCommit, 'an event SHA remains valid even after HEAD advances');
98
- assert.strictEqual(processorCalls, 2);
99
-
100
- const terminalFailedCommit = 'a'.repeat(40);
101
- await projects.updateState('project-existing', state => {
102
- state.analysis.activeClaim = {
103
- schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: terminalFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
104
- promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
105
- runId: 'failed-run', operationId: 'failed-operation', phase: 'failed', attempt: 1, fingerprint: 'failed-fingerprint', error: { code: 'EPERM', retryable: false },
106
- };
107
- state.analysis.status = 'failed';
108
- });
109
- const explicitAfterFailure = commit(existingRepo, 'after-terminal-failure');
110
- await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: explicitAfterFailure, branch: 'main' });
111
- assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, explicitAfterFailure, 'only the new explicit Hook SHA may proceed after a terminal failure');
112
- assert.strictEqual(processorCalls, 3, 'terminal failed claims must not retry their old SHA');
113
-
114
- const retryableFailedCommit = 'b'.repeat(40);
115
- await projects.updateState('project-existing', state => {
116
- state.analysis.activeClaim = {
117
- schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: retryableFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
118
- promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
119
- runId: 'retryable-run', operationId: 'retryable-operation', phase: 'failed', attempt: 1, fingerprint: 'retryable-fingerprint', error: { code: 'EAGAIN', retryable: true },
120
- };
121
- state.analysis.status = 'failed';
122
- });
123
- const blockedByRetryable = commit(existingRepo, 'after-retryable-failure');
124
- const blockedResult = await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: blockedByRetryable, branch: 'main' });
125
- assert.strictEqual(blockedResult.status, 'failed');
126
- assert.strictEqual(processorCalls, 3, 'retryable claims must remain blocked instead of being silently superseded');
127
- assert.strictEqual(projects.readState('project-existing').analysis.activeClaim.commitSha, retryableFailedCommit);
96
+ await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: laterCommit, branch: 'main' });
97
+ assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, laterCommit, 'an event SHA remains valid even after HEAD advances');
98
+ assert.strictEqual(processorCalls, 2);
99
+
100
+ const terminalFailedCommit = 'a'.repeat(40);
101
+ await projects.updateState('project-existing', state => {
102
+ state.analysis.activeClaim = {
103
+ schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: terminalFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
104
+ promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
105
+ runId: 'failed-run', operationId: 'failed-operation', phase: 'failed', attempt: 1, fingerprint: 'failed-fingerprint', error: { code: 'EPERM', retryable: false },
106
+ };
107
+ state.analysis.status = 'failed';
108
+ });
109
+ const explicitAfterFailure = commit(existingRepo, 'after-terminal-failure');
110
+ await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: explicitAfterFailure, branch: 'main' });
111
+ assert.strictEqual(projects.readState('project-existing').lastAnalyzedCommit, explicitAfterFailure, 'only the new explicit Hook SHA may proceed after a terminal failure');
112
+ assert.strictEqual(processorCalls, 3, 'terminal failed claims must not retry their old SHA');
113
+
114
+ const retryableFailedCommit = 'b'.repeat(40);
115
+ await projects.updateState('project-existing', state => {
116
+ state.analysis.activeClaim = {
117
+ schema: 'commit-claim/v1', projectId: 'project-existing', commitSha: retryableFailedCommit, parents: [], requirementIds: [], requirementBinding: 'unavailable',
118
+ promptTemplateVersion: 'v1', promptHash: 'prompt', patchHash: 'patch', evidenceHash: 'evidence', evidenceManifestHash: 'manifest', retrievalManifestHash: 'retrieval', knowledgePath: knowledgeRoot,
119
+ runId: 'retryable-run', operationId: 'retryable-operation', phase: 'failed', attempt: 1, fingerprint: 'retryable-fingerprint', error: { code: 'EAGAIN', retryable: true },
120
+ };
121
+ state.analysis.status = 'failed';
122
+ });
123
+ const blockedByRetryable = commit(existingRepo, 'after-retryable-failure');
124
+ const blockedResult = await reconciler.processCommitEvent({ projectId: 'project-existing', commitSha: blockedByRetryable, branch: 'main' });
125
+ assert.strictEqual(blockedResult.status, 'failed');
126
+ assert.strictEqual(processorCalls, 3, 'retryable claims must remain blocked instead of being silently superseded');
127
+ assert.strictEqual(projects.readState('project-existing').analysis.activeClaim.commitSha, retryableFailedCommit);
128
128
 
129
129
  const emptyRepo = repo('empty-repo', false);
130
130
  const emptyImported = await lifecycle.importProject({ localPath: emptyRepo, projectId: 'project-empty', aiProfileId: 'test-profile' });
131
131
  assert.strictEqual(emptyImported.state.trackingMode, 'empty-repo');
132
132
  assert.strictEqual(emptyImported.state.trackingStartCommit, null);
133
133
  const firstCommit = commit(emptyRepo, 'first');
134
- await reconciler.processCommitEvent({ projectId: 'project-empty', commitSha: firstCommit, branch: 'main' });
135
- assert.strictEqual(projects.readState('project-empty').lastAnalyzedCommit, firstCommit, 'first commit after empty import must be analyzed');
136
- assert.strictEqual(processorCalls, 4);
134
+ await reconciler.processCommitEvent({ projectId: 'project-empty', commitSha: firstCommit, branch: 'main' });
135
+ assert.strictEqual(projects.readState('project-empty').lastAnalyzedCommit, firstCommit, 'first commit after empty import must be analyzed');
136
+ assert.strictEqual(processorCalls, 4);
137
137
 
138
138
  console.log('tracking-start-test PASS');
139
139
  })().catch(error => {
@@ -1,22 +1,22 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const vm = require('node:vm');
6
- const source = fs.readFileSync(path.join(__dirname, '../../ui/product.js'), 'utf8');
7
- const code = source.slice(source.indexOf(' function frameContext('), source.indexOf(' function setView('));
8
- const messages = [], frame = { ready: false }, state = { epoch: 1, scope: 'project' };
9
- const project = { projectId: 'a', workspacePath: '/workspace', modules: { terminal: { projectId: 'context-a' } } };
10
- const ctx = vm.createContext({ state, frames: { terminal: frame }, activeProject: () => project,
11
- document: { documentElement: { dataset: { theme: 'light' } } }, frameVisible: () => true,
12
- post: (_key, message) => messages.push(message) });
13
- vm.runInContext(code, ctx);
14
- vm.runInContext('frameContext("terminal")', ctx);
15
- assert.equal(messages.length, 0);
16
- frame.ready = true;
17
- vm.runInContext('frameContext("terminal"); frameContext("terminal")', ctx);
18
- assert.equal(messages.filter(m => m.type === 'kb:select-project').length, 1, 'identical context receipts are not repeated');
19
- state.epoch++;
20
- vm.runInContext('frameContext("terminal")', ctx);
21
- assert.equal(messages.filter(m => m.type === 'kb:select-project').length, 2, 'a real selection still reaches the iframe');
22
- console.log('workbench-context-receipts-test PASS');
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const vm = require('node:vm');
6
+ const source = fs.readFileSync(path.join(__dirname, '../../ui/product.js'), 'utf8');
7
+ const code = source.slice(source.indexOf(' function frameContext('), source.indexOf(' function setView('));
8
+ const messages = [], frame = { ready: false }, state = { epoch: 1, scope: 'project' };
9
+ const project = { projectId: 'a', workspacePath: '/workspace', modules: { terminal: { projectId: 'context-a' } } };
10
+ const ctx = vm.createContext({ state, frames: { terminal: frame }, activeProject: () => project,
11
+ document: { documentElement: { dataset: { theme: 'light' } } }, frameVisible: () => true,
12
+ post: (_key, message) => messages.push(message) });
13
+ vm.runInContext(code, ctx);
14
+ vm.runInContext('frameContext("terminal")', ctx);
15
+ assert.equal(messages.length, 0);
16
+ frame.ready = true;
17
+ vm.runInContext('frameContext("terminal"); frameContext("terminal")', ctx);
18
+ assert.equal(messages.filter(m => m.type === 'kb:select-project').length, 1, 'identical context receipts are not repeated');
19
+ state.epoch++;
20
+ vm.runInContext('frameContext("terminal")', ctx);
21
+ assert.equal(messages.filter(m => m.type === 'kb:select-project').length, 2, 'a real selection still reaches the iframe');
22
+ console.log('workbench-context-receipts-test PASS');
@@ -1,31 +1,31 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const root = path.resolve(__dirname, '../..');
6
- const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
7
- const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
8
- const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
9
- for (const view of ['home', 'work', 'search', 'memory', 'projects', 'activity']) {
10
- assert.equal((html.match(new RegExp(`data-view="${view}"`, 'g')) || []).length, 1);
11
- assert.ok(html.includes(`id="view-${view}"`));
12
- }
13
- assert.equal((html.match(/data-view=/g) || []).length, 6, 'diagnostics must not become another primary product');
14
- assert.match(html, /<select\b[^>]*id="projectSelect"[^>]*\bdisabled\b/, 'project selection is disabled until the initial registry snapshot has loaded');
15
- for (const id of ['workFrame', 'vectorFrame', 'bridgeFrame', 'engineFrame', 'importDialog', 'importCode', 'importKnowledge', 'importDomain', 'setupImportRoot', 'createDomain', 'logsDialog', 'systemDrawer', 'copyReference']) assert.equal((html.match(new RegExp(`id="${id}"`, 'g')) || []).length, 1);
16
- assert.ok(html.includes('data-action="import"') && html.includes('不分析全部历史'));
17
- assert.ok(!html.includes('srcdoc') && !/data-simulate|data-analyze|data-install-hook/.test(html));
18
- assert.equal((script.match(/\.src = /g) || []).length, 1, 'frames initialize once, not on project/theme/page changes');
19
- for (const contract of ['kb:theme', 'kb:select-project', 'kb:scope', 'kb:knowledge', 'kb:ready']) assert.ok(script.includes(contract));
20
- assert.ok(script.includes('event.origin !== location.origin') && script.includes('contentWindow === event.source'), 'module messages must be from the expected frame and origin');
21
- assert.ok(script.includes('state.epoch++') && script.includes('referenceEpoch') && script.includes('logEpoch'), 'late project/reference/log feedback must be isolated');
22
- assert.match(script, /on\('domainForm'[\s\S]*?await refreshRegistry\(true\)/, 'domain writes must invalidate pre-mutation reads');
23
- assert.match(script, /registered = true;[\s\S]*?await refreshRegistry\(true\)/, 'import must not reuse a pre-import snapshot');
24
- assert.ok(script.includes('esc(r.title)') && script.includes('esc(m.text)') && script.includes('esc(project.name)'));
25
- assert.ok(script.includes('text.textContent = record.message') && script.includes("'logs/appended'"), 'diagnostics stay inert and stream live');
26
- assert.ok(script.includes('URL.revokeObjectURL(url)') && script.includes('logStream?.close()'));
27
- assert.ok(script.includes('resumeImport = true') && script.includes("$('importDialog').showModal()") && script.includes("p.code === 'KNOWLEDGE_ROOT_MISSING'"), 'first import can return from setup without resetting its input');
28
- assert.ok(css.includes('height:100%') && css.includes('.view.module-view{padding:0;overflow:hidden}') && css.includes('overflow-wrap:anywhere'));
29
- assert.ok(css.includes('html[data-theme=dark]') && css.includes('prefers-reduced-motion'));
30
- assert.doesNotThrow(() => new Function(script));
31
- console.log('workspace UI contract test PASS (source contract; not browser acceptance)');
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const root = path.resolve(__dirname, '../..');
6
+ const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
7
+ const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
8
+ const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
9
+ for (const view of ['home', 'work', 'search', 'memory', 'projects', 'activity']) {
10
+ assert.equal((html.match(new RegExp(`data-view="${view}"`, 'g')) || []).length, 1);
11
+ assert.ok(html.includes(`id="view-${view}"`));
12
+ }
13
+ assert.equal((html.match(/data-view=/g) || []).length, 6, 'diagnostics must not become another primary product');
14
+ assert.match(html, /<select\b[^>]*id="projectSelect"[^>]*\bdisabled\b/, 'project selection is disabled until the initial registry snapshot has loaded');
15
+ for (const id of ['workFrame', 'vectorFrame', 'bridgeFrame', 'engineFrame', 'importDialog', 'importCode', 'importKnowledge', 'importDomain', 'setupImportRoot', 'createDomain', 'logsDialog', 'systemDrawer', 'copyReference']) assert.equal((html.match(new RegExp(`id="${id}"`, 'g')) || []).length, 1);
16
+ assert.ok(html.includes('data-action="import"') && html.includes('不分析全部历史'));
17
+ assert.ok(!html.includes('srcdoc') && !/data-simulate|data-analyze|data-install-hook/.test(html));
18
+ assert.equal((script.match(/\.src = /g) || []).length, 1, 'frames initialize once, not on project/theme/page changes');
19
+ for (const contract of ['kb:theme', 'kb:select-project', 'kb:scope', 'kb:knowledge', 'kb:ready']) assert.ok(script.includes(contract));
20
+ assert.ok(script.includes('event.origin !== location.origin') && script.includes('contentWindow === event.source'), 'module messages must be from the expected frame and origin');
21
+ assert.ok(script.includes('state.epoch++') && script.includes('referenceEpoch') && script.includes('logEpoch'), 'late project/reference/log feedback must be isolated');
22
+ assert.match(script, /on\('domainForm'[\s\S]*?await refreshRegistry\(true\)/, 'domain writes must invalidate pre-mutation reads');
23
+ assert.match(script, /registered = true;[\s\S]*?await refreshRegistry\(true\)/, 'import must not reuse a pre-import snapshot');
24
+ assert.ok(script.includes('esc(r.title)') && script.includes('esc(m.text)') && script.includes('esc(project.name)'));
25
+ assert.ok(script.includes('text.textContent = record.message') && script.includes("'logs/appended'"), 'diagnostics stay inert and stream live');
26
+ assert.ok(script.includes('URL.revokeObjectURL(url)') && script.includes('logStream?.close()'));
27
+ assert.ok(script.includes('resumeImport = true') && script.includes("$('importDialog').showModal()") && script.includes("p.code === 'KNOWLEDGE_ROOT_MISSING'"), 'first import can return from setup without resetting its input');
28
+ assert.ok(css.includes('height:100%') && css.includes('.view.module-view{padding:0;overflow:hidden}') && css.includes('overflow-wrap:anywhere'));
29
+ assert.ok(css.includes('html[data-theme=dark]') && css.includes('prefers-reduced-motion'));
30
+ assert.doesNotThrow(() => new Function(script));
31
+ console.log('workspace UI contract test PASS (source contract; not browser acceptance)');