@sanqianx/project-knowledge 5.0.17 → 5.0.18-rc.2

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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "project-knowledge",
12
- "version": "5.0.17",
12
+ "version": "5.0.18-rc.2",
13
13
  "source": "./plugins/project-knowledge",
14
14
  "description": "Search durable project decisions and implementation history through MCP."
15
15
  }
@@ -1,87 +1,100 @@
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
- const { Journal } = fromCore('@sanqianx/ai-coding-event-bridge-console/src/bridge');
16
- const { spawnSync } = require('node:child_process');
17
-
18
- async function main() {
19
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-capture-ui-'));
20
- const dataDir = path.join(temp, 'data');
21
- const homeDir = path.join(temp, '用户 空格');
22
- const eventsHome = path.join(dataDir, 'events');
23
- const configFile = path.join(homeDir, '.zcode', 'cli', 'config.json');
24
- fs.mkdirSync(path.dirname(configFile), { recursive: true });
25
- fs.writeFileSync(configFile, JSON.stringify({ provider: { name: 'retain' } }));
26
- const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: '本轮测试中非采集模块离线' } })); });
27
- await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
28
- const offlineUrl = `http://127.0.0.1:${offline.address().port}`;
29
- const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
30
- await initializeRuntime(runtime);
31
- const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
32
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
33
- const base = `http://127.0.0.1:${server.address().port}`;
34
- let browser;
35
- try {
36
- browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1920, height: 1080 });
37
- await browser.setViewport(1920, 1080);
38
- const run = expression => browser.evaluate(expression);
39
- const click = selector => run(`document.querySelector(${JSON.stringify(selector)}).click()`);
40
- await click('#openSystem');
41
- await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'four capture statuses');
42
- assert.equal(await run("document.querySelector('[data-agent-client=claude]').disabled"), true);
43
- assert.equal(await run("document.querySelector('[data-agent-client=zcode]').disabled"), false);
44
- assert(await run("document.getElementById('agentCaptureList').textContent.includes('检测到配置')"));
45
- await click('[data-agent-client=zcode][data-agent-capture=install]');
46
- await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]') !== null"), 'installed status reloaded from backend');
47
- const config = JSON.parse(fs.readFileSync(configFile));
48
- assert.equal(config.hooks.enabled, true);
49
- const hook = config.hooks.events.UserPromptSubmit[0].hooks[0];
50
- const env = { ...process.env }; delete env.AI_CODING_EVENT_BRIDGE_HOME; delete env.AI_CODING_EVENT_BRIDGE_CAPTURE;
51
- const child = spawnSync(process.execPath, hook.args, { env, input: JSON.stringify({ hook_event_name: 'UserPromptSubmit', session_id: 'ui-zcode-session', cwd: temp, prompt: '从网页接入后写入真实 journal' }), encoding: 'utf8', timeout: 15000 });
52
- assert.equal(child.status, 0, child.stderr);
53
- const events = await new Journal(path.join(eventsHome, 'journal')).readEvents({});
54
- assert.equal(events.length, 1); assert.equal(events[0].source, 'zcode');
55
- await click('[data-agent-client=zcode][data-agent-capture=install]');
56
- await waitFor(() => run("!document.querySelector('[data-agent-client=zcode]').disabled"), 'repair response');
57
- assert.equal(JSON.parse(fs.readFileSync(configFile)).hooks.events.UserPromptSubmit.length, 1);
58
- assert.equal(await run('document.documentElement.scrollWidth > innerWidth'), false);
59
- const artifacts = path.resolve(process.env.KB_CAPTURE_TEST_ARTIFACTS || path.join(__dirname, '../..', '.agent-state/acceptance/agent-capture'));
60
- await browser.screenshot(path.join(artifacts, '1920-light.png'));
61
- await click('#closeSystem'); await click('#themeToggle'); await click('#openSystem');
62
- await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]') !== null"), 'theme retains capture status');
63
- await browser.screenshot(path.join(artifacts, '1920-dark.png'));
64
- fs.writeFileSync(configFile, '{invalid JSON');
65
- await waitFor(() => run("(() => { const b = document.querySelector('[data-agent-client=zcode][data-agent-capture=install]'); return b !== null && !b.disabled; })()"), 'capture row settled before invalid config');
66
- await click('[data-agent-client=zcode][data-agent-capture=install]');
67
- await waitFor(() => run("document.getElementById('agentCaptureFeedback').textContent.includes('操作 ID')"), 'partial failure rendered');
68
- assert(await run("document.getElementById('agentCaptureFeedback').classList.contains('issue')"));
69
- const logs = (await (await fetch(base + '/api/logs?event=integration.capture.failed')).json()).entries;
70
- assert(logs.length > 0 && logs[0].error.message.includes('valid JSON'));
71
- fs.writeFileSync(configFile, JSON.stringify(config));
72
- await click('#refreshAgentCapture');
73
- await waitFor(() => run("(() => { const b = document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]'); return b !== null && !b.disabled; })()"), 'manual status retry');
74
- await click('[data-agent-client=zcode][data-agent-capture=uninstall]');
75
- await waitFor(() => run("!document.querySelector('[data-agent-client=zcode]').disabled && document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]') === null"), 'removed capture status');
76
- assert.equal(JSON.parse(fs.readFileSync(configFile)).provider.name, 'retain');
77
- assert.equal((await new Journal(path.join(eventsHome, 'journal')).readEvents({})).length, 1, 'removal retains prior evidence');
78
- assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
79
- console.log(`agent-capture-ui-test PASS (1920x1080; real install/repair/Hook/journal/error logs/removal/light+dark; core=${coreRoot})`);
80
- } finally {
81
- await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
82
- offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
83
- assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-capture-ui-'));
84
- fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
85
- }
86
- }
87
- 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
+ const { Journal } = fromCore('@sanqianx/ai-coding-event-bridge-console/src/bridge');
16
+ const { spawnSync } = require('node:child_process');
17
+
18
+ async function main() {
19
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-capture-ui-'));
20
+ const dataDir = path.join(temp, 'data');
21
+ const homeDir = path.join(temp, '用户 空格');
22
+ const eventsHome = path.join(dataDir, 'events');
23
+ const configFile = path.join(homeDir, '.zcode', 'cli', 'config.json');
24
+ const intentFile = path.join(dataDir, 'integration-intent.json');
25
+ fs.mkdirSync(path.dirname(configFile), { recursive: true });
26
+ fs.writeFileSync(configFile, JSON.stringify({ provider: { name: 'retain' } }));
27
+ const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: '本轮测试中非采集模块离线' } })); });
28
+ await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
29
+ const offlineUrl = `http://127.0.0.1:${offline.address().port}`;
30
+ const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
31
+ await initializeRuntime(runtime);
32
+ const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
33
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
34
+ const base = `http://127.0.0.1:${server.address().port}`;
35
+ let browser;
36
+ try {
37
+ browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1920, height: 1080 });
38
+ await browser.setViewport(1920, 1080);
39
+ const run = expression => browser.evaluate(expression);
40
+ const click = selector => run(`document.querySelector(${JSON.stringify(selector)}).click()`);
41
+ await click('#openSystem');
42
+ await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'four capture statuses');
43
+ // Single-switch rows: claude undetected -> disabled switch; zcode configured -> enabled, badge 未接入.
44
+ assert.equal(await run("document.querySelector('[data-agent-client=claude][data-agent-switch]').disabled"), true);
45
+ assert.equal(await run("document.querySelector('[data-agent-client=zcode][data-agent-switch]').disabled"), false);
46
+ await waitFor(() => run("document.querySelector('[data-agent-client=zcode]').closest('.agent-capture-row').textContent.includes('未接入')"), 'zcode badge settles to disconnected');
47
+ assert(await run("document.getElementById('agentCaptureList').textContent.includes('检测到配置')"));
48
+ assert.equal(await run("document.querySelector('[data-agent-client=zcode]').closest('.agent-capture-row').querySelector('details.agent-detail').open"), false, 'details must be collapsed by default');
49
+ assert.equal(await run("document.getElementById('refreshAgentCapture')"), null, 'manual status button removed');
50
+ // Switch ON: full install (knowledge + capture), real config written.
51
+ await click('[data-agent-client=zcode][data-agent-switch=install]');
52
+ await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-switch=uninstall]') !== null"), 'connected status reloaded from backend');
53
+ assert(await run("document.querySelector('[data-agent-client=zcode]').closest('.agent-capture-row').textContent.includes('已接入')"));
54
+ const config = JSON.parse(fs.readFileSync(configFile));
55
+ assert.equal(config.hooks.enabled, true);
56
+ const intent = JSON.parse(fs.readFileSync(intentFile));
57
+ assert.equal(intent.clients.zcode, 'connected', 'successful install records intent');
58
+ const env = { ...process.env }; delete env.AI_CODING_EVENT_BRIDGE_HOME; delete env.AI_CODING_EVENT_BRIDGE_CAPTURE;
59
+ // Full install wires every UserPromptSubmit hook (context pre-read first,
60
+ // capture second) — run them all like the real client does.
61
+ for (const hook of config.hooks.events.UserPromptSubmit.flatMap(entry => entry.hooks)) {
62
+ const child = spawnSync(process.execPath, hook.args, { env, input: JSON.stringify({ hook_event_name: 'UserPromptSubmit', session_id: 'ui-zcode-session', cwd: temp, prompt: '从网页接入后写入真实 journal' }), encoding: 'utf8', timeout: 15000 });
63
+ assert.equal(child.status, 0, child.stderr);
64
+ }
65
+ const events = await new Journal(path.join(eventsHome, 'journal')).readEvents({});
66
+ assert.equal(events.length, 1); assert.equal(events[0].source, 'zcode');
67
+ assert.equal(await run('document.documentElement.scrollWidth > innerWidth'), false);
68
+ const artifacts = path.resolve(process.env.KB_CAPTURE_TEST_ARTIFACTS || path.join(__dirname, '../..', '.agent-state/acceptance/agent-capture'));
69
+ await browser.screenshot(path.join(artifacts, '1920-light.png'));
70
+ await click('#closeSystem'); await click('#themeToggle'); await click('#openSystem');
71
+ await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-switch=uninstall]') !== null"), 'theme retains capture status');
72
+ await browser.screenshot(path.join(artifacts, '1920-dark.png'));
73
+ // Broken config: the single switch acts as the explicit repair path.
74
+ fs.writeFileSync(configFile, '{invalid JSON');
75
+ await click('#closeSystem'); await click('#openSystem');
76
+ await waitFor(() => run("(() => { const b = document.querySelector('[data-agent-client=zcode][data-agent-switch=install]'); return b !== null && !b.disabled; })()"), 'capture row settled before invalid config');
77
+ await click('[data-agent-client=zcode][data-agent-switch=install]');
78
+ await waitFor(() => run("document.getElementById('agentCaptureFeedback').textContent.includes('操作未完成')"), 'partial failure rendered');
79
+ assert(await run("document.getElementById('agentCaptureFeedback').classList.contains('issue')"));
80
+ const logs = (await (await fetch(base + '/api/logs?event=integration.capture.failed')).json()).entries;
81
+ assert(logs.length > 0 && logs[0].error.message.includes('valid JSON'));
82
+ fs.writeFileSync(configFile, JSON.stringify(config));
83
+ await click('#closeSystem'); await click('#openSystem');
84
+ await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-switch=uninstall]') !== null"), 'switch repair succeeds');
85
+ // Switch OFF: removal keeps third-party config and prior evidence.
86
+ await click('[data-agent-client=zcode][data-agent-switch=uninstall]');
87
+ await waitFor(() => run("!document.querySelector('[data-agent-client=zcode][data-agent-switch]').disabled && document.querySelector('[data-agent-client=zcode][data-agent-switch=uninstall]') === null"), 'removed capture status');
88
+ assert.equal(JSON.parse(fs.readFileSync(configFile)).provider.name, 'retain');
89
+ assert.equal(JSON.parse(fs.readFileSync(intentFile)).clients.zcode, 'disconnected', 'uninstall records disconnected intent');
90
+ assert.equal((await new Journal(path.join(eventsHome, 'journal')).readEvents({})).length, 1, 'removal retains prior evidence');
91
+ assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
92
+ console.log(`agent-capture-ui-test PASS (1920x1080; single-switch install/repair/Hook/journal/error logs/intent/removal/light+dark; core=${coreRoot})`);
93
+ } finally {
94
+ await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
95
+ offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
96
+ assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-capture-ui-'));
97
+ fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
98
+ }
99
+ }
100
+ main().catch(error => { console.error(error.stack); process.exitCode = 1; });
@@ -20,7 +20,7 @@ assert(css.includes('@media(prefers-reduced-motion:reduce)'), 'reduced-motion co
20
20
  assert(css.includes('grid-template-columns:232px minmax(0,1fr)') && css.includes('min-width:0'), 'desktop shell must bound its content layout');
21
21
 
22
22
  assert(preload.includes('project-knowledge:open-external') && preload.includes('openExternal:'), 'desktop preload bounded external-link bridge regressed');
23
- assert(main.includes('externalLink.registerExternalLink'), 'desktop main external-link policy regressed');
23
+ assert(main.includes("ipcMain.handle('project-knowledge:open-external'") && main.includes('runtime.isAllowedExternalUrl(url)'), 'desktop main external-link policy regressed');
24
24
  assert(runtime.isAllowedExternalUrl('https://github.com/login/device'), 'HTTPS authorization URL policy regressed');
25
25
  assert(!runtime.isAllowedExternalUrl('file:///C:/Users/test/token.txt'), 'local files must not cross the desktop boundary');
26
26
  assert(!runtime.isAllowedExternalUrl('javascript:alert(1)'), 'script URLs must not cross the desktop boundary');