@sanqianx/project-knowledge 5.0.19 → 5.0.20

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
@@ -114,6 +114,10 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
114
114
 
115
115
  ## 验证与尚未验收项
116
116
 
117
+ 默认维护模型额度或余额不足时,不会成功生成知识;提交任务仍先持久化。SDK 明确报告 billing 错误时记录 `MODEL_QUOTA_EXHAUSTED`,请求限流单独记录 `MODEL_RATE_LIMITED`,不把任意 HTTP 429 当作余额耗尽。同项目后续提交保持排队,失败与排队记录跨重启保留;恢复额度或更换默认维护模型后需显式重试前一条失败提交,不会自动消耗额度反复重试。
118
+
119
+ `/api/snapshot` 的兼容扩展字段 `maintenance` 包含执行启用状态、事件消费启用状态及按当前项目过滤的 journal 故障。建单前的消费故障不会伪造提交运行;外壳展示“提交事件消费受阻”,不能将零条运行当作没有提交。故障变化和恢复通过现有 progress 流通知,稳定故障不产生重复刷新事件。未启动服务时,Hook 已捕获的事件保留于 journal,服务消费后才生成分析任务。
120
+
117
121
  `npm test` 覆盖真实 Git/Hook、真实分项目 journal、真实 vector-hub 文件索引、晋升、重复抑制、FIFO/并发、记忆、旧格式拒绝、新版恢复及进程隔离。自动测试中的模型和 embedding 是可控替身。
118
122
 
119
123
  `npm run test:web` 使用兄弟源码仓库启动隔离的五服务网页环境,只有模型与 embedding 是替身。单一 1920×1080 视口验证双目录 UI 导入、真实 Hook 到自动刷新、原生消息流与中止、快速项目切换/草稿/历史、迟到检索及文件响应、领域检索、记忆落盘、四 iframe 身份与主题、唯一进度连接、诊断关闭和保留数据解绑;不会在用户代码仓库提交。该脚本是开发验收工具,不属于引擎生产运行依赖。
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const failures = {
4
+ MODEL_QUOTA_EXHAUSTED: '默认维护模型的额度或余额不足。提交证据与失败记录已保留;恢复额度或更换默认模型后,请重试此提交。',
5
+ MODEL_RATE_LIMITED: '默认维护模型请求受限。提交证据与失败记录已保留,请稍后重试。',
6
+ MODEL_AUTH_FAILED: '默认维护模型认证失败。提交证据与失败记录已保留,请检查凭据后重试。',
7
+ AI_RUNTIME_REQUIRED: '后台分析运行时不可用,请修复安装后重试。',
8
+ ANALYSIS_OUTPUT_MISSING: '后台分析未生成有效文件,提交证据与失败记录已保留,请重试。',
9
+ MODEL_FAILED: '后台 Agent 分析失败。提交证据与失败记录已保留,请检查默认维护模型后重试。',
10
+ };
11
+
12
+ // Provider text can echo credentials. Classify structured codes only; neither
13
+ // raw messages nor unknown codes may cross the worker IPC boundary.
14
+ function classifyAnalysisFailure(input) {
15
+ const codes = new Set(), statuses = new Set(), seen = new Set();
16
+ function collect(value, depth = 0) {
17
+ if (typeof value === 'string') { codes.add(value.toLowerCase()); return; }
18
+ if (!value || typeof value !== 'object' || depth > 3 || seen.has(value)) return;
19
+ seen.add(value);
20
+ for (const key of ['code', 'type']) if (typeof value[key] === 'string') codes.add(value[key].toLowerCase());
21
+ if (value.status !== undefined) statuses.add(Number(value.status));
22
+ collect(value.error, depth + 1); collect(value.cause, depth + 1);
23
+ }
24
+ collect(input);
25
+ const has = (...values) => values.some(value => codes.has(value));
26
+ if (has('billing_error', 'insufficient_quota', 'quota_exceeded', 'insufficient_balance', 'model_quota_exhausted')) return 'MODEL_QUOTA_EXHAUSTED';
27
+ if (has('authentication_failed', 'authentication_error', 'model_auth_failed') || statuses.has(401)) return 'MODEL_AUTH_FAILED';
28
+ if (has('rate_limit', 'rate_limit_error', 'model_rate_limited') || statuses.has(429)) return 'MODEL_RATE_LIMITED';
29
+ if (has('module_not_found', 'ai_runtime_required')) return 'AI_RUNTIME_REQUIRED';
30
+ if (has('analysis_output_missing')) return 'ANALYSIS_OUTPUT_MISSING';
31
+ return 'MODEL_FAILED';
32
+ }
33
+
34
+ function analysisFailure(code) {
35
+ code = Object.hasOwn(failures, code) ? code : 'MODEL_FAILED';
36
+ return Object.assign(new Error(failures[code]), { code, retryable: true });
37
+ }
38
+
39
+ module.exports = { classifyAnalysisFailure, analysisFailure };
@@ -4,6 +4,7 @@ const { pathToFileURL } = require('node:url');
4
4
  const { evaluateAutomationToolUse } = require('./runtime/automation-config');
5
5
  const { RuntimeLayout } = require('./runtime/layout');
6
6
  const { ANALYSIS_SYSTEM_INSTRUCTIONS, requireAnalysisOutput } = require('./analysis-contract');
7
+ const { classifyAnalysisFailure, analysisFailure } = require('./analysis-failures');
7
8
 
8
9
  const abortController = new AbortController();
9
10
  process.once('SIGTERM', () => abortController.abort());
@@ -11,6 +12,7 @@ process.once('disconnect', () => abortController.abort());
11
12
  process.on('message', message => { if (message?.type === 'cancel') abortController.abort(); });
12
13
 
13
14
  process.once('message', async job => {
15
+ let failureCode = 'MODEL_FAILED';
14
16
  try {
15
17
  const { query } = await import(pathToFileURL(require.resolve('@anthropic-ai/claude-agent-sdk')).href);
16
18
  const layout = new RuntimeLayout(job.stagingPath);
@@ -40,15 +42,17 @@ process.once('message', async job => {
40
42
  }] }] },
41
43
  } });
42
44
  for await (const message of turn) {
45
+ if (message.type === 'assistant' && message.error) failureCode = classifyAnalysisFailure(message.error);
43
46
  if (message.type === 'result') completed = message.subtype === 'success' && !message.is_error;
44
47
  }
45
- if (!completed) throw new Error('MODEL_FAILED');
48
+ if (!completed) throw analysisFailure(failureCode);
46
49
  requireAnalysisOutput(job.manifestPath);
47
50
  process.send({ type: 'complete' }, () => process.exit(0));
48
51
  } catch (error) {
49
52
  // SDK/provider messages can echo credentials. Only controlled codes leave
50
53
  // this isolated process; raw output is never written to state or logs.
51
- const code = error.code === 'MODULE_NOT_FOUND' ? 'AI_RUNTIME_REQUIRED' : error.code === 'ANALYSIS_OUTPUT_MISSING' ? error.code : 'MODEL_FAILED';
54
+ const classified = classifyAnalysisFailure(error);
55
+ const code = classified === 'MODEL_FAILED' ? failureCode : classified;
52
56
  process.send({ type: 'failure', code }, () => process.exit(1));
53
57
  }
54
58
  });
@@ -137,6 +137,7 @@ class EngineCoordinator extends EventEmitter {
137
137
  return job;
138
138
  }
139
139
  async drainJournals() {
140
+ const prior = JSON.stringify([...this.journalFailures.values()]);
140
141
  this.draining = true;
141
142
  try {
142
143
  await this.bridge.registerConsumer('knowledge-engine', { journals: ['*'] });
@@ -157,8 +158,14 @@ class EngineCoordinator extends EventEmitter {
157
158
  finally {
158
159
  this.journalError = this.journalFailures.size ? '事件消费异常,未消费证据与游标已保留,请查看诊断日志。' : null;
159
160
  this.draining = false;
161
+ if (JSON.stringify([...this.journalFailures.values()]) !== prior) this.emit('progress', this.store.addEvent({ type: 'knowledge.journal', summary: this.journalError ? '提交事件消费受阻,请查看维护状态' : '提交事件消费已恢复' }));
160
162
  }
161
163
  }
164
+ maintenanceStatus(projectId) {
165
+ const project = projectId ? this.store.getProject(projectId) : null;
166
+ const failures = [...this.journalFailures.values()].filter(failure => !projectId || failure.journalId === 'connection' || (failure.journalId === 'global' && !failure.projectId) || failure.projectId === projectId || failure.journalId === project?.journalId);
167
+ return { projectId: projectId || null, executionEnabled: this.executionEnabled && (!projectId || !!project && (!this.allowedProjects || this.allowedProjects.has(projectId))), consuming: this.consume, journal: { error: failures.length ? this.journalError : null, failures } };
168
+ }
162
169
  async recordJournalFailure(journalId, error) {
163
170
  const context = error.journalContext || {};
164
171
  const failure = { journalId, sequence: context.sequence ?? null, projectId: context.projectId || null, code: error.code || 'JOURNAL_FAILED', message: this.safeMessage(error.message) };
@@ -4,6 +4,7 @@ const path = require('node:path');
4
4
  const { frameFrozenEvidence, memoryCandidateContract } = require('./analysis-contract');
5
5
  const { finalizeAnalysisOutput } = require('./analysis-output');
6
6
  const { freezeKnowledgeTargets } = require('./knowledge-targets');
7
+ const { analysisFailure } = require('./analysis-failures');
7
8
 
8
9
  class HeadlessAnalyzer {
9
10
  get sourceReadMode() { return 'isolated-frozen-evidence'; }
@@ -42,7 +43,7 @@ class HeadlessAnalyzer {
42
43
  const timer = setTimeout(() => stop(Object.assign(new Error('知识分析超时。'), { code: 'MODEL_TIMEOUT', retryable: true })), this.timeoutMs);
43
44
  child.on('message', message => {
44
45
  if (message?.type === 'complete') stop();
45
- else if (message?.type === 'failure') stop(Object.assign(new Error('后台 Agent 分析失败,请检查模型配置。'), { code: ['AI_RUNTIME_REQUIRED', 'ANALYSIS_OUTPUT_MISSING'].includes(message.code) ? message.code : 'MODEL_FAILED', retryable: true }));
46
+ else if (message?.type === 'failure') stop(analysisFailure(message.code));
46
47
  });
47
48
  child.on('error', () => stop(Object.assign(new Error('无法启动后台分析进程。'), { code: 'WORKER_START_FAILED', retryable: true })));
48
49
  // A completion message is not an exit: retain the directory lock until
@@ -143,7 +143,7 @@ async function api(method, pathname, search, body, store, coordinator, ctx) {
143
143
  if (method === 'GET' && pathname === '/api/events') return { events: store.listEvents(search.get('limit')).filter(e => !search.get('projectId') || e.projectId === search.get('projectId')) };
144
144
  if (method === 'POST' && pathname === '/api/events' && ctx.debug) return { event: store.addEvent(body) };
145
145
  if (method === 'POST' && pathname === '/api/bridge/notify') { if (coordinator.consume) coordinator.drain(); return { accepted: true }; }
146
- if (method === 'GET' && pathname === '/api/snapshot') return { projects: store.listProjects(), ...(search.get('summary') === '1' ? store.listRunPage({ projectId: search.get('projectId'), limit: search.get('limit') || 50, before: search.get('before') }) : { runs: store.listRuns(search.get('projectId')) }), events: store.listEvents(50), memories: coordinator.memories.list(), sharedScopes: store.state.sharedScopes };
146
+ if (method === 'GET' && pathname === '/api/snapshot') return { projects: store.listProjects(), ...(search.get('summary') === '1' ? store.listRunPage({ projectId: search.get('projectId'), limit: search.get('limit') || 50, before: search.get('before') }) : { runs: store.listRuns(search.get('projectId')) }), events: store.listEvents(50), memories: coordinator.memories.list(), sharedScopes: store.state.sharedScopes, maintenance: coordinator.maintenanceStatus(search.get('projectId')) };
147
147
  throw httpError(404, 'NOT_FOUND', '接口不存在。');
148
148
  }
149
149
 
@@ -1,67 +1,67 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const fs = require('node:fs');
4
- const os = require('node:os');
5
- const path = require('node:path');
6
- const http = require('node:http');
7
- const { createRequire } = require('node:module');
8
- const coreRoot = path.resolve(process.env.KB_CAPTURE_TEST_CORE_ROOT || path.join(__dirname, '../..'));
9
- const fromCore = createRequire(path.join(coreRoot, 'package.json'));
10
- const { createRuntime, initializeRuntime, createRequestHandler } = fromCore('./_site/lib/server-app');
11
- const { BridgeAdapter } = fromCore('./_site/lib/bridge-adapter');
12
- const { ModuleBridge } = fromCore('./_site/lib/module-bridge');
13
- const { findChrome } = require('./helpers/find-chrome');
14
- const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
15
-
16
- const HOSTILE_DETAIL = '<img src=x onerror="window.__kbXssProbe=1"> INJECT-STATUS-MARKER sk-abcdef123456';
17
-
18
- function clientFixture(client, label) {
19
- const mcpCapable = client === 'claude' || client === 'codex';
20
- return {
21
- client, label, available: false, configFound: false,
22
- knowledgeIntegration: { ok: true, state: 'installed' },
23
- automaticContext: { supported: client !== 'opencode', configured: false, state: 'check-failed', detail: HOSTILE_DETAIL },
24
- developmentCapture: { ok: true, state: 'installed', installed: true, detail: HOSTILE_DETAIL },
25
- mcpStartup: mcpCapable ? { state: 'failed', checkedAt: new Date().toISOString(), scope: 'client:' + client, code: 'PROBE_TIMEOUT', expectedVersion: '9.9.9-test', detail: HOSTILE_DETAIL } : undefined,
26
- };
27
- }
28
-
29
- async function main() {
30
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-mcp-ui-'));
31
- const dataDir = path.join(temp, 'data');
32
- const homeDir = path.join(temp, 'home');
33
- const eventsHome = path.join(dataDir, 'events');
34
- const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: 'offline stub' } })); });
35
- await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
36
- const offlineUrl = 'http://127.0.0.1:' + offline.address().port;
37
- const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
38
- await initializeRuntime(runtime);
39
- const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
40
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
41
- const base = 'http://127.0.0.1:' + server.address().port;
42
- const payload = JSON.stringify({ bridge: { ok: true }, scan: { state: 'ready' }, clients: [clientFixture('claude', 'Claude Code'), clientFixture('codex', 'Codex'), clientFixture('zcode', 'ZCode'), clientFixture('opencode', 'OpenCode')] });
43
- let browser;
44
- try {
45
- browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1440, height: 900 });
46
- const run = expression => browser.evaluate(expression);
47
- const click = selector => run('document.querySelector(' + JSON.stringify(selector) + ').click()');
48
- await run('(() => { const original = window.fetch; window.__originalFetch = original; window.fetch = (input, init) => { const url = String(input && input.url || input); if (url.includes(\'/api/integrations/status\')) { return Promise.resolve({ ok: true, json: async () => (' + payload + ') }); } return original(input, init); }; })()');
49
- await click('#openSystem');
50
- await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'capture rows render');
51
- assert(await run("document.getElementById('agentCaptureList').textContent.includes('PROBE_TIMEOUT')"), 'the MCP startup failure code must be rendered for MCP-capable clients');
52
- const claudeRowHtml = await run("document.querySelector('#agentCaptureList .agent-capture-row').innerHTML");
53
- assert(claudeRowHtml.includes('&lt;img'), 'hostile mcpStartup detail must be HTML-escaped, got: ' + claudeRowHtml.slice(0, 300));
54
- assert(!claudeRowHtml.includes('<img'), 'no raw hostile markup may reach the DOM');
55
- assert.equal(await run('window.__kbXssProbe'), undefined, 'the hostile onerror must never execute');
56
- const smalls = await run("[...document.querySelectorAll('#agentCaptureList small')].map(el => el.textContent).join('|')");
57
- assert(smalls.split('|').some(text => text.startsWith('MCP')), 'the MCP diagnostic is its own line item, got: ' + smalls);
58
- assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
59
- console.log('agent-capture-mcp-ui-test PASS (MCP line rendered separately; hostile detail escaped; no XSS execution)');
60
- } finally {
61
- await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
62
- offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
63
- assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-mcp-ui-'));
64
- fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
65
- }
66
- }
67
- main().catch(error => { console.error(error.stack); process.exitCode = 1; });
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const http = require('node:http');
7
+ const { createRequire } = require('node:module');
8
+ const coreRoot = path.resolve(process.env.KB_CAPTURE_TEST_CORE_ROOT || path.join(__dirname, '../..'));
9
+ const fromCore = createRequire(path.join(coreRoot, 'package.json'));
10
+ const { createRuntime, initializeRuntime, createRequestHandler } = fromCore('./_site/lib/server-app');
11
+ const { BridgeAdapter } = fromCore('./_site/lib/bridge-adapter');
12
+ const { ModuleBridge } = fromCore('./_site/lib/module-bridge');
13
+ const { findChrome } = require('./helpers/find-chrome');
14
+ const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
15
+
16
+ const HOSTILE_DETAIL = '<img src=x onerror="window.__kbXssProbe=1"> INJECT-STATUS-MARKER sk-abcdef123456';
17
+
18
+ function clientFixture(client, label) {
19
+ const mcpCapable = client === 'claude' || client === 'codex';
20
+ return {
21
+ client, label, available: false, configFound: false,
22
+ knowledgeIntegration: { ok: true, state: 'installed' },
23
+ automaticContext: { supported: client !== 'opencode', configured: false, state: 'check-failed', detail: HOSTILE_DETAIL },
24
+ developmentCapture: { ok: true, state: 'installed', installed: true, detail: HOSTILE_DETAIL },
25
+ mcpStartup: mcpCapable ? { state: 'failed', checkedAt: new Date().toISOString(), scope: 'client:' + client, code: 'PROBE_TIMEOUT', expectedVersion: '9.9.9-test', detail: HOSTILE_DETAIL } : undefined,
26
+ };
27
+ }
28
+
29
+ async function main() {
30
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-mcp-ui-'));
31
+ const dataDir = path.join(temp, 'data');
32
+ const homeDir = path.join(temp, 'home');
33
+ const eventsHome = path.join(dataDir, 'events');
34
+ const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: 'offline stub' } })); });
35
+ await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
36
+ const offlineUrl = 'http://127.0.0.1:' + offline.address().port;
37
+ const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
38
+ await initializeRuntime(runtime);
39
+ const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
40
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
41
+ const base = 'http://127.0.0.1:' + server.address().port;
42
+ const payload = JSON.stringify({ bridge: { ok: true }, scan: { state: 'ready' }, clients: [clientFixture('claude', 'Claude Code'), clientFixture('codex', 'Codex'), clientFixture('zcode', 'ZCode'), clientFixture('opencode', 'OpenCode')] });
43
+ let browser;
44
+ try {
45
+ browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1440, height: 900 });
46
+ const run = expression => browser.evaluate(expression);
47
+ const click = selector => run('document.querySelector(' + JSON.stringify(selector) + ').click()');
48
+ await run('(() => { const original = window.fetch; window.__originalFetch = original; window.fetch = (input, init) => { const url = String(input && input.url || input); if (url.includes(\'/api/integrations/status\')) { return Promise.resolve({ ok: true, json: async () => (' + payload + ') }); } return original(input, init); }; })()');
49
+ await click('#openSystem');
50
+ await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'capture rows render');
51
+ assert(await run("document.getElementById('agentCaptureList').textContent.includes('PROBE_TIMEOUT')"), 'the MCP startup failure code must be rendered for MCP-capable clients');
52
+ const claudeRowHtml = await run("document.querySelector('#agentCaptureList .agent-capture-row').innerHTML");
53
+ assert(claudeRowHtml.includes('&lt;img'), 'hostile mcpStartup detail must be HTML-escaped, got: ' + claudeRowHtml.slice(0, 300));
54
+ assert(!claudeRowHtml.includes('<img'), 'no raw hostile markup may reach the DOM');
55
+ assert.equal(await run('window.__kbXssProbe'), undefined, 'the hostile onerror must never execute');
56
+ const smalls = await run("[...document.querySelectorAll('#agentCaptureList small')].map(el => el.textContent).join('|')");
57
+ assert(smalls.split('|').some(text => text.startsWith('MCP')), 'the MCP diagnostic is its own line item, got: ' + smalls);
58
+ assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
59
+ console.log('agent-capture-mcp-ui-test PASS (MCP line rendered separately; hostile detail escaped; no XSS execution)');
60
+ } finally {
61
+ await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
62
+ offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
63
+ assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-mcp-ui-'));
64
+ fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
65
+ }
66
+ }
67
+ main().catch(error => { console.error(error.stack); process.exitCode = 1; });
@@ -1,75 +1,75 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { spawnServer } = require('./helpers/spawn-server');
6
-
7
- const ROOT = path.resolve(__dirname, '..', '..');
8
- const PORT = 7795;
9
- const BASE = `http://127.0.0.1:${PORT}`;
10
- const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-ai-v2-'));
11
-
12
- async function request(method, pathname, body) {
13
- const response = await fetch(`${BASE}${pathname}`, {
14
- method,
15
- headers: body === undefined ? undefined : { 'content-type': 'application/json' },
16
- body: body === undefined ? undefined : JSON.stringify(body),
17
- });
18
- return { status: response.status, text: await response.text() };
19
- }
20
-
21
- async function json(method, pathname, body) {
22
- const result = await request(method, pathname, body);
23
- return { ...result, data: JSON.parse(result.text || '{}') };
24
- }
25
-
26
- async function waitForServer() {
27
- for (let attempt = 0; attempt < 100; attempt += 1) {
28
- try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
29
- await new Promise(resolve => setTimeout(resolve, 100));
30
- }
31
- throw new Error('server did not start');
32
- }
33
-
34
- (async () => {
35
- const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'ai-profile-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
36
- try {
37
- await waitForServer();
38
- let result = await json('PUT', '/api/ai-profiles', {
39
- schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{
40
- id: 'secure', name: 'Secure', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'replace', value: 'sk-secret-1234' },
41
- }],
42
- });
43
- assert.strictEqual(result.status, 200, result.text);
44
- assert.strictEqual(result.data.config.profiles[0].hasApiKey, true);
45
- assert.strictEqual(result.data.config.profiles[0].apiKeyMasked, '****1234');
46
- assert(!result.text.includes('sk-secret-1234'));
47
-
48
- result = await json('GET', '/api/ai-profiles');
49
- assert.strictEqual(result.status, 200);
50
- assert(!result.text.includes('sk-secret-1234'));
51
- assert(!/"apiKey"\s*:/.test(result.text));
52
-
53
- result = await json('PUT', '/api/ai-profiles', {
54
- schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'preserve' } }],
55
- });
56
- assert.strictEqual(result.status, 200);
57
- let settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
58
- assert.strictEqual(settings.ai.profiles[0].apiKey, 'sk-secret-1234');
59
-
60
- result = await json('PUT', '/api/ai-profiles', {
61
- schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'clear' } }],
62
- });
63
- assert.strictEqual(result.status, 200);
64
- settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
65
- assert(!Object.prototype.hasOwnProperty.call(settings.ai.profiles[0], 'apiKey'));
66
-
67
- result = await json('PUT', '/api/ai-profiles', { profiles: [{ id: '../bad', apiKeyUpdate: { mode: 'clear' } }] });
68
- assert.strictEqual(result.status, 400);
69
- assert.strictEqual(result.data.error.code, 'INVALID_ARGUMENT');
70
- assert(!Object.prototype.hasOwnProperty.call(result.data.error, 'stack'));
71
- console.log('ai-profile-test PASS');
72
- } finally {
73
- await spawned.cleanup();
74
- }
75
- })().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 { spawnServer } = require('./helpers/spawn-server');
6
+
7
+ const ROOT = path.resolve(__dirname, '..', '..');
8
+ const PORT = 7795;
9
+ const BASE = `http://127.0.0.1:${PORT}`;
10
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-ai-v2-'));
11
+
12
+ async function request(method, pathname, body) {
13
+ const response = await fetch(`${BASE}${pathname}`, {
14
+ method,
15
+ headers: body === undefined ? undefined : { 'content-type': 'application/json' },
16
+ body: body === undefined ? undefined : JSON.stringify(body),
17
+ });
18
+ return { status: response.status, text: await response.text() };
19
+ }
20
+
21
+ async function json(method, pathname, body) {
22
+ const result = await request(method, pathname, body);
23
+ return { ...result, data: JSON.parse(result.text || '{}') };
24
+ }
25
+
26
+ async function waitForServer() {
27
+ for (let attempt = 0; attempt < 100; attempt += 1) {
28
+ try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
29
+ await new Promise(resolve => setTimeout(resolve, 100));
30
+ }
31
+ throw new Error('server did not start');
32
+ }
33
+
34
+ (async () => {
35
+ const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'ai-profile-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
36
+ try {
37
+ await waitForServer();
38
+ let result = await json('PUT', '/api/ai-profiles', {
39
+ schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{
40
+ id: 'secure', name: 'Secure', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'replace', value: 'sk-secret-1234' },
41
+ }],
42
+ });
43
+ assert.strictEqual(result.status, 200, result.text);
44
+ assert.strictEqual(result.data.config.profiles[0].hasApiKey, true);
45
+ assert.strictEqual(result.data.config.profiles[0].apiKeyMasked, '****1234');
46
+ assert(!result.text.includes('sk-secret-1234'));
47
+
48
+ result = await json('GET', '/api/ai-profiles');
49
+ assert.strictEqual(result.status, 200);
50
+ assert(!result.text.includes('sk-secret-1234'));
51
+ assert(!/"apiKey"\s*:/.test(result.text));
52
+
53
+ result = await json('PUT', '/api/ai-profiles', {
54
+ schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'preserve' } }],
55
+ });
56
+ assert.strictEqual(result.status, 200);
57
+ let settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
58
+ assert.strictEqual(settings.ai.profiles[0].apiKey, 'sk-secret-1234');
59
+
60
+ result = await json('PUT', '/api/ai-profiles', {
61
+ schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'clear' } }],
62
+ });
63
+ assert.strictEqual(result.status, 200);
64
+ settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
65
+ assert(!Object.prototype.hasOwnProperty.call(settings.ai.profiles[0], 'apiKey'));
66
+
67
+ result = await json('PUT', '/api/ai-profiles', { profiles: [{ id: '../bad', apiKeyUpdate: { mode: 'clear' } }] });
68
+ assert.strictEqual(result.status, 400);
69
+ assert.strictEqual(result.data.error.code, 'INVALID_ARGUMENT');
70
+ assert(!Object.prototype.hasOwnProperty.call(result.data.error, 'stack'));
71
+ console.log('ai-profile-test PASS');
72
+ } finally {
73
+ await spawned.cleanup();
74
+ }
75
+ })().catch(error => { console.error(error); process.exitCode = 1; });
@@ -1,91 +1,91 @@
1
- // Tests for ai-vendor-presets: 4 vendors with full fields, name/id lookup.
2
-
3
- const { AI_VENDOR_PRESETS, listVendorPresetNames, findVendorPresetByName } = require('../lib/ai-vendor-presets');
4
-
5
- function assert(cond, msg) { if (!cond) throw new Error(msg); }
6
-
7
- (() => {
8
- try {
9
- // 1. Exactly 4 vendors
10
- assert(Array.isArray(AI_VENDOR_PRESETS), 'AI_VENDOR_PRESETS should be an array');
11
- assert(AI_VENDOR_PRESETS.length === 4, `expected 4 vendors, got ${AI_VENDOR_PRESETS.length}`);
12
-
13
- // 2. IDs unique and match the expected set
14
- const ids = AI_VENDOR_PRESETS.map(v => v.id).sort();
15
- assert(JSON.stringify(ids) === JSON.stringify(['anthropic', 'deepseek', 'minimax', 'zhipu']),
16
- `vendor ids should be anthropic/deepseek/minimax/zhipu, got ${JSON.stringify(ids)}`);
17
-
18
- // 3. Every preset has the required fields
19
- const required = ['id', 'name', 'baseUrl', 'mainModel', 'thinkingModel',
20
- 'haikuModel', 'sonnetModel', 'opusModel', 'contextWindow', 'website'];
21
- for (const v of AI_VENDOR_PRESETS) {
22
- for (const f of required) {
23
- assert(v[f] !== undefined && v[f] !== null && v[f] !== '',
24
- `vendor ${v.id} missing or empty field: ${f}`);
25
- }
26
- }
27
-
28
- // 4. baseUrl: https://, no trailing slash
29
- for (const v of AI_VENDOR_PRESETS) {
30
- assert(/^https?:\/\//.test(v.baseUrl), `${v.id} baseUrl must start with http(s)://`);
31
- assert(!/\/$/.test(v.baseUrl), `${v.id} baseUrl must NOT have trailing slash`);
32
- }
33
-
34
- // 5. contextWindow is a positive integer
35
- for (const v of AI_VENDOR_PRESETS) {
36
- assert(Number.isInteger(v.contextWindow) && v.contextWindow > 0,
37
- `${v.id} contextWindow must be a positive integer, got ${v.contextWindow}`);
38
- }
39
-
40
- // 6. Specific known values
41
- const minimax = findVendorPresetByName('MiniMax');
42
- assert(minimax && minimax.baseUrl === 'https://api.minimaxi.com/anthropic',
43
- `minimax baseUrl wrong: ${minimax && minimax.baseUrl}`);
44
- assert(minimax && minimax.mainModel === 'MiniMax-M3',
45
- `minimax mainModel wrong: ${minimax && minimax.mainModel}`);
46
- assert(minimax && minimax.contextWindow === 1048576,
47
- `minimax contextWindow should be 1M`);
48
-
49
- const anthropic = findVendorPresetByName('Anthropic Claude');
50
- assert(anthropic && anthropic.baseUrl === 'https://api.anthropic.com',
51
- `anthropic baseUrl wrong`);
52
- assert(anthropic && anthropic.contextWindow === 200000,
53
- `anthropic contextWindow should be 200K`);
54
-
55
- const zhipu = findVendorPresetByName('智谱 GLM');
56
- assert(zhipu && zhipu.baseUrl === 'https://open.bigmodel.cn/api/anthropic',
57
- `zhipu baseUrl wrong: ${zhipu && zhipu.baseUrl}`);
58
-
59
- const deepseek = findVendorPresetByName('DeepSeek');
60
- assert(deepseek && deepseek.baseUrl === 'https://api.deepseek.com/anthropic',
61
- `deepseek baseUrl wrong: ${deepseek && deepseek.baseUrl}`);
62
- assert(deepseek && deepseek.mainModel === 'deepseek-chat',
63
- `deepseek mainModel wrong`);
64
- assert(deepseek && deepseek.thinkingModel === 'deepseek-reasoner',
65
- `deepseek thinkingModel wrong`);
66
-
67
- // 7. Case-insensitive lookup by name
68
- assert(findVendorPresetByName('minimax').id === 'minimax', 'lowercase name lookup');
69
- assert(findVendorPresetByName('MINIMAX').id === 'minimax', 'uppercase name lookup');
70
-
71
- // 8. Lookup by id also works
72
- assert(findVendorPresetByName('minimax').id === 'minimax', 'id lookup');
73
-
74
- // 9. Unknown / empty / null returns null (no false positive)
75
- assert(findVendorPresetByName('Totally Unknown Vendor') === null, 'unknown vendor → null');
76
- assert(findVendorPresetByName('') === null, 'empty name → null');
77
- assert(findVendorPresetByName(null) === null, 'null name → null');
78
- assert(findVendorPresetByName(undefined) === null, 'undefined name → null');
79
-
80
- // 10. listVendorPresetNames returns all 4 names in stable order
81
- const names = listVendorPresetNames();
82
- assert(names.length === 4, `expected 4 names, got ${names.length}`);
83
- assert(names.every(n => typeof n === 'string' && n.length > 0),
84
- 'all names should be non-empty strings');
85
-
86
- console.log('ai-vendor-presets test passed');
87
- } catch (e) {
88
- console.error('ai-vendor-presets test failed:', e.message);
89
- process.exitCode = 1;
90
- }
91
- })();
1
+ // Tests for ai-vendor-presets: 4 vendors with full fields, name/id lookup.
2
+
3
+ const { AI_VENDOR_PRESETS, listVendorPresetNames, findVendorPresetByName } = require('../lib/ai-vendor-presets');
4
+
5
+ function assert(cond, msg) { if (!cond) throw new Error(msg); }
6
+
7
+ (() => {
8
+ try {
9
+ // 1. Exactly 4 vendors
10
+ assert(Array.isArray(AI_VENDOR_PRESETS), 'AI_VENDOR_PRESETS should be an array');
11
+ assert(AI_VENDOR_PRESETS.length === 4, `expected 4 vendors, got ${AI_VENDOR_PRESETS.length}`);
12
+
13
+ // 2. IDs unique and match the expected set
14
+ const ids = AI_VENDOR_PRESETS.map(v => v.id).sort();
15
+ assert(JSON.stringify(ids) === JSON.stringify(['anthropic', 'deepseek', 'minimax', 'zhipu']),
16
+ `vendor ids should be anthropic/deepseek/minimax/zhipu, got ${JSON.stringify(ids)}`);
17
+
18
+ // 3. Every preset has the required fields
19
+ const required = ['id', 'name', 'baseUrl', 'mainModel', 'thinkingModel',
20
+ 'haikuModel', 'sonnetModel', 'opusModel', 'contextWindow', 'website'];
21
+ for (const v of AI_VENDOR_PRESETS) {
22
+ for (const f of required) {
23
+ assert(v[f] !== undefined && v[f] !== null && v[f] !== '',
24
+ `vendor ${v.id} missing or empty field: ${f}`);
25
+ }
26
+ }
27
+
28
+ // 4. baseUrl: https://, no trailing slash
29
+ for (const v of AI_VENDOR_PRESETS) {
30
+ assert(/^https?:\/\//.test(v.baseUrl), `${v.id} baseUrl must start with http(s)://`);
31
+ assert(!/\/$/.test(v.baseUrl), `${v.id} baseUrl must NOT have trailing slash`);
32
+ }
33
+
34
+ // 5. contextWindow is a positive integer
35
+ for (const v of AI_VENDOR_PRESETS) {
36
+ assert(Number.isInteger(v.contextWindow) && v.contextWindow > 0,
37
+ `${v.id} contextWindow must be a positive integer, got ${v.contextWindow}`);
38
+ }
39
+
40
+ // 6. Specific known values
41
+ const minimax = findVendorPresetByName('MiniMax');
42
+ assert(minimax && minimax.baseUrl === 'https://api.minimaxi.com/anthropic',
43
+ `minimax baseUrl wrong: ${minimax && minimax.baseUrl}`);
44
+ assert(minimax && minimax.mainModel === 'MiniMax-M3',
45
+ `minimax mainModel wrong: ${minimax && minimax.mainModel}`);
46
+ assert(minimax && minimax.contextWindow === 1048576,
47
+ `minimax contextWindow should be 1M`);
48
+
49
+ const anthropic = findVendorPresetByName('Anthropic Claude');
50
+ assert(anthropic && anthropic.baseUrl === 'https://api.anthropic.com',
51
+ `anthropic baseUrl wrong`);
52
+ assert(anthropic && anthropic.contextWindow === 200000,
53
+ `anthropic contextWindow should be 200K`);
54
+
55
+ const zhipu = findVendorPresetByName('智谱 GLM');
56
+ assert(zhipu && zhipu.baseUrl === 'https://open.bigmodel.cn/api/anthropic',
57
+ `zhipu baseUrl wrong: ${zhipu && zhipu.baseUrl}`);
58
+
59
+ const deepseek = findVendorPresetByName('DeepSeek');
60
+ assert(deepseek && deepseek.baseUrl === 'https://api.deepseek.com/anthropic',
61
+ `deepseek baseUrl wrong: ${deepseek && deepseek.baseUrl}`);
62
+ assert(deepseek && deepseek.mainModel === 'deepseek-chat',
63
+ `deepseek mainModel wrong`);
64
+ assert(deepseek && deepseek.thinkingModel === 'deepseek-reasoner',
65
+ `deepseek thinkingModel wrong`);
66
+
67
+ // 7. Case-insensitive lookup by name
68
+ assert(findVendorPresetByName('minimax').id === 'minimax', 'lowercase name lookup');
69
+ assert(findVendorPresetByName('MINIMAX').id === 'minimax', 'uppercase name lookup');
70
+
71
+ // 8. Lookup by id also works
72
+ assert(findVendorPresetByName('minimax').id === 'minimax', 'id lookup');
73
+
74
+ // 9. Unknown / empty / null returns null (no false positive)
75
+ assert(findVendorPresetByName('Totally Unknown Vendor') === null, 'unknown vendor → null');
76
+ assert(findVendorPresetByName('') === null, 'empty name → null');
77
+ assert(findVendorPresetByName(null) === null, 'null name → null');
78
+ assert(findVendorPresetByName(undefined) === null, 'undefined name → null');
79
+
80
+ // 10. listVendorPresetNames returns all 4 names in stable order
81
+ const names = listVendorPresetNames();
82
+ assert(names.length === 4, `expected 4 names, got ${names.length}`);
83
+ assert(names.every(n => typeof n === 'string' && n.length > 0),
84
+ 'all names should be non-empty strings');
85
+
86
+ console.log('ai-vendor-presets test passed');
87
+ } catch (e) {
88
+ console.error('ai-vendor-presets test failed:', e.message);
89
+ process.exitCode = 1;
90
+ }
91
+ })();