@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,109 +1,109 @@
|
|
|
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 { spawn } = require('node:child_process');
|
|
8
|
-
const { StorageLayout } = require('../lib/storage-layout');
|
|
9
|
-
const { SettingsStore } = require('../lib/settings-store');
|
|
10
|
-
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
11
|
-
const { ProjectStore } = require('../lib/project-store');
|
|
12
|
-
const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
|
|
13
|
-
const endpoint = require('../lib/runtime-endpoint');
|
|
14
|
-
const control = require('../lib/runtime-control');
|
|
15
|
-
const { EngineKnowledgeClient } = require('../lib/engine-knowledge-client');
|
|
16
|
-
|
|
17
|
-
(async () => {
|
|
18
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-engine-client-'));
|
|
19
|
-
const layout = new StorageLayout({ dataDir: path.join(root, 'data') });
|
|
20
|
-
const settings = new SettingsStore({ layout }), registry = new ProjectRegistryStore({ layout }), projects = new ProjectStore({ layout });
|
|
21
|
-
const projectId = 'project-test', repoPath = path.join(root, 'repo'), knowledgePath = path.join(root, 'knowledge');
|
|
22
|
-
fs.mkdirSync(repoPath); fs.mkdirSync(knowledgePath);
|
|
23
|
-
fs.writeFileSync(path.join(knowledgePath, 'README.md'), '# Legacy\n\nlegacy-local-only\n');
|
|
24
|
-
await settings.initialize(); await registry.initialize();
|
|
25
|
-
await projects.create(projectId, { repoPath, knowledgePath }, { modules: { engine: { status: 'registered', projectId } } });
|
|
26
|
-
await projects.updateState(projectId, state => { state.modules = { engine: { status: 'registered', projectId } }; });
|
|
27
|
-
await registry.add(projectId, { displayNameSnapshot: 'Test' });
|
|
28
|
-
let invalidIdentity = false, malformed = false, businessError = false, oversized = false, wrongBinding = false, scopeChanged = false;
|
|
29
|
-
const calls = [];
|
|
30
|
-
const reference = { scope_project_id: projectId, entry_id: 'changes/aaaaaaaaaaaa.md', commitSha: 'a'.repeat(40), chunk_text: 'Engine authoritative knowledge' };
|
|
31
|
-
const payload = { ok: true, projectId, backend: 'hybrid+markdown-truth', results: [reference], answer: reference.chunk_text, citations: [{ ...reference, entryId: reference.entry_id }], chunks: [{ chunk_text: reference.chunk_text }] };
|
|
32
|
-
const server = http.createServer(async (req, res) => {
|
|
33
|
-
res.setHeader('Content-Type', 'application/json');
|
|
34
|
-
if (req.url === '/api/runtime/identity') return res.end(JSON.stringify(control.identity(invalidIdentity ? root : layout.getDataDir(), { port: server.address().port, host: '127.0.0.1', startedAt: 'test-start' })));
|
|
35
|
-
if (req.url === '/api/health') return res.end(JSON.stringify({ name: 'knowledge-engine', dataSchemaVersion: 3 }));
|
|
36
|
-
if (req.url === '/api/projects') return res.end(JSON.stringify({ projects: [{ projectId, repoPath, knowledgePath: wrongBinding ? root : knowledgePath }] }));
|
|
37
|
-
let text = ''; for await (const bytes of req) text += bytes;
|
|
38
|
-
calls.push({ route: req.url, input: JSON.parse(text || '{}') });
|
|
39
|
-
if (oversized) return res.end(JSON.stringify({ ok: true, text: 'x'.repeat(2 * 1024 * 1024) }));
|
|
40
|
-
if (scopeChanged) { res.statusCode = 409; return res.end(JSON.stringify({ ok: false, error: { code: 'PROJECT_SCOPE_CHANGED' } })); }
|
|
41
|
-
if (businessError) { res.statusCode = 403; return res.end(JSON.stringify({ ok: false, error: { code: 'PATH_OUTSIDE_ROOT', message: 'private-test-secret' } })); }
|
|
42
|
-
res.end(malformed ? '[]' : JSON.stringify(payload));
|
|
43
|
-
});
|
|
44
|
-
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
45
|
-
endpoint.writeEndpoint(layout.getDataDir(), { pid: process.pid, port: server.address().port, host: '127.0.0.1', startedAt: 'test-start' });
|
|
46
|
-
const runtime = new KnowledgeToolRuntime({ layout, settingsStore: settings, registryStore: registry, projectStore: projects });
|
|
47
|
-
try {
|
|
48
|
-
for (const action of ['search', 'ask', 'get', 'history']) {
|
|
49
|
-
assert.deepEqual(await runtime[action]({ project: projectId, query: 'knowledge', entry: reference.entry_id, scope: 'brain', apiKey: 'must-not-forward', password: 'must-not-forward' }), payload);
|
|
50
|
-
const call = calls.at(-1);
|
|
51
|
-
assert.equal(call.route, `/api/engine/${action}`);
|
|
52
|
-
assert.equal(call.input.projectId, projectId); assert.equal(call.input.scope, 'brain');
|
|
53
|
-
assert.ok(!JSON.stringify(call.input).includes('must-not-forward'));
|
|
54
|
-
}
|
|
55
|
-
const subprocess = (file, args) => new Promise((resolve, reject) => {
|
|
56
|
-
const child = spawn(process.execPath, [file, ...args], { env: { ...process.env, KB_DATA_DIR: layout.getDataDir() }, windowsHide: true });
|
|
57
|
-
let out = '', err = ''; child.stdout.on('data', b => { out += b; }); child.stderr.on('data', b => { err += b; });
|
|
58
|
-
child.once('error', reject); child.once('close', code => resolve({ code, out, err }));
|
|
59
|
-
});
|
|
60
|
-
const cli = await subprocess(path.resolve(__dirname, '../../bin/project-knowledge-kb.js'), ['search', '--project', projectId, '--query', 'knowledge', '--scope', 'brain', '--json']);
|
|
61
|
-
assert.equal(cli.code, 0, cli.err); assert.deepEqual(JSON.parse(cli.out), payload);
|
|
62
|
-
assert.equal(calls.at(-1).input.scope, 'brain');
|
|
63
|
-
await runtime.get({ projectId, entry: reference.entry_id, space: 'brain:test' });
|
|
64
|
-
assert.equal(calls.at(-1).input.spaceId, 'brain:test', 'legacy space alias is forwarded');
|
|
65
|
-
const { createRequire } = require('node:module');
|
|
66
|
-
const sdk = createRequire(path.resolve(__dirname, '../../package.json'));
|
|
67
|
-
const { Client } = sdk('@modelcontextprotocol/sdk/client/index.js');
|
|
68
|
-
const { StdioClientTransport } = sdk('@modelcontextprotocol/sdk/client/stdio.js');
|
|
69
|
-
const transport = new StdioClientTransport({ command: process.execPath, args: [path.resolve(__dirname, '../../bin/project-knowledge-mcp.js')], env: { ...process.env, KB_DATA_DIR: layout.getDataDir() }, stderr: 'pipe' });
|
|
70
|
-
const client = new Client({ name: 'engine-regression', version: '1.0.0' });
|
|
71
|
-
try {
|
|
72
|
-
await client.connect(transport);
|
|
73
|
-
const tools = await client.listTools();
|
|
74
|
-
assert.deepEqual(tools.tools.find(t => t.name === 'project_knowledge_search').inputSchema.properties.scope.enum, ['project', 'domain', 'brain']);
|
|
75
|
-
for (const action of ['search', 'ask', 'get', 'history']) {
|
|
76
|
-
const result = await client.callTool({ name: `project_knowledge_${action}`, arguments: { projectId, scope: 'brain', ...(action === 'get' ? { entry: reference.entry_id, space: 'brain:test' } : action === 'history' ? {} : { query: 'knowledge' }) } });
|
|
77
|
-
assert.ok(!result.isError); assert.deepEqual(JSON.parse(result.content[0].text), payload);
|
|
78
|
-
assert.equal(calls.at(-1).input.scope, 'brain');
|
|
79
|
-
}
|
|
80
|
-
} finally { await client.close(); }
|
|
81
|
-
invalidIdentity = true;
|
|
82
|
-
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'ENGINE_IDENTITY_MISMATCH');
|
|
83
|
-
invalidIdentity = false; malformed = true;
|
|
84
|
-
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'ENGINE_INVALID_RESPONSE');
|
|
85
|
-
malformed = false; businessError = true;
|
|
86
|
-
await assert.rejects(runtime.get({ projectId, entry: '../outside.md' }), e => e.code === 'PATH_OUTSIDE_ROOT' && !e.message.includes('private-test-secret'));
|
|
87
|
-
businessError = false; oversized = true;
|
|
88
|
-
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'ENGINE_INVALID_RESPONSE');
|
|
89
|
-
oversized = false;
|
|
90
|
-
scopeChanged = true;
|
|
91
|
-
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'PROJECT_SCOPE_CHANGED' && e.status === 409);
|
|
92
|
-
scopeChanged = false;
|
|
93
|
-
const direct = new EngineKnowledgeClient({ layout, engineUrl: `http://127.0.0.1:${server.address().port}` });
|
|
94
|
-
assert.deepEqual(await direct.request('search', {}, runtime.resolveProject({ projectId })), payload);
|
|
95
|
-
assert.equal(calls.at(-1).route, '/api/search');
|
|
96
|
-
wrongBinding = true;
|
|
97
|
-
await assert.rejects(direct.request('search', {}, runtime.resolveProject({ projectId })), e => e.code === 'ENGINE_IDENTITY_MISMATCH');
|
|
98
|
-
wrongBinding = false;
|
|
99
|
-
for (const engineUrl of ['http://example.com', 'http://secret@127.0.0.1', 'http://127.0.0.1/?apiKey=secret', 'file:///private']) {
|
|
100
|
-
const unsafe = new EngineKnowledgeClient({ layout, engineUrl });
|
|
101
|
-
await assert.rejects(unsafe.request('search', {}, runtime.resolveProject({ projectId })), e => e.code === 'ENGINE_URL_INVALID' && !e.message.includes('secret'));
|
|
102
|
-
}
|
|
103
|
-
await assert.rejects(direct.request('../runtime-config', {}, {}), e => e.code === 'INVALID_ARGUMENT');
|
|
104
|
-
await assert.rejects(runtime.search({ projectId, scope: 'another-brain' }), e => e.code === 'INVALID_ARGUMENT');
|
|
105
|
-
const unavailable = new EngineKnowledgeClient({ layout, fetchImpl: async () => { throw new Error('private-test-secret'); } });
|
|
106
|
-
await assert.rejects(unavailable.request('search', {}, runtime.resolveProject({ projectId })), e => e.code === 'ENGINE_UNAVAILABLE' && !e.message.includes('private-test-secret'));
|
|
107
|
-
console.log('knowledge-engine-client-test PASS (native runtime, CLI and MCP share engine facts)');
|
|
108
|
-
} finally { await runtime.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }
|
|
109
|
-
})().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 os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const http = require('node:http');
|
|
7
|
+
const { spawn } = require('node:child_process');
|
|
8
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
9
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
10
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
11
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
12
|
+
const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
|
|
13
|
+
const endpoint = require('../lib/runtime-endpoint');
|
|
14
|
+
const control = require('../lib/runtime-control');
|
|
15
|
+
const { EngineKnowledgeClient } = require('../lib/engine-knowledge-client');
|
|
16
|
+
|
|
17
|
+
(async () => {
|
|
18
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-engine-client-'));
|
|
19
|
+
const layout = new StorageLayout({ dataDir: path.join(root, 'data') });
|
|
20
|
+
const settings = new SettingsStore({ layout }), registry = new ProjectRegistryStore({ layout }), projects = new ProjectStore({ layout });
|
|
21
|
+
const projectId = 'project-test', repoPath = path.join(root, 'repo'), knowledgePath = path.join(root, 'knowledge');
|
|
22
|
+
fs.mkdirSync(repoPath); fs.mkdirSync(knowledgePath);
|
|
23
|
+
fs.writeFileSync(path.join(knowledgePath, 'README.md'), '# Legacy\n\nlegacy-local-only\n');
|
|
24
|
+
await settings.initialize(); await registry.initialize();
|
|
25
|
+
await projects.create(projectId, { repoPath, knowledgePath }, { modules: { engine: { status: 'registered', projectId } } });
|
|
26
|
+
await projects.updateState(projectId, state => { state.modules = { engine: { status: 'registered', projectId } }; });
|
|
27
|
+
await registry.add(projectId, { displayNameSnapshot: 'Test' });
|
|
28
|
+
let invalidIdentity = false, malformed = false, businessError = false, oversized = false, wrongBinding = false, scopeChanged = false;
|
|
29
|
+
const calls = [];
|
|
30
|
+
const reference = { scope_project_id: projectId, entry_id: 'changes/aaaaaaaaaaaa.md', commitSha: 'a'.repeat(40), chunk_text: 'Engine authoritative knowledge' };
|
|
31
|
+
const payload = { ok: true, projectId, backend: 'hybrid+markdown-truth', results: [reference], answer: reference.chunk_text, citations: [{ ...reference, entryId: reference.entry_id }], chunks: [{ chunk_text: reference.chunk_text }] };
|
|
32
|
+
const server = http.createServer(async (req, res) => {
|
|
33
|
+
res.setHeader('Content-Type', 'application/json');
|
|
34
|
+
if (req.url === '/api/runtime/identity') return res.end(JSON.stringify(control.identity(invalidIdentity ? root : layout.getDataDir(), { port: server.address().port, host: '127.0.0.1', startedAt: 'test-start' })));
|
|
35
|
+
if (req.url === '/api/health') return res.end(JSON.stringify({ name: 'knowledge-engine', dataSchemaVersion: 3 }));
|
|
36
|
+
if (req.url === '/api/projects') return res.end(JSON.stringify({ projects: [{ projectId, repoPath, knowledgePath: wrongBinding ? root : knowledgePath }] }));
|
|
37
|
+
let text = ''; for await (const bytes of req) text += bytes;
|
|
38
|
+
calls.push({ route: req.url, input: JSON.parse(text || '{}') });
|
|
39
|
+
if (oversized) return res.end(JSON.stringify({ ok: true, text: 'x'.repeat(2 * 1024 * 1024) }));
|
|
40
|
+
if (scopeChanged) { res.statusCode = 409; return res.end(JSON.stringify({ ok: false, error: { code: 'PROJECT_SCOPE_CHANGED' } })); }
|
|
41
|
+
if (businessError) { res.statusCode = 403; return res.end(JSON.stringify({ ok: false, error: { code: 'PATH_OUTSIDE_ROOT', message: 'private-test-secret' } })); }
|
|
42
|
+
res.end(malformed ? '[]' : JSON.stringify(payload));
|
|
43
|
+
});
|
|
44
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
45
|
+
endpoint.writeEndpoint(layout.getDataDir(), { pid: process.pid, port: server.address().port, host: '127.0.0.1', startedAt: 'test-start' });
|
|
46
|
+
const runtime = new KnowledgeToolRuntime({ layout, settingsStore: settings, registryStore: registry, projectStore: projects });
|
|
47
|
+
try {
|
|
48
|
+
for (const action of ['search', 'ask', 'get', 'history']) {
|
|
49
|
+
assert.deepEqual(await runtime[action]({ project: projectId, query: 'knowledge', entry: reference.entry_id, scope: 'brain', apiKey: 'must-not-forward', password: 'must-not-forward' }), payload);
|
|
50
|
+
const call = calls.at(-1);
|
|
51
|
+
assert.equal(call.route, `/api/engine/${action}`);
|
|
52
|
+
assert.equal(call.input.projectId, projectId); assert.equal(call.input.scope, 'brain');
|
|
53
|
+
assert.ok(!JSON.stringify(call.input).includes('must-not-forward'));
|
|
54
|
+
}
|
|
55
|
+
const subprocess = (file, args) => new Promise((resolve, reject) => {
|
|
56
|
+
const child = spawn(process.execPath, [file, ...args], { env: { ...process.env, KB_DATA_DIR: layout.getDataDir() }, windowsHide: true });
|
|
57
|
+
let out = '', err = ''; child.stdout.on('data', b => { out += b; }); child.stderr.on('data', b => { err += b; });
|
|
58
|
+
child.once('error', reject); child.once('close', code => resolve({ code, out, err }));
|
|
59
|
+
});
|
|
60
|
+
const cli = await subprocess(path.resolve(__dirname, '../../bin/project-knowledge-kb.js'), ['search', '--project', projectId, '--query', 'knowledge', '--scope', 'brain', '--json']);
|
|
61
|
+
assert.equal(cli.code, 0, cli.err); assert.deepEqual(JSON.parse(cli.out), payload);
|
|
62
|
+
assert.equal(calls.at(-1).input.scope, 'brain');
|
|
63
|
+
await runtime.get({ projectId, entry: reference.entry_id, space: 'brain:test' });
|
|
64
|
+
assert.equal(calls.at(-1).input.spaceId, 'brain:test', 'legacy space alias is forwarded');
|
|
65
|
+
const { createRequire } = require('node:module');
|
|
66
|
+
const sdk = createRequire(path.resolve(__dirname, '../../package.json'));
|
|
67
|
+
const { Client } = sdk('@modelcontextprotocol/sdk/client/index.js');
|
|
68
|
+
const { StdioClientTransport } = sdk('@modelcontextprotocol/sdk/client/stdio.js');
|
|
69
|
+
const transport = new StdioClientTransport({ command: process.execPath, args: [path.resolve(__dirname, '../../bin/project-knowledge-mcp.js')], env: { ...process.env, KB_DATA_DIR: layout.getDataDir() }, stderr: 'pipe' });
|
|
70
|
+
const client = new Client({ name: 'engine-regression', version: '1.0.0' });
|
|
71
|
+
try {
|
|
72
|
+
await client.connect(transport);
|
|
73
|
+
const tools = await client.listTools();
|
|
74
|
+
assert.deepEqual(tools.tools.find(t => t.name === 'project_knowledge_search').inputSchema.properties.scope.enum, ['project', 'domain', 'brain']);
|
|
75
|
+
for (const action of ['search', 'ask', 'get', 'history']) {
|
|
76
|
+
const result = await client.callTool({ name: `project_knowledge_${action}`, arguments: { projectId, scope: 'brain', ...(action === 'get' ? { entry: reference.entry_id, space: 'brain:test' } : action === 'history' ? {} : { query: 'knowledge' }) } });
|
|
77
|
+
assert.ok(!result.isError); assert.deepEqual(JSON.parse(result.content[0].text), payload);
|
|
78
|
+
assert.equal(calls.at(-1).input.scope, 'brain');
|
|
79
|
+
}
|
|
80
|
+
} finally { await client.close(); }
|
|
81
|
+
invalidIdentity = true;
|
|
82
|
+
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'ENGINE_IDENTITY_MISMATCH');
|
|
83
|
+
invalidIdentity = false; malformed = true;
|
|
84
|
+
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'ENGINE_INVALID_RESPONSE');
|
|
85
|
+
malformed = false; businessError = true;
|
|
86
|
+
await assert.rejects(runtime.get({ projectId, entry: '../outside.md' }), e => e.code === 'PATH_OUTSIDE_ROOT' && !e.message.includes('private-test-secret'));
|
|
87
|
+
businessError = false; oversized = true;
|
|
88
|
+
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'ENGINE_INVALID_RESPONSE');
|
|
89
|
+
oversized = false;
|
|
90
|
+
scopeChanged = true;
|
|
91
|
+
await assert.rejects(runtime.search({ projectId, query: 'knowledge' }), e => e.code === 'PROJECT_SCOPE_CHANGED' && e.status === 409);
|
|
92
|
+
scopeChanged = false;
|
|
93
|
+
const direct = new EngineKnowledgeClient({ layout, engineUrl: `http://127.0.0.1:${server.address().port}` });
|
|
94
|
+
assert.deepEqual(await direct.request('search', {}, runtime.resolveProject({ projectId })), payload);
|
|
95
|
+
assert.equal(calls.at(-1).route, '/api/search');
|
|
96
|
+
wrongBinding = true;
|
|
97
|
+
await assert.rejects(direct.request('search', {}, runtime.resolveProject({ projectId })), e => e.code === 'ENGINE_IDENTITY_MISMATCH');
|
|
98
|
+
wrongBinding = false;
|
|
99
|
+
for (const engineUrl of ['http://example.com', 'http://secret@127.0.0.1', 'http://127.0.0.1/?apiKey=secret', 'file:///private']) {
|
|
100
|
+
const unsafe = new EngineKnowledgeClient({ layout, engineUrl });
|
|
101
|
+
await assert.rejects(unsafe.request('search', {}, runtime.resolveProject({ projectId })), e => e.code === 'ENGINE_URL_INVALID' && !e.message.includes('secret'));
|
|
102
|
+
}
|
|
103
|
+
await assert.rejects(direct.request('../runtime-config', {}, {}), e => e.code === 'INVALID_ARGUMENT');
|
|
104
|
+
await assert.rejects(runtime.search({ projectId, scope: 'another-brain' }), e => e.code === 'INVALID_ARGUMENT');
|
|
105
|
+
const unavailable = new EngineKnowledgeClient({ layout, fetchImpl: async () => { throw new Error('private-test-secret'); } });
|
|
106
|
+
await assert.rejects(unavailable.request('search', {}, runtime.resolveProject({ projectId })), e => e.code === 'ENGINE_UNAVAILABLE' && !e.message.includes('private-test-secret'));
|
|
107
|
+
console.log('knowledge-engine-client-test PASS (native runtime, CLI and MCP share engine facts)');
|
|
108
|
+
} finally { await runtime.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); }
|
|
109
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
-
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
7
|
-
const { ProjectStore } = require('../lib/project-store');
|
|
8
|
-
const { IndexService } = require('../lib/index-service');
|
|
9
|
-
|
|
10
|
-
(async () => {
|
|
11
|
-
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-maintenance-v2-'));
|
|
12
|
-
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
13
|
-
const registry = new ProjectRegistryStore({ layout });
|
|
14
|
-
const projects = new ProjectStore({ layout });
|
|
15
|
-
await registry.initialize();
|
|
16
|
-
const target = layout.getIndexPath();
|
|
17
|
-
fs.mkdirSync(target, { recursive: true });
|
|
18
|
-
fs.writeFileSync(path.join(target, 'version.txt'), 'old-index', 'utf8');
|
|
19
|
-
const adapter = {
|
|
20
|
-
validationOk: false,
|
|
21
|
-
async buildFull({ targetPath }) {
|
|
22
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
23
|
-
fs.writeFileSync(path.join(targetPath, 'version.txt'), 'new-index', 'utf8');
|
|
24
|
-
},
|
|
25
|
-
async validateIndex(candidate) {
|
|
26
|
-
return { ok: this.validationOk, marker: fs.readFileSync(path.join(candidate, 'version.txt'), 'utf8') };
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
const service = new IndexService({ layout, registryStore: registry, projectStore: projects, adapter });
|
|
30
|
-
await assert.rejects(service.fullRebuild({ operationId: 'op-validation-failure' }), error => error.code === 'DATA_CORRUPT');
|
|
31
|
-
assert.strictEqual(fs.readFileSync(path.join(target, 'version.txt'), 'utf8'), 'old-index', 'failed validation must leave the live index untouched');
|
|
32
|
-
assert(!fs.existsSync(path.join(path.dirname(target), '.knowledge.rebuild.op-validation-failure')));
|
|
33
|
-
|
|
34
|
-
adapter.validationOk = true;
|
|
35
|
-
const rebuilt = await service.fullRebuild({ operationId: 'op-valid-rebuild' });
|
|
36
|
-
assert.strictEqual(fs.readFileSync(path.join(target, 'version.txt'), 'utf8'), 'new-index');
|
|
37
|
-
assert(rebuilt.backup && fs.existsSync(rebuilt.backup));
|
|
38
|
-
assert.strictEqual(fs.readFileSync(path.join(rebuilt.backup, 'version.txt'), 'utf8'), 'old-index', 'atomic swap must retain the previous live index');
|
|
39
|
-
console.log('knowledge-maintenance-test: PASS');
|
|
40
|
-
})().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 { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
7
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
8
|
+
const { IndexService } = require('../lib/index-service');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-maintenance-v2-'));
|
|
12
|
+
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
13
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
14
|
+
const projects = new ProjectStore({ layout });
|
|
15
|
+
await registry.initialize();
|
|
16
|
+
const target = layout.getIndexPath();
|
|
17
|
+
fs.mkdirSync(target, { recursive: true });
|
|
18
|
+
fs.writeFileSync(path.join(target, 'version.txt'), 'old-index', 'utf8');
|
|
19
|
+
const adapter = {
|
|
20
|
+
validationOk: false,
|
|
21
|
+
async buildFull({ targetPath }) {
|
|
22
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(targetPath, 'version.txt'), 'new-index', 'utf8');
|
|
24
|
+
},
|
|
25
|
+
async validateIndex(candidate) {
|
|
26
|
+
return { ok: this.validationOk, marker: fs.readFileSync(path.join(candidate, 'version.txt'), 'utf8') };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const service = new IndexService({ layout, registryStore: registry, projectStore: projects, adapter });
|
|
30
|
+
await assert.rejects(service.fullRebuild({ operationId: 'op-validation-failure' }), error => error.code === 'DATA_CORRUPT');
|
|
31
|
+
assert.strictEqual(fs.readFileSync(path.join(target, 'version.txt'), 'utf8'), 'old-index', 'failed validation must leave the live index untouched');
|
|
32
|
+
assert(!fs.existsSync(path.join(path.dirname(target), '.knowledge.rebuild.op-validation-failure')));
|
|
33
|
+
|
|
34
|
+
adapter.validationOk = true;
|
|
35
|
+
const rebuilt = await service.fullRebuild({ operationId: 'op-valid-rebuild' });
|
|
36
|
+
assert.strictEqual(fs.readFileSync(path.join(target, 'version.txt'), 'utf8'), 'new-index');
|
|
37
|
+
assert(rebuilt.backup && fs.existsSync(rebuilt.backup));
|
|
38
|
+
assert.strictEqual(fs.readFileSync(path.join(rebuilt.backup, 'version.txt'), 'utf8'), 'old-index', 'atomic swap must retain the previous live index');
|
|
39
|
+
console.log('knowledge-maintenance-test: PASS');
|
|
40
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// K1 knowledge panel contract: the shell registers a same-origin provider with
|
|
3
|
+
// the workbench, streams structured references, folds the fixed column only
|
|
4
|
+
// after a version-matched panel ack, restores it on any failure, and the panel
|
|
5
|
+
// page itself renders external strings through textContent only.
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const root = path.resolve(__dirname, '../..');
|
|
10
|
+
const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
11
|
+
const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
|
|
12
|
+
const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
|
|
13
|
+
const panelPath = path.join(root, 'ui/knowledge-panel.html');
|
|
14
|
+
const panelHtml = fs.readFileSync(panelPath, 'utf8');
|
|
15
|
+
|
|
16
|
+
/* ===================== panel page source contract ===================== */
|
|
17
|
+
assert.ok(panelHtml.includes('KB-K1-KNOWLEDGE-PANEL-v1'), 'the panel page shows the stable marker');
|
|
18
|
+
assert.ok(panelHtml.includes('event.source !== window.parent'), 'the panel only accepts its Workbench parent as the message source');
|
|
19
|
+
assert.ok(panelHtml.includes("data.type !== 'kb:panel/content'"), 'kb:panel/content is the only inbound verb');
|
|
20
|
+
assert.ok(panelHtml.includes("'kb:panel/content-ack'"), 'the panel answers with a content ack');
|
|
21
|
+
assert.ok(!panelHtml.includes('innerHTML') && !panelHtml.includes('insertAdjacentHTML') && !panelHtml.includes('document.write'), 'no string becomes markup');
|
|
22
|
+
assert.ok(!/<(?:script|link|img)[^>]+(?:src|href)=/.test(panelHtml.replace(/<script>[\s\S]*?<\/script>/, '')), 'the panel page loads no external resources');
|
|
23
|
+
assert.ok(!panelHtml.includes('fetch(') && !panelHtml.includes('XMLHttpRequest'), 'the sandboxed panel never talks to APIs directly');
|
|
24
|
+
const panelScriptMatch = panelHtml.match(/<script>([\s\S]*?)<\/script>/);
|
|
25
|
+
assert.ok(panelScriptMatch, 'the panel page carries an inline script');
|
|
26
|
+
assert.doesNotThrow(() => new Function(panelScriptMatch[1]));
|
|
27
|
+
|
|
28
|
+
/* ===================== panel page behavior (minimal DOM) ===================== */
|
|
29
|
+
function runPanelPage() {
|
|
30
|
+
const acks = [];
|
|
31
|
+
const node = () => ({ children: [], textContent: '', append(...items) { this.children.push(...items); }, replaceChildren() { this.children.length = 0; } });
|
|
32
|
+
const elements = { panelTitle: node(), panelProject: node(), panelStatus: node(), panelReferences: node(), panelMarker: node() };
|
|
33
|
+
let listener = null;
|
|
34
|
+
const parent = { postMessage(message) { acks.push(message); } };
|
|
35
|
+
const fakeWindow = { parent, addEventListener(type, fn) { if (type === 'message') listener = fn; } };
|
|
36
|
+
const fakeDocument = {
|
|
37
|
+
getElementById: id => elements[id] || null,
|
|
38
|
+
createElement: () => node(),
|
|
39
|
+
};
|
|
40
|
+
new Function('window', 'document', panelScriptMatch[1])(fakeWindow, fakeDocument);
|
|
41
|
+
assert.equal(typeof listener, 'function', 'the panel registers a message listener');
|
|
42
|
+
const deliver = (data, source = parent) => listener({ source, data });
|
|
43
|
+
return { acks, elements, deliver };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
const page = runPanelPage();
|
|
48
|
+
page.deliver({ type: 'kb:panel/content', panelId: 'tab-1', version: 3, content: {
|
|
49
|
+
title: '本次知识引用', projectId: 'proj-a', status: 'ready',
|
|
50
|
+
references: [{ entryId: 'docs/a.md', title: 'T', text: '<img src=x onerror=alert(1)>', source: 'proj / docs/a.md' }],
|
|
51
|
+
} });
|
|
52
|
+
const ack = page.acks[page.acks.length - 1];
|
|
53
|
+
assert.equal(ack.type, 'kb:panel/content-ack');
|
|
54
|
+
assert.equal(ack.panelId, 'tab-1');
|
|
55
|
+
assert.equal(ack.version, 3);
|
|
56
|
+
assert.equal(ack.ok, true);
|
|
57
|
+
assert.equal(page.elements.panelTitle.textContent, '本次知识引用');
|
|
58
|
+
assert.equal(page.elements.panelProject.textContent, 'proj-a');
|
|
59
|
+
const reference = page.elements.panelReferences.children[0];
|
|
60
|
+
assert.ok(reference, 'one reference card rendered');
|
|
61
|
+
const text = reference.children.find(child => child.textContent.includes('<img'));
|
|
62
|
+
assert.ok(text, 'the reference body exists');
|
|
63
|
+
assert.equal(text.textContent, '<img src=x onerror=alert(1)>', 'markup-looking strings stay inert text');
|
|
64
|
+
}
|
|
65
|
+
{
|
|
66
|
+
const page = runPanelPage();
|
|
67
|
+
const stranger = { postMessage() {} };
|
|
68
|
+
page.deliver({ type: 'kb:panel/content', panelId: 'tab-1', version: 1, content: { title: 'x' } }, stranger);
|
|
69
|
+
assert.equal(page.acks.length, 0, 'a non-parent source is ignored without any ack');
|
|
70
|
+
page.deliver({ type: 'kb:panel/command', panelId: 'tab-1' });
|
|
71
|
+
assert.equal(page.acks.length, 0, 'non-content verbs are ignored');
|
|
72
|
+
page.deliver({ type: 'kb:panel/content', panelId: 'tab-1', version: 2, content: ['not', 'an', 'object'] });
|
|
73
|
+
const failure = page.acks[page.acks.length - 1];
|
|
74
|
+
assert.equal(failure.ok, false, 'illegal content is refused with an explicit receipt');
|
|
75
|
+
assert.ok(failure.error, 'the refusal carries an error message');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ===================== shell (product.js) contract ===================== */
|
|
79
|
+
assert.ok(script.includes('kb.knowledge.references'), 'the provider id is stable');
|
|
80
|
+
assert.match(script, /url: '\/knowledge-panel\.html'/, 'the provider URL is the KB-owned panel page');
|
|
81
|
+
assert.match(script, /panelType: 'knowledge', title: '本次知识引用'/, 'panel type and title match the product');
|
|
82
|
+
assert.match(script, /capabilities: \['read-only'\]/, 'the panel declares itself read-only');
|
|
83
|
+
assert.match(script, /multiInstance: 'single'/, 'the provider is single-instance');
|
|
84
|
+
assert.match(script, /if \(message\.type === 'kb:ready'\)[\s\S]*?if \(key === 'terminal'\) void resetKnowledgePanel\(\)/, 'registration starts from the terminal kb:ready');
|
|
85
|
+
assert.ok(script.includes("'kb:panel/register'") && script.includes("'kb:panel/open'") && script.includes("'kb:panel/content'"), 'the shell uses the W3 verbs');
|
|
86
|
+
assert.match(script, /if \(message\.type === 'kb:knowledge' && key === 'terminal'\)[\s\S]*?renderContext\(\); void pushKnowledgeToPanel/, 'kb:knowledge still refreshes the fixed column first, then streams the panel');
|
|
87
|
+
assert.match(script, /const ack = panel\.content\?\.ack;[\s\S]*?if \(ack && String\(ack\.version\) === String\(pending\.version\)\)/, 'collapse waits for the version-matched ack');
|
|
88
|
+
assert.match(script, /if \(ack\.ok && !panel\.error\) \{ setKnowledgeCollapsed\(true\)/, 'folding requires an error-free ack');
|
|
89
|
+
assert.match(script, /if \(!panel\) \{[\s\S]*?if \(knowledgePanel\.collapsed\) setKnowledgeCollapsed\(false\);[\s\S]*?return;/, 'closing the dock panel unfolds the fixed column again');
|
|
90
|
+
assert.match(script, /function knowledgePanelFailed\(reason\)[\s\S]*?setKnowledgeCollapsed\(false\);[\s\S]*?panelNotice\(/, 'every failure path restores the full column with a non-blocking notice');
|
|
91
|
+
assert.match(script, /function selectProject\(id\)[\s\S]*?setKnowledgeCollapsed\(false\);[\s\S]*?pushKnowledgeToPanel\(id\)/, 'a project switch re-scopes the panel to the new project before folding again');
|
|
92
|
+
assert.match(script, /renderProjectOptions\(\); setKnowledgeCollapsed\(false\); render\(\)/, 'unbinding a project also restores the fixed column');
|
|
93
|
+
assert.match(script, /\.slice\(0, 50\)\.map\(r => \(\{/, 'the payload bounds the reference count');
|
|
94
|
+
assert.match(script, /text: String\(r\.text \|\| ''\)\.slice\(0, 800\)/, 'the payload bounds reference text');
|
|
95
|
+
assert.match(script, /knowledgePayload\(projectId\)[\s\S]*?entryId: String\(/, 'payload fields are coerced to plain strings');
|
|
96
|
+
assert.equal((script.match(/classList\.toggle\('kb-panel-collapsed'/g) || []).length, 1, 'collapse is driven only through the work-layout class');
|
|
97
|
+
assert.ok(!/data-action="collapse|toggle-context|折叠知识栏/.test(script + html), 'no duplicate column control button is introduced');
|
|
98
|
+
assert.match(script, /panelRequest\('kb:panel\/content', \{ panelId: knowledgePanel\.panelId, providerId: knowledgePanel\.providerId, version, content: knowledgePayload\(projectId\) \}\)/, 'content carries panelId, providerId and version');
|
|
99
|
+
assert.match(script, /message\.type\.startsWith\('kb:panel\/'\)/, 'panel replies are routed before the frame-context staleness drop');
|
|
100
|
+
assert.match(script, /if \(message\.requestId && message\.requestId !== frames\[key\]\.requestId\) return;/, 'the legacy staleness drop stays intact for module traffic');
|
|
101
|
+
assert.doesNotThrow(() => new Function(script));
|
|
102
|
+
|
|
103
|
+
/* ===================== markup and css contract ===================== */
|
|
104
|
+
assert.equal((html.match(/id="panelNotice"/g) || []).length, 1, 'exactly one non-blocking panel notice exists');
|
|
105
|
+
assert.ok(html.includes('knowledge-context') && html.includes('id="contextContent"'), 'the fixed knowledge column markup is untouched');
|
|
106
|
+
assert.ok(css.includes('.work-layout.kb-panel-collapsed{grid-template-columns:minmax(0,1fr) 0px}'), 'the fold animates the grid track instead of removing the column');
|
|
107
|
+
assert.match(css, /\.work-layout\{[^}]*transition:grid-template-columns/, 'the fold is a smooth transition');
|
|
108
|
+
assert.match(css, /@media \(prefers-reduced-motion: reduce\)\{\.work-layout,\.knowledge-context\{transition:none\}\}/, 'reduced-motion users get no animation');
|
|
109
|
+
assert.ok(css.includes('.panel-notice'), 'the notice has styling');
|
|
110
|
+
assert.match(css, /\.work-layout\{display:grid;grid-template-columns:minmax\(0,1fr\) 288px/, 'the default full-width column is unchanged');
|
|
111
|
+
|
|
112
|
+
console.log('knowledge panel UI contract test PASS (source contract + sandboxed render behavior; not browser acceptance)');
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
-
const { SettingsStore } = require('../lib/settings-store');
|
|
7
|
-
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
8
|
-
const { ProjectStore } = require('../lib/project-store');
|
|
9
|
-
const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
|
|
10
|
-
|
|
11
|
-
(async () => {
|
|
12
|
-
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-scopes-v2-'));
|
|
13
|
-
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
14
|
-
const settings = new SettingsStore({ layout });
|
|
15
|
-
const registry = new ProjectRegistryStore({ layout });
|
|
16
|
-
const projects = new ProjectStore({ layout });
|
|
17
|
-
await settings.initialize({ knowledge: { rootPath: path.join(temp, 'knowledge') } });
|
|
18
|
-
await registry.initialize();
|
|
19
|
-
for (const projectId of ['project-api', 'project-web', 'project-transitive']) {
|
|
20
|
-
const knowledgePath = path.join(temp, 'knowledge', projectId);
|
|
21
|
-
fs.mkdirSync(knowledgePath, { recursive: true });
|
|
22
|
-
await projects.create(projectId, {
|
|
23
|
-
displayName: projectId, storageName: projectId, repoPath: path.join(temp, projectId), knowledgePath,
|
|
24
|
-
relatedProjectIds: projectId === 'project-api' ? ['project-web'] : projectId === 'project-web' ? ['project-transitive'] : [],
|
|
25
|
-
});
|
|
26
|
-
await registry.add(projectId, { displayNameSnapshot: projectId });
|
|
27
|
-
}
|
|
28
|
-
const runtime = new KnowledgeToolRuntime({ layout, settingsStore: settings, registryStore: registry, projectStore: projects });
|
|
29
|
-
const project = runtime.resolveProject({ projectId: 'project-api' });
|
|
30
|
-
const scopes = runtime.scopes(project);
|
|
31
|
-
assert.deepStrictEqual(scopes.map(scope => scope.spaceId), ['project:project-api', 'project:project-web']);
|
|
32
|
-
assert(!scopes.some(scope => scope.projectId === 'project-transitive'), 'related scopes must remain non-transitive');
|
|
33
|
-
assert.strictEqual(scopes[0].weight, 1);
|
|
34
|
-
assert.strictEqual(scopes[1].weight, 0.88);
|
|
35
|
-
assert(!fs.existsSync(path.join(layout.getDataDir(), 'knowledge-scopes.json')), 'scope resolution must derive from immutable project config without side effects');
|
|
36
|
-
console.log('knowledge-scopes-test PASS');
|
|
37
|
-
})().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 { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
7
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
8
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
9
|
+
const { KnowledgeToolRuntime } = require('../lib/knowledge-tool-runtime');
|
|
10
|
+
|
|
11
|
+
(async () => {
|
|
12
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-scopes-v2-'));
|
|
13
|
+
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
14
|
+
const settings = new SettingsStore({ layout });
|
|
15
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
16
|
+
const projects = new ProjectStore({ layout });
|
|
17
|
+
await settings.initialize({ knowledge: { rootPath: path.join(temp, 'knowledge') } });
|
|
18
|
+
await registry.initialize();
|
|
19
|
+
for (const projectId of ['project-api', 'project-web', 'project-transitive']) {
|
|
20
|
+
const knowledgePath = path.join(temp, 'knowledge', projectId);
|
|
21
|
+
fs.mkdirSync(knowledgePath, { recursive: true });
|
|
22
|
+
await projects.create(projectId, {
|
|
23
|
+
displayName: projectId, storageName: projectId, repoPath: path.join(temp, projectId), knowledgePath,
|
|
24
|
+
relatedProjectIds: projectId === 'project-api' ? ['project-web'] : projectId === 'project-web' ? ['project-transitive'] : [],
|
|
25
|
+
});
|
|
26
|
+
await registry.add(projectId, { displayNameSnapshot: projectId });
|
|
27
|
+
}
|
|
28
|
+
const runtime = new KnowledgeToolRuntime({ layout, settingsStore: settings, registryStore: registry, projectStore: projects });
|
|
29
|
+
const project = runtime.resolveProject({ projectId: 'project-api' });
|
|
30
|
+
const scopes = runtime.scopes(project);
|
|
31
|
+
assert.deepStrictEqual(scopes.map(scope => scope.spaceId), ['project:project-api', 'project:project-web']);
|
|
32
|
+
assert(!scopes.some(scope => scope.projectId === 'project-transitive'), 'related scopes must remain non-transitive');
|
|
33
|
+
assert.strictEqual(scopes[0].weight, 1);
|
|
34
|
+
assert.strictEqual(scopes[1].weight, 0.88);
|
|
35
|
+
assert(!fs.existsSync(path.join(layout.getDataDir(), 'knowledge-scopes.json')), 'scope resolution must derive from immutable project config without side effects');
|
|
36
|
+
console.log('knowledge-scopes-test PASS');
|
|
37
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
-
const { SettingsStore } = require('../lib/settings-store');
|
|
7
|
-
|
|
8
|
-
(async () => {
|
|
9
|
-
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-storage-location-v2-'));
|
|
10
|
-
const dataDir = path.join(temp, 'data');
|
|
11
|
-
const rootA = path.join(temp, 'knowledge-a');
|
|
12
|
-
const rootB = path.join(temp, 'knowledge-b');
|
|
13
|
-
const layout = new StorageLayout({ dataDir });
|
|
14
|
-
const settings = new SettingsStore({ layout });
|
|
15
|
-
await settings.initialize({ knowledge: { rootPath: rootA } });
|
|
16
|
-
const indexPath = layout.getIndexPath();
|
|
17
|
-
fs.mkdirSync(indexPath, { recursive: true });
|
|
18
|
-
fs.writeFileSync(path.join(indexPath, 'sentinel'), 'derived-index', 'utf8');
|
|
19
|
-
await settings.updatePatch({ knowledge: { rootPath: rootB } });
|
|
20
|
-
assert.strictEqual(layout.getIndexPath(), indexPath, 'the internal index path must not follow the user knowledge root');
|
|
21
|
-
assert.strictEqual(fs.readFileSync(path.join(indexPath, 'sentinel'), 'utf8'), 'derived-index');
|
|
22
|
-
assert.strictEqual(layout.isPathInside(rootA, indexPath), false);
|
|
23
|
-
assert.strictEqual(layout.isPathInside(rootB, indexPath), false);
|
|
24
|
-
assert.strictEqual(layout.getKnowledgeRootPath(settings.read()), path.resolve(rootB));
|
|
25
|
-
assert(!fs.existsSync(path.join(rootA, '.project-knowledge')));
|
|
26
|
-
assert(!fs.existsSync(path.join(rootB, '.project-knowledge')));
|
|
27
|
-
console.log('knowledge-storage-location-test: PASS');
|
|
28
|
-
})().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 { StorageLayout } = require('../lib/storage-layout');
|
|
6
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
7
|
+
|
|
8
|
+
(async () => {
|
|
9
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-storage-location-v2-'));
|
|
10
|
+
const dataDir = path.join(temp, 'data');
|
|
11
|
+
const rootA = path.join(temp, 'knowledge-a');
|
|
12
|
+
const rootB = path.join(temp, 'knowledge-b');
|
|
13
|
+
const layout = new StorageLayout({ dataDir });
|
|
14
|
+
const settings = new SettingsStore({ layout });
|
|
15
|
+
await settings.initialize({ knowledge: { rootPath: rootA } });
|
|
16
|
+
const indexPath = layout.getIndexPath();
|
|
17
|
+
fs.mkdirSync(indexPath, { recursive: true });
|
|
18
|
+
fs.writeFileSync(path.join(indexPath, 'sentinel'), 'derived-index', 'utf8');
|
|
19
|
+
await settings.updatePatch({ knowledge: { rootPath: rootB } });
|
|
20
|
+
assert.strictEqual(layout.getIndexPath(), indexPath, 'the internal index path must not follow the user knowledge root');
|
|
21
|
+
assert.strictEqual(fs.readFileSync(path.join(indexPath, 'sentinel'), 'utf8'), 'derived-index');
|
|
22
|
+
assert.strictEqual(layout.isPathInside(rootA, indexPath), false);
|
|
23
|
+
assert.strictEqual(layout.isPathInside(rootB, indexPath), false);
|
|
24
|
+
assert.strictEqual(layout.getKnowledgeRootPath(settings.read()), path.resolve(rootB));
|
|
25
|
+
assert(!fs.existsSync(path.join(rootA, '.project-knowledge')));
|
|
26
|
+
assert(!fs.existsSync(path.join(rootB, '.project-knowledge')));
|
|
27
|
+
console.log('knowledge-storage-location-test: PASS');
|
|
28
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|