@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.
- package/.agents/plugins/marketplace.json +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/README.md +4 -0
- package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
- package/_modules/knowledge-engine/src/coordinator.js +7 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/server.js +1 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -35
- package/_site/_test/candidate-product-assets-test.js +13 -13
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/claude-executable-discovery-test.js +28 -28
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +134 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +4 -4
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +10 -10
- package/_site/_test/conversation-origin-test.js +15 -15
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/integration-adapters-test.js +67 -67
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +109 -109
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -33
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -63
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -42
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-import-ui-test.js +104 -104
- package/_site/_test/product-progress-status-test.js +12 -1
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/registry-events-test.js +38 -38
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-scope-root-binding-test.js +67 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +6 -6
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +35 -35
- package/_site/_test/workbench-context-receipts-test.js +22 -22
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/_site/_test/zcode-capture-integration-test.js +108 -108
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +3 -3
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +89 -89
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +12 -12
- package/_site/lib/commit-reconciler.js +38 -38
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +17 -17
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -93
- package/_site/lib/engine-knowledge-client.js +72 -72
- package/_site/lib/folder-picker-output.js +3 -3
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/github-team-store.js +3 -3
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +15 -15
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/live-gateway.js +78 -78
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -874
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +54 -54
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +14 -14
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/registry-events.js +16 -16
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +5 -5
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -89
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -83
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +8 -8
- package/bin/project-knowledge.js +170 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +205 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +42 -42
- package/docs/leader-glm-workbench-playbook.md +122 -122
- package/docs/project-registry-schema.md +11 -11
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -25
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +53 -33
- package/package.json +3 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/change.md +30 -30
- package/templates/project-readme.md +18 -18
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +118 -0
- package/ui/product.css +8 -0
- package/ui/product.js +146 -4
- package/vendor-manifest.json +55 -54
|
@@ -1,96 +1,96 @@
|
|
|
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
|
-
const { createRequestHandler } = require('../lib/server-app');
|
|
7
|
-
|
|
8
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
9
|
-
const PORT = 7832;
|
|
10
|
-
const BASE = `http://127.0.0.1:${PORT}`;
|
|
11
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-security-v2-'));
|
|
12
|
-
const REMOTE_PORT = 7833;
|
|
13
|
-
const REMOTE_BASE = `http://127.0.0.1:${REMOTE_PORT}`;
|
|
14
|
-
const remoteDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-security-remote-v2-'));
|
|
15
|
-
|
|
16
|
-
async function waitForServer() {
|
|
17
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
18
|
-
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
19
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
20
|
-
}
|
|
21
|
-
throw new Error('server did not start');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
(async () => {
|
|
25
|
-
assert.throws(
|
|
26
|
-
() => createRequestHandler({}, { host: '0.0.0.0', port: 9999 }),
|
|
27
|
-
error => error && error.code === 'AUTH_REQUIRED',
|
|
28
|
-
'non-loopback bind must require authentication',
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'security-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
32
|
-
try {
|
|
33
|
-
await waitForServer();
|
|
34
|
-
let response = await fetch(`${BASE}/api/ai-profiles`, { headers: { Origin: 'https://evil.example' } });
|
|
35
|
-
assert.strictEqual(response.status, 403);
|
|
36
|
-
assert.strictEqual(response.headers.get('access-control-allow-origin'), null);
|
|
37
|
-
|
|
38
|
-
response = await fetch(`${BASE}/api/ai-profiles`, {
|
|
39
|
-
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({
|
|
40
|
-
schema: 'ai-profiles/v1', profiles: [{ id: 'secret', enabled: true, apiKeyUpdate: { mode: 'replace', value: 'top-secret-api-key' } }],
|
|
41
|
-
}),
|
|
42
|
-
});
|
|
43
|
-
assert.strictEqual(response.status, 200);
|
|
44
|
-
response = await fetch(`${BASE}/api/ai-profiles`);
|
|
45
|
-
const profileText = await response.text();
|
|
46
|
-
assert(!profileText.includes('top-secret-api-key'));
|
|
47
|
-
assert(!/"apiKey"\s*:/.test(profileText));
|
|
48
|
-
|
|
49
|
-
response = await fetch(`${BASE}/api/raw?path=../../settings.json`);
|
|
50
|
-
assert.strictEqual(response.status, 404);
|
|
51
|
-
const rawText = await response.text();
|
|
52
|
-
assert(!rawText.includes('top-secret-api-key'));
|
|
53
|
-
|
|
54
|
-
response = await fetch(`${BASE}/api/projects/not-valid%2Fid`);
|
|
55
|
-
const errorText = await response.text();
|
|
56
|
-
assert(!/\bstack\b/i.test(errorText));
|
|
57
|
-
assert(!errorText.includes('top-secret-api-key'));
|
|
58
|
-
const error = JSON.parse(errorText);
|
|
59
|
-
assert(error.error.operationId);
|
|
60
|
-
|
|
61
|
-
const remote = spawnServer({
|
|
62
|
-
root: ROOT,
|
|
63
|
-
port: REMOTE_PORT,
|
|
64
|
-
host: '0.0.0.0',
|
|
65
|
-
dataDir: remoteDataDir,
|
|
66
|
-
tag: 'security-remote-v2',
|
|
67
|
-
extraEnv: {
|
|
68
|
-
KB_SKIP_MIGRATION: '1',
|
|
69
|
-
KB_SITE_AUTH_TOKEN: 'remote-auth-secret',
|
|
70
|
-
KB_ALLOWED_ORIGINS: 'https://allowed.example',
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
try {
|
|
74
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
75
|
-
try {
|
|
76
|
-
const ready = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Authorization: 'Bearer remote-auth-secret', Origin: 'https://allowed.example' } });
|
|
77
|
-
if (ready.ok) break;
|
|
78
|
-
} catch {}
|
|
79
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
80
|
-
}
|
|
81
|
-
let remoteResponse = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Origin: 'https://allowed.example' } });
|
|
82
|
-
assert.strictEqual(remoteResponse.status, 401, 'non-loopback request without token must fail');
|
|
83
|
-
remoteResponse = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Authorization: 'Bearer remote-auth-secret', Origin: 'https://evil.example' } });
|
|
84
|
-
assert.strictEqual(remoteResponse.status, 403, 'non-loopback request from an unlisted Origin must fail');
|
|
85
|
-
remoteResponse = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Authorization: 'Bearer remote-auth-secret', Origin: 'https://allowed.example' } });
|
|
86
|
-
assert.strictEqual(remoteResponse.status, 200);
|
|
87
|
-
assert.strictEqual(remoteResponse.headers.get('access-control-allow-origin'), 'https://allowed.example');
|
|
88
|
-
} finally {
|
|
89
|
-
remote.cleanup();
|
|
90
|
-
}
|
|
91
|
-
console.log('server-security-test PASS');
|
|
92
|
-
} finally {
|
|
93
|
-
await spawned.cleanup();
|
|
94
|
-
fs.rmSync(remoteDataDir, { recursive: true, force: true });
|
|
95
|
-
}
|
|
96
|
-
})().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
|
+
const { createRequestHandler } = require('../lib/server-app');
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
9
|
+
const PORT = 7832;
|
|
10
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
11
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-security-v2-'));
|
|
12
|
+
const REMOTE_PORT = 7833;
|
|
13
|
+
const REMOTE_BASE = `http://127.0.0.1:${REMOTE_PORT}`;
|
|
14
|
+
const remoteDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-security-remote-v2-'));
|
|
15
|
+
|
|
16
|
+
async function waitForServer() {
|
|
17
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
18
|
+
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
20
|
+
}
|
|
21
|
+
throw new Error('server did not start');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
(async () => {
|
|
25
|
+
assert.throws(
|
|
26
|
+
() => createRequestHandler({}, { host: '0.0.0.0', port: 9999 }),
|
|
27
|
+
error => error && error.code === 'AUTH_REQUIRED',
|
|
28
|
+
'non-loopback bind must require authentication',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'security-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
32
|
+
try {
|
|
33
|
+
await waitForServer();
|
|
34
|
+
let response = await fetch(`${BASE}/api/ai-profiles`, { headers: { Origin: 'https://evil.example' } });
|
|
35
|
+
assert.strictEqual(response.status, 403);
|
|
36
|
+
assert.strictEqual(response.headers.get('access-control-allow-origin'), null);
|
|
37
|
+
|
|
38
|
+
response = await fetch(`${BASE}/api/ai-profiles`, {
|
|
39
|
+
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({
|
|
40
|
+
schema: 'ai-profiles/v1', profiles: [{ id: 'secret', enabled: true, apiKeyUpdate: { mode: 'replace', value: 'top-secret-api-key' } }],
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
assert.strictEqual(response.status, 200);
|
|
44
|
+
response = await fetch(`${BASE}/api/ai-profiles`);
|
|
45
|
+
const profileText = await response.text();
|
|
46
|
+
assert(!profileText.includes('top-secret-api-key'));
|
|
47
|
+
assert(!/"apiKey"\s*:/.test(profileText));
|
|
48
|
+
|
|
49
|
+
response = await fetch(`${BASE}/api/raw?path=../../settings.json`);
|
|
50
|
+
assert.strictEqual(response.status, 404);
|
|
51
|
+
const rawText = await response.text();
|
|
52
|
+
assert(!rawText.includes('top-secret-api-key'));
|
|
53
|
+
|
|
54
|
+
response = await fetch(`${BASE}/api/projects/not-valid%2Fid`);
|
|
55
|
+
const errorText = await response.text();
|
|
56
|
+
assert(!/\bstack\b/i.test(errorText));
|
|
57
|
+
assert(!errorText.includes('top-secret-api-key'));
|
|
58
|
+
const error = JSON.parse(errorText);
|
|
59
|
+
assert(error.error.operationId);
|
|
60
|
+
|
|
61
|
+
const remote = spawnServer({
|
|
62
|
+
root: ROOT,
|
|
63
|
+
port: REMOTE_PORT,
|
|
64
|
+
host: '0.0.0.0',
|
|
65
|
+
dataDir: remoteDataDir,
|
|
66
|
+
tag: 'security-remote-v2',
|
|
67
|
+
extraEnv: {
|
|
68
|
+
KB_SKIP_MIGRATION: '1',
|
|
69
|
+
KB_SITE_AUTH_TOKEN: 'remote-auth-secret',
|
|
70
|
+
KB_ALLOWED_ORIGINS: 'https://allowed.example',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
75
|
+
try {
|
|
76
|
+
const ready = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Authorization: 'Bearer remote-auth-secret', Origin: 'https://allowed.example' } });
|
|
77
|
+
if (ready.ok) break;
|
|
78
|
+
} catch {}
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
80
|
+
}
|
|
81
|
+
let remoteResponse = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Origin: 'https://allowed.example' } });
|
|
82
|
+
assert.strictEqual(remoteResponse.status, 401, 'non-loopback request without token must fail');
|
|
83
|
+
remoteResponse = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Authorization: 'Bearer remote-auth-secret', Origin: 'https://evil.example' } });
|
|
84
|
+
assert.strictEqual(remoteResponse.status, 403, 'non-loopback request from an unlisted Origin must fail');
|
|
85
|
+
remoteResponse = await fetch(`${REMOTE_BASE}/api/health`, { headers: { Authorization: 'Bearer remote-auth-secret', Origin: 'https://allowed.example' } });
|
|
86
|
+
assert.strictEqual(remoteResponse.status, 200);
|
|
87
|
+
assert.strictEqual(remoteResponse.headers.get('access-control-allow-origin'), 'https://allowed.example');
|
|
88
|
+
} finally {
|
|
89
|
+
remote.cleanup();
|
|
90
|
+
}
|
|
91
|
+
console.log('server-security-test PASS');
|
|
92
|
+
} finally {
|
|
93
|
+
await spawned.cleanup();
|
|
94
|
+
fs.rmSync(remoteDataDir, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
7
|
-
const { spawnServer } = require('./helpers/spawn-server');
|
|
8
|
-
const freePort = require('./helpers/free-port');
|
|
9
|
-
|
|
10
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
-
let PORT, BASE;
|
|
12
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-sse-v2-'));
|
|
13
|
-
const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-sse-repo-'));
|
|
14
|
-
|
|
15
|
-
function git(args) {
|
|
16
|
-
const result = spawnSync('git', args, { cwd: repoPath, encoding: 'utf8' });
|
|
17
|
-
assert.strictEqual(result.status, 0, result.stderr);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function json(method, pathname, body) {
|
|
21
|
-
const response = await fetch(`${BASE}${pathname}`, { method, headers: body === undefined ? undefined : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
22
|
-
return { status: response.status, data: await response.json() };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function waitForServer() {
|
|
26
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
27
|
-
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
28
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
29
|
-
}
|
|
30
|
-
throw new Error('server did not start');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function openSse() {
|
|
34
|
-
const frames = [];
|
|
35
|
-
let buffer = '';
|
|
36
|
-
const request = http.get(`${BASE}/api/claude/sessions-stream`, response => {
|
|
37
|
-
assert.strictEqual(response.statusCode, 200);
|
|
38
|
-
response.on('data', chunk => {
|
|
39
|
-
buffer += chunk.toString('utf8');
|
|
40
|
-
let boundary;
|
|
41
|
-
while ((boundary = buffer.indexOf('\n\n')) >= 0) {
|
|
42
|
-
const block = buffer.slice(0, boundary);
|
|
43
|
-
buffer = buffer.slice(boundary + 2);
|
|
44
|
-
const event = /^event:\s*(.+)$/m.exec(block);
|
|
45
|
-
const data = /^data:\s*(.+)$/m.exec(block);
|
|
46
|
-
if (event && data) frames.push({ event: event[1], data: JSON.parse(data[1]) });
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
request.on('error', error => { frames.push({ event: 'transport-error', data: { message: error.message } }); });
|
|
51
|
-
return { frames, close: () => request.destroy() };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function waitFor(predicate) {
|
|
55
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
56
|
-
if (predicate()) return;
|
|
57
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
58
|
-
}
|
|
59
|
-
throw new Error('timed out waiting for SSE frame');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
(async () => {
|
|
63
|
-
PORT = await freePort(); BASE = `http://127.0.0.1:${PORT}`;
|
|
64
|
-
git(['init', '--initial-branch=main']);
|
|
65
|
-
git(['config', 'user.email', 'sse@example.test']);
|
|
66
|
-
git(['config', 'user.name', 'SSE Test']);
|
|
67
|
-
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'sse-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
68
|
-
try {
|
|
69
|
-
await waitForServer();
|
|
70
|
-
await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(dataDir, 'knowledge') } });
|
|
71
|
-
await json('PUT', '/api/ai-profiles', { schema: 'ai-profiles/v1', defaultProfileId: 'test', profiles: [{ id: 'test', name: 'Test', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'replace', value: 'sk-test' } }] });
|
|
72
|
-
const imported = await json('POST', '/api/projects/import', { localPath: repoPath, aiProfileId: 'test' });
|
|
73
|
-
assert.strictEqual(imported.status, 201, JSON.stringify(imported.data));
|
|
74
|
-
|
|
75
|
-
const stream = openSse();
|
|
76
|
-
await waitFor(() => stream.frames.some(frame => frame.event === 'claude/snapshot'));
|
|
77
|
-
const snapshot = stream.frames.find(frame => frame.event === 'claude/snapshot');
|
|
78
|
-
assert(Array.isArray(snapshot.data.sessions));
|
|
79
|
-
|
|
80
|
-
const started = await json('POST', '/api/claude/sessions', { projectId: imported.data.projectId });
|
|
81
|
-
assert.strictEqual(started.status, 201, JSON.stringify(started.data));
|
|
82
|
-
await waitFor(() => stream.frames.some(frame => frame.event === 'claude/sessions-changed' && frame.data.kind === 'create'));
|
|
83
|
-
const created = stream.frames.find(frame => frame.event === 'claude/sessions-changed' && frame.data.kind === 'create');
|
|
84
|
-
assert.strictEqual(created.data.projectSlug, imported.data.projectId);
|
|
85
|
-
const sessions = await json('GET', `/api/claude/sessions?projectId=${encodeURIComponent(imported.data.projectId)}`);
|
|
86
|
-
assert(sessions.data.sessions.some(session => session.sessionId === started.data.sessionId));
|
|
87
|
-
stream.close();
|
|
88
|
-
console.log('sessions-stream-test PASS');
|
|
89
|
-
} finally {
|
|
90
|
-
await spawned.cleanup();
|
|
91
|
-
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
92
|
-
}
|
|
93
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
8
|
+
const freePort = require('./helpers/free-port');
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
+
let PORT, BASE;
|
|
12
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-sse-v2-'));
|
|
13
|
+
const repoPath = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-sse-repo-'));
|
|
14
|
+
|
|
15
|
+
function git(args) {
|
|
16
|
+
const result = spawnSync('git', args, { cwd: repoPath, encoding: 'utf8' });
|
|
17
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function json(method, pathname, body) {
|
|
21
|
+
const response = await fetch(`${BASE}${pathname}`, { method, headers: body === undefined ? undefined : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
22
|
+
return { status: response.status, data: await response.json() };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function waitForServer() {
|
|
26
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
27
|
+
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
29
|
+
}
|
|
30
|
+
throw new Error('server did not start');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openSse() {
|
|
34
|
+
const frames = [];
|
|
35
|
+
let buffer = '';
|
|
36
|
+
const request = http.get(`${BASE}/api/claude/sessions-stream`, response => {
|
|
37
|
+
assert.strictEqual(response.statusCode, 200);
|
|
38
|
+
response.on('data', chunk => {
|
|
39
|
+
buffer += chunk.toString('utf8');
|
|
40
|
+
let boundary;
|
|
41
|
+
while ((boundary = buffer.indexOf('\n\n')) >= 0) {
|
|
42
|
+
const block = buffer.slice(0, boundary);
|
|
43
|
+
buffer = buffer.slice(boundary + 2);
|
|
44
|
+
const event = /^event:\s*(.+)$/m.exec(block);
|
|
45
|
+
const data = /^data:\s*(.+)$/m.exec(block);
|
|
46
|
+
if (event && data) frames.push({ event: event[1], data: JSON.parse(data[1]) });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
request.on('error', error => { frames.push({ event: 'transport-error', data: { message: error.message } }); });
|
|
51
|
+
return { frames, close: () => request.destroy() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitFor(predicate) {
|
|
55
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
56
|
+
if (predicate()) return;
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
58
|
+
}
|
|
59
|
+
throw new Error('timed out waiting for SSE frame');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
(async () => {
|
|
63
|
+
PORT = await freePort(); BASE = `http://127.0.0.1:${PORT}`;
|
|
64
|
+
git(['init', '--initial-branch=main']);
|
|
65
|
+
git(['config', 'user.email', 'sse@example.test']);
|
|
66
|
+
git(['config', 'user.name', 'SSE Test']);
|
|
67
|
+
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'sse-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
68
|
+
try {
|
|
69
|
+
await waitForServer();
|
|
70
|
+
await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(dataDir, 'knowledge') } });
|
|
71
|
+
await json('PUT', '/api/ai-profiles', { schema: 'ai-profiles/v1', defaultProfileId: 'test', profiles: [{ id: 'test', name: 'Test', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'replace', value: 'sk-test' } }] });
|
|
72
|
+
const imported = await json('POST', '/api/projects/import', { localPath: repoPath, aiProfileId: 'test' });
|
|
73
|
+
assert.strictEqual(imported.status, 201, JSON.stringify(imported.data));
|
|
74
|
+
|
|
75
|
+
const stream = openSse();
|
|
76
|
+
await waitFor(() => stream.frames.some(frame => frame.event === 'claude/snapshot'));
|
|
77
|
+
const snapshot = stream.frames.find(frame => frame.event === 'claude/snapshot');
|
|
78
|
+
assert(Array.isArray(snapshot.data.sessions));
|
|
79
|
+
|
|
80
|
+
const started = await json('POST', '/api/claude/sessions', { projectId: imported.data.projectId });
|
|
81
|
+
assert.strictEqual(started.status, 201, JSON.stringify(started.data));
|
|
82
|
+
await waitFor(() => stream.frames.some(frame => frame.event === 'claude/sessions-changed' && frame.data.kind === 'create'));
|
|
83
|
+
const created = stream.frames.find(frame => frame.event === 'claude/sessions-changed' && frame.data.kind === 'create');
|
|
84
|
+
assert.strictEqual(created.data.projectSlug, imported.data.projectId);
|
|
85
|
+
const sessions = await json('GET', `/api/claude/sessions?projectId=${encodeURIComponent(imported.data.projectId)}`);
|
|
86
|
+
assert(sessions.data.sessions.some(session => session.sessionId === started.data.sessionId));
|
|
87
|
+
stream.close();
|
|
88
|
+
console.log('sessions-stream-test PASS');
|
|
89
|
+
} finally {
|
|
90
|
+
await spawned.cleanup();
|
|
91
|
+
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const { createEngineServer } = require('../../_modules/knowledge-engine/src/server');
|
|
7
|
-
const { SettingsStore } = require('../lib/settings-store');
|
|
8
|
-
const { ModuleBridge } = require('../lib/module-bridge');
|
|
9
|
-
|
|
10
|
-
(async () => {
|
|
11
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-shared-bindings-'));
|
|
12
|
-
const oldRoot = path.join(root, '原知识根目录'), newRoot = path.join(root, '新知识根目录');
|
|
13
|
-
const settings = new SettingsStore({ dataDir: path.join(root, 'shell') });
|
|
14
|
-
await settings.initialize({ knowledge: { rootPath: oldRoot } });
|
|
15
|
-
await settings.addDomain({ id: 'domain-tools', name: '开发工具' });
|
|
16
|
-
let engine;
|
|
17
|
-
async function start() {
|
|
18
|
-
engine = createEngineServer({ dataDir: path.join(root, 'engine'), consume: false, runtimeToken: 'scope-binding-test' });
|
|
19
|
-
await engine.ready;
|
|
20
|
-
await new Promise(resolve => engine.server.listen(0, '127.0.0.1', resolve));
|
|
21
|
-
return `http://127.0.0.1:${engine.server.address().port}`;
|
|
22
|
-
}
|
|
23
|
-
const logs = [];
|
|
24
|
-
const bridge = new ModuleBridge({ settingsStore: settings, runtimeToken: 'scope-binding-test', logger: { warn: (...args) => logs.push(args) } });
|
|
25
|
-
const upstream = bridge._fetch.bind(bridge);
|
|
26
|
-
bridge._fetch = (url, init) => url.startsWith(bridge.engineUrl) ? upstream(url, init) : Promise.resolve({ ok: false });
|
|
27
|
-
try {
|
|
28
|
-
bridge.engineUrl = await start();
|
|
29
|
-
assert.equal((await bridge.syncEngineConfiguration()).scopes, true);
|
|
30
|
-
const scopes = structuredClone(engine.store.state.sharedScopes);
|
|
31
|
-
const brain = scopes.find(scope => scope.kind === 'brain');
|
|
32
|
-
const domain = scopes.find(scope => scope.kind === 'domain');
|
|
33
|
-
engine.store.addProject({ projectId: 'project-test', repoPath: path.join(root, 'code'), knowledgePath: path.join(root, 'project-knowledge'), domainId: domain.id });
|
|
34
|
-
engine.store.state.memories.push({ id: 'memory-test', brainId: brain.id, status: 'candidate', text: '保持目录绑定稳定', evidence: { projectId: 'project-test', commitSha: 'a'.repeat(40), entryId: 'README.md' } });
|
|
35
|
-
engine.store.save();
|
|
36
|
-
await engine.coordinator.memories.update('memory-test', { status: 'confirmed' });
|
|
37
|
-
fs.writeFileSync(path.join(domain.knowledgePath, 'README.md'), '# 领域经验\n\n领域知识不能随根目录设置隐式移动。\n');
|
|
38
|
-
const memoryFile = engine.coordinator.memories.file(brain);
|
|
39
|
-
const before = fs.readFileSync(memoryFile);
|
|
40
|
-
await settings.updatePatch({ knowledge: { rootPath: newRoot } });
|
|
41
|
-
assert.equal((await bridge.syncEngineConfiguration()).scopes, true, 'changing index root must not invalidate shared memory wiring');
|
|
42
|
-
assert.deepEqual(engine.store.state.sharedScopes.map(scope => scope.knowledgePath), scopes.map(scope => scope.knowledgePath));
|
|
43
|
-
assert.deepEqual(fs.readFileSync(memoryFile), before);
|
|
44
|
-
assert.equal(settings.read().brain.knowledgePath, brain.knowledgePath);
|
|
45
|
-
assert.equal(settings.read().brain.domains[0].knowledgePath, domain.knowledgePath);
|
|
46
|
-
await assert.rejects(settings.updatePatch({ brain: { knowledgePath: newRoot } }), { code: 'INVALID_ARGUMENT' });
|
|
47
|
-
await assert.rejects(settings.bindSharedKnowledgePaths([{ ...brain, knowledgePath: newRoot }]), { code: 'IMMUTABLE_FIELD' });
|
|
48
|
-
await settings.addDomain({ id: 'domain-new', name: '新领域' });
|
|
49
|
-
assert.equal((await bridge.syncEngineConfiguration()).scopes, true);
|
|
50
|
-
assert.equal(engine.store.state.sharedScopes.find(scope => scope.id === 'domain-new').knowledgePath, path.join(newRoot, '_shared', 'domain-new'));
|
|
51
|
-
await engine.close();
|
|
52
|
-
bridge.engineUrl = await start();
|
|
53
|
-
assert.equal((await bridge.syncEngineConfiguration()).scopes, true);
|
|
54
|
-
assert.deepEqual(fs.readFileSync(memoryFile), before);
|
|
55
|
-
const memory = await engine.coordinator.retrieval.get({ projectId: 'project-test', spaceId: 'brain:brain-default', entryId: 'memories/confirmed.md' });
|
|
56
|
-
assert.ok(memory.chunks.some(chunk => chunk.chunk_text.includes('保持目录绑定稳定')));
|
|
57
|
-
const shared = await engine.coordinator.retrieval.get({ projectId: 'project-test', spaceId: 'domain:domain-tools', entryId: 'README.md' });
|
|
58
|
-
assert.ok(shared.chunks.some(chunk => chunk.chunk_text.includes('不能随根目录设置隐式移动')));
|
|
59
|
-
await engine.close(); engine = null;
|
|
60
|
-
const pending = await bridge.syncEngineConfiguration();
|
|
61
|
-
assert.equal(pending.scopes, false);
|
|
62
|
-
assert.equal(pending.scopeStatus.status, 'pending');
|
|
63
|
-
assert.ok(logs.some(entry => entry[0] === 'modules.scopes.pending'));
|
|
64
|
-
assert.equal(settings.read().brain.knowledgePath, brain.knowledgePath);
|
|
65
|
-
console.log('shared-scope-root-binding-test PASS');
|
|
66
|
-
} finally { if (engine) await engine.close(); }
|
|
67
|
-
})().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 os = require('node:os');
|
|
6
|
+
const { createEngineServer } = require('../../_modules/knowledge-engine/src/server');
|
|
7
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
8
|
+
const { ModuleBridge } = require('../lib/module-bridge');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-shared-bindings-'));
|
|
12
|
+
const oldRoot = path.join(root, '原知识根目录'), newRoot = path.join(root, '新知识根目录');
|
|
13
|
+
const settings = new SettingsStore({ dataDir: path.join(root, 'shell') });
|
|
14
|
+
await settings.initialize({ knowledge: { rootPath: oldRoot } });
|
|
15
|
+
await settings.addDomain({ id: 'domain-tools', name: '开发工具' });
|
|
16
|
+
let engine;
|
|
17
|
+
async function start() {
|
|
18
|
+
engine = createEngineServer({ dataDir: path.join(root, 'engine'), consume: false, runtimeToken: 'scope-binding-test' });
|
|
19
|
+
await engine.ready;
|
|
20
|
+
await new Promise(resolve => engine.server.listen(0, '127.0.0.1', resolve));
|
|
21
|
+
return `http://127.0.0.1:${engine.server.address().port}`;
|
|
22
|
+
}
|
|
23
|
+
const logs = [];
|
|
24
|
+
const bridge = new ModuleBridge({ settingsStore: settings, runtimeToken: 'scope-binding-test', logger: { warn: (...args) => logs.push(args) } });
|
|
25
|
+
const upstream = bridge._fetch.bind(bridge);
|
|
26
|
+
bridge._fetch = (url, init) => url.startsWith(bridge.engineUrl) ? upstream(url, init) : Promise.resolve({ ok: false });
|
|
27
|
+
try {
|
|
28
|
+
bridge.engineUrl = await start();
|
|
29
|
+
assert.equal((await bridge.syncEngineConfiguration()).scopes, true);
|
|
30
|
+
const scopes = structuredClone(engine.store.state.sharedScopes);
|
|
31
|
+
const brain = scopes.find(scope => scope.kind === 'brain');
|
|
32
|
+
const domain = scopes.find(scope => scope.kind === 'domain');
|
|
33
|
+
engine.store.addProject({ projectId: 'project-test', repoPath: path.join(root, 'code'), knowledgePath: path.join(root, 'project-knowledge'), domainId: domain.id });
|
|
34
|
+
engine.store.state.memories.push({ id: 'memory-test', brainId: brain.id, status: 'candidate', text: '保持目录绑定稳定', evidence: { projectId: 'project-test', commitSha: 'a'.repeat(40), entryId: 'README.md' } });
|
|
35
|
+
engine.store.save();
|
|
36
|
+
await engine.coordinator.memories.update('memory-test', { status: 'confirmed' });
|
|
37
|
+
fs.writeFileSync(path.join(domain.knowledgePath, 'README.md'), '# 领域经验\n\n领域知识不能随根目录设置隐式移动。\n');
|
|
38
|
+
const memoryFile = engine.coordinator.memories.file(brain);
|
|
39
|
+
const before = fs.readFileSync(memoryFile);
|
|
40
|
+
await settings.updatePatch({ knowledge: { rootPath: newRoot } });
|
|
41
|
+
assert.equal((await bridge.syncEngineConfiguration()).scopes, true, 'changing index root must not invalidate shared memory wiring');
|
|
42
|
+
assert.deepEqual(engine.store.state.sharedScopes.map(scope => scope.knowledgePath), scopes.map(scope => scope.knowledgePath));
|
|
43
|
+
assert.deepEqual(fs.readFileSync(memoryFile), before);
|
|
44
|
+
assert.equal(settings.read().brain.knowledgePath, brain.knowledgePath);
|
|
45
|
+
assert.equal(settings.read().brain.domains[0].knowledgePath, domain.knowledgePath);
|
|
46
|
+
await assert.rejects(settings.updatePatch({ brain: { knowledgePath: newRoot } }), { code: 'INVALID_ARGUMENT' });
|
|
47
|
+
await assert.rejects(settings.bindSharedKnowledgePaths([{ ...brain, knowledgePath: newRoot }]), { code: 'IMMUTABLE_FIELD' });
|
|
48
|
+
await settings.addDomain({ id: 'domain-new', name: '新领域' });
|
|
49
|
+
assert.equal((await bridge.syncEngineConfiguration()).scopes, true);
|
|
50
|
+
assert.equal(engine.store.state.sharedScopes.find(scope => scope.id === 'domain-new').knowledgePath, path.join(newRoot, '_shared', 'domain-new'));
|
|
51
|
+
await engine.close();
|
|
52
|
+
bridge.engineUrl = await start();
|
|
53
|
+
assert.equal((await bridge.syncEngineConfiguration()).scopes, true);
|
|
54
|
+
assert.deepEqual(fs.readFileSync(memoryFile), before);
|
|
55
|
+
const memory = await engine.coordinator.retrieval.get({ projectId: 'project-test', spaceId: 'brain:brain-default', entryId: 'memories/confirmed.md' });
|
|
56
|
+
assert.ok(memory.chunks.some(chunk => chunk.chunk_text.includes('保持目录绑定稳定')));
|
|
57
|
+
const shared = await engine.coordinator.retrieval.get({ projectId: 'project-test', spaceId: 'domain:domain-tools', entryId: 'README.md' });
|
|
58
|
+
assert.ok(shared.chunks.some(chunk => chunk.chunk_text.includes('不能随根目录设置隐式移动')));
|
|
59
|
+
await engine.close(); engine = null;
|
|
60
|
+
const pending = await bridge.syncEngineConfiguration();
|
|
61
|
+
assert.equal(pending.scopes, false);
|
|
62
|
+
assert.equal(pending.scopeStatus.status, 'pending');
|
|
63
|
+
assert.ok(logs.some(entry => entry[0] === 'modules.scopes.pending'));
|
|
64
|
+
assert.equal(settings.read().brain.knowledgePath, brain.knowledgePath);
|
|
65
|
+
console.log('shared-scope-root-binding-test PASS');
|
|
66
|
+
} finally { if (engine) await engine.close(); }
|
|
67
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|