@sanqianx/project-knowledge 4.6.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 +20 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +780 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +266 -0
- package/_site/README.md +30 -0
- package/_site/_test/ai-profile-resolver-test.js +137 -0
- package/_site/_test/ai-profile-test.js +75 -0
- package/_site/_test/ai-vendor-presets-test.js +91 -0
- package/_site/_test/automation-queue-test.js +14 -0
- package/_site/_test/automation-ui-test.js +40 -0
- package/_site/_test/background-task-registry-test.js +43 -0
- package/_site/_test/baseline-schema-test.js +91 -0
- package/_site/_test/bin-cli-test.js +218 -0
- package/_site/_test/bridge-adapter-test.js +95 -0
- package/_site/_test/bridge-consumer-service-test.js +310 -0
- package/_site/_test/cdp-browser-failure-test.js +100 -0
- package/_site/_test/chat-claudecodeui-match-test.js +92 -0
- package/_site/_test/claude-executable-discovery-test.js +90 -0
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
- package/_site/_test/claude-workbench-test.js +170 -0
- package/_site/_test/client-capture-restart-e2e-test.js +189 -0
- package/_site/_test/codex-conversation-projection-test.js +59 -0
- package/_site/_test/codex-notify-fanout-test.js +45 -0
- package/_site/_test/commit-boundary-freeze-test.js +147 -0
- package/_site/_test/commit-conversation-binding-test.js +99 -0
- package/_site/_test/commit-evidence-test.js +110 -0
- package/_site/_test/context-pack-test.js +198 -0
- package/_site/_test/conversation-api-test.js +119 -0
- package/_site/_test/conversation-store-test.js +136 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
- package/_site/_test/data-dir-migration-test.js +77 -0
- package/_site/_test/desktop-browser-compat-test.js +28 -0
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
- package/_site/_test/embedding-config-test.js +32 -0
- package/_site/_test/explicit-commit-processor-test.js +37 -0
- package/_site/_test/fixtures/fake-browser-exit.js +10 -0
- package/_site/_test/fixtures/fake-browser-hang.js +10 -0
- package/_site/_test/fixtures/make-git-repos.js +89 -0
- package/_site/_test/folder-picker-output-test.js +19 -0
- package/_site/_test/full-integration-e2e-test.js +165 -0
- package/_site/_test/git-validation-test.js +81 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -0
- package/_site/_test/hook-status-repair-api-test.js +152 -0
- package/_site/_test/hook-trigger-test.js +85 -0
- package/_site/_test/hook-worktree-test.js +42 -0
- package/_site/_test/import-preflight-api-test.js +233 -0
- package/_site/_test/import-ui-flow-test.js +171 -0
- package/_site/_test/index-builder-test.js +41 -0
- package/_site/_test/index-writer-concurrency-test.js +135 -0
- package/_site/_test/integration-adapters-test.js +243 -0
- package/_site/_test/integration-capture-setup-test.js +108 -0
- package/_site/_test/integration-surface-coverage-test.js +165 -0
- package/_site/_test/kb-framework-test.js +57 -0
- package/_site/_test/kbpath-follow-test.js +63 -0
- package/_site/_test/knowledge-db-test.js +144 -0
- package/_site/_test/knowledge-language-control-test.js +170 -0
- package/_site/_test/knowledge-maintenance-test.js +40 -0
- package/_site/_test/knowledge-migration-test.js +75 -0
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
- package/_site/_test/knowledge-query-test.js +54 -0
- package/_site/_test/knowledge-retrieval-service-test.js +77 -0
- package/_site/_test/knowledge-scopes-test.js +37 -0
- package/_site/_test/knowledge-storage-location-test.js +28 -0
- package/_site/_test/knowledge-storage-startup-test.js +52 -0
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
- package/_site/_test/legacy-claude-session-alias-test.js +106 -0
- package/_site/_test/legacy-forward-compat-test.js +257 -0
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
- package/_site/_test/legacy-routes-removed-test.js +47 -0
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
- package/_site/_test/log-redaction-test.js +58 -0
- package/_site/_test/logging-api-test.js +89 -0
- package/_site/_test/logging-sse-no-gap-test.js +88 -0
- package/_site/_test/logging-ui-test.js +104 -0
- package/_site/_test/markdown-delta-overlay-test.js +85 -0
- package/_site/_test/markdown-maintenance-api-test.js +75 -0
- package/_site/_test/markdown-maintenance-test.js +11 -0
- package/_site/_test/mcp-server-test.js +149 -0
- package/_site/_test/model-context-windows-test.js +89 -0
- package/_site/_test/module-bridge-eventbridge-test.js +193 -0
- package/_site/_test/non-release-ci-test.js +34 -0
- package/_site/_test/offline-boundary-isolation-test.js +32 -0
- package/_site/_test/orphaned-claim-recovery-test.js +80 -0
- package/_site/_test/p0-data-migration-characterization-test.js +37 -0
- package/_site/_test/p0-e2e-gate-test.js +347 -0
- package/_site/_test/package-boundary-test.js +42 -0
- package/_site/_test/package-startup-test.js +56 -0
- package/_site/_test/packaged-ui-smoke-test.js +79 -0
- package/_site/_test/path-consistency-test.js +151 -0
- package/_site/_test/pending-sweep-test.js +9 -0
- package/_site/_test/post-commit-automation-test.js +87 -0
- package/_site/_test/pr-consumer-contract-test.js +55 -0
- package/_site/_test/project-control-panel-task14-test.js +63 -0
- package/_site/_test/project-delete-recovery-test.js +64 -0
- package/_site/_test/project-goal-editor-test.js +145 -0
- package/_site/_test/project-layout-v2-migration-test.js +127 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -0
- package/_site/_test/project-store-test.js +90 -0
- package/_site/_test/prompt-settings-test.js +115 -0
- package/_site/_test/protected-architecture-gate-test.js +123 -0
- package/_site/_test/refactor-characterization-test.js +36 -0
- package/_site/_test/release-version-sync-test.js +98 -0
- package/_site/_test/requirement-binding-test.js +113 -0
- package/_site/_test/requirement-recorder-test.js +173 -0
- package/_site/_test/run-all-tests.js +156 -0
- package/_site/_test/runtime-endpoint-test.js +64 -0
- package/_site/_test/scanner-test.js +83 -0
- package/_site/_test/server-runtime-migration-safety-test.js +27 -0
- package/_site/_test/server-security-test.js +96 -0
- package/_site/_test/sessions-stream-test.js +92 -0
- package/_site/_test/shared-contracts-test.js +58 -0
- package/_site/_test/simple-import-test.js +97 -0
- package/_site/_test/startup-analysis-disabled-test.js +16 -0
- package/_site/_test/storage-foundation-test.js +78 -0
- package/_site/_test/structured-logger-test.js +101 -0
- package/_site/_test/task15-20-ui-flow-test.js +148 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
- package/_site/_test/team-knowledge-store-test.js +131 -0
- package/_site/_test/tracking-start-test.js +143 -0
- package/_site/_test/ui-i18n-toggle-test.js +114 -0
- package/_site/_test/ui-smoke-test.js +73 -0
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
- package/_site/_test/workbench-permission-test.js +118 -0
- package/_site/_test/workspace-ui-contract-test.js +43 -0
- package/_site/lib/ai-profile-resolver.js +78 -0
- package/_site/lib/ai-vendor-presets.js +83 -0
- package/_site/lib/ai-workspace.js +104 -0
- package/_site/lib/atomic-file.js +204 -0
- package/_site/lib/automation-config.js +140 -0
- package/_site/lib/bridge-adapter.js +256 -0
- package/_site/lib/bridge-consumer-service.js +402 -0
- package/_site/lib/claude-cli-runner.js +1599 -0
- package/_site/lib/commit-conversation-binder.js +246 -0
- package/_site/lib/commit-processing-ledger.js +27 -0
- package/_site/lib/commit-prompt.js +202 -0
- package/_site/lib/commit-reconciler.js +444 -0
- package/_site/lib/context-pack-builder.js +299 -0
- package/_site/lib/contracts.js +279 -0
- package/_site/lib/conversation-exclusions.js +204 -0
- package/_site/lib/conversation-query-service.js +161 -0
- package/_site/lib/conversation-store.js +385 -0
- package/_site/lib/data-dir.js +81 -0
- package/_site/lib/data-state-classifier.js +40 -0
- package/_site/lib/embedding-service.js +85 -0
- package/_site/lib/evidence-bundle.js +228 -0
- package/_site/lib/folder-picker-output.js +24 -0
- package/_site/lib/git-runner.js +62 -0
- package/_site/lib/github-team-store.js +1175 -0
- package/_site/lib/hook-manager.js +202 -0
- package/_site/lib/index-builder.js +157 -0
- package/_site/lib/index-service.js +173 -0
- package/_site/lib/integration-installer.js +907 -0
- package/_site/lib/kb-framework.js +191 -0
- package/_site/lib/kb-validator.js +124 -0
- package/_site/lib/knowledge-db.js +363 -0
- package/_site/lib/knowledge-promotion.js +435 -0
- package/_site/lib/knowledge-retrieval-service.js +401 -0
- package/_site/lib/knowledge-schema.js +121 -0
- package/_site/lib/knowledge-tool-runtime.js +359 -0
- package/_site/lib/legacy-data-manifest.js +26 -0
- package/_site/lib/llm-client.js +173 -0
- package/_site/lib/markdown-knowledge-indexer.js +230 -0
- package/_site/lib/migration-service.js +442 -0
- package/_site/lib/model-context-windows.js +82 -0
- package/_site/lib/module-bridge.js +496 -0
- package/_site/lib/post-commit-automation.js +141 -0
- package/_site/lib/project-lifecycle-service.js +511 -0
- package/_site/lib/project-registry-store.js +129 -0
- package/_site/lib/project-store.js +262 -0
- package/_site/lib/prompt-registry.js +148 -0
- package/_site/lib/requirement-adapters.js +10 -0
- package/_site/lib/requirement-binder.js +112 -0
- package/_site/lib/requirement-recorder.js +278 -0
- package/_site/lib/runtime-endpoint.js +160 -0
- package/_site/lib/scanner.js +298 -0
- package/_site/lib/server-app.js +1520 -0
- package/_site/lib/settings-store.js +113 -0
- package/_site/lib/storage-layout.js +162 -0
- package/_site/lib/structured-logger.js +574 -0
- package/_site/scripts/codex-notify-fanout.cjs +51 -0
- package/_site/scripts/folder-picker.ps1 +156 -0
- package/_site/scripts/hook-trigger.js +156 -0
- package/_site/scripts/sync-release-version.js +158 -0
- package/_site/server.js +12 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/bin/project-knowledge-integrations.js +241 -0
- package/bin/project-knowledge-kb.js +65 -0
- package/bin/project-knowledge-mcp.js +195 -0
- package/bin/project-knowledge.js +439 -0
- package/claude-prompts.json +21 -0
- package/docs/README.zh-CN.md +175 -0
- package/docs/assets/architecture.svg +182 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/assets/draft-review.png +0 -0
- package/docs/assets/logo.svg +50 -0
- package/docs/assets/runs-drafts.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/docs/project-registry-schema.md +33 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -0
- package/docs/testing-strategy.md +33 -0
- package/package.json +100 -0
- package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
- package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
- package/plugins/project-knowledge/.mcp.json +14 -0
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
- package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
- package/templates/change.md +30 -0
- package/templates/project-readme.md +52 -0
- package/ui/app.css +58 -0
- package/ui/app.js +607 -0
- package/ui/favicon.svg +38 -0
- package/ui/i18n.js +146 -0
- package/ui/index.html +162 -0
|
@@ -0,0 +1,88 @@
|
|
|
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 { StorageLayout } = require('../lib/storage-layout');
|
|
7
|
+
const { LogRepository } = require('../lib/structured-logger');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
10
|
+
const PORT = 7950 + (process.pid % 300);
|
|
11
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
12
|
+
|
|
13
|
+
async function waitForServer() {
|
|
14
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
15
|
+
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
16
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
17
|
+
}
|
|
18
|
+
throw new Error('server did not start');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
(async () => {
|
|
22
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-log-sse-'));
|
|
23
|
+
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'log-sse', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
24
|
+
try {
|
|
25
|
+
await waitForServer();
|
|
26
|
+
let response = await fetch(`${BASE}/api/logs?q=http.request_`);
|
|
27
|
+
let snapshot = await response.json();
|
|
28
|
+
assert.strictEqual(snapshot.pageSize, 500, 'HTTP logs API must preserve the 500-row default');
|
|
29
|
+
assert(snapshot.streamCursor);
|
|
30
|
+
response = await fetch(`${BASE}/api/logs?pageSize=999999`);
|
|
31
|
+
assert.strictEqual((await response.json()).pageSize, 5000, 'HTTP logs API must cap the display page at 5000');
|
|
32
|
+
|
|
33
|
+
await fetch(`${BASE}/api/state`);
|
|
34
|
+
await fetch(`${BASE}/api/projects/not-valid%2Fid`);
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
36
|
+
const watermark = JSON.parse(Buffer.from(snapshot.streamCursor, 'base64url').toString('utf8'));
|
|
37
|
+
const repository = new LogRepository({ layout: new StorageLayout({ dataDir }) });
|
|
38
|
+
const page = repository.query({ q: 'http.request_', pageSize: 5000 });
|
|
39
|
+
const expectedGapIds = [];
|
|
40
|
+
for (const entry of page.entries) {
|
|
41
|
+
if (entry.id === watermark.afterId) break;
|
|
42
|
+
expectedGapIds.push(entry.id);
|
|
43
|
+
}
|
|
44
|
+
assert(expectedGapIds.length >= 3, 'fixture must create completed and failed requests between GET and SSE');
|
|
45
|
+
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
response = await fetch(`${BASE}/api/logs/stream?q=http.request_&streamCursor=${encodeURIComponent(snapshot.streamCursor)}`, { signal: controller.signal });
|
|
48
|
+
assert.strictEqual(response.status, 200);
|
|
49
|
+
assert.match(response.headers.get('content-type'), /text\/event-stream/);
|
|
50
|
+
const reader = response.body.getReader();
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
let pending = '';
|
|
53
|
+
const streamedIds = new Set();
|
|
54
|
+
let ready = false;
|
|
55
|
+
const deadline = Date.now() + 5000;
|
|
56
|
+
while (Date.now() < deadline && (!ready || expectedGapIds.some(id => !streamedIds.has(id)))) {
|
|
57
|
+
const read = await Promise.race([
|
|
58
|
+
reader.read(),
|
|
59
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('SSE read timeout')), 1000)),
|
|
60
|
+
]);
|
|
61
|
+
if (read.done) break;
|
|
62
|
+
pending += decoder.decode(read.value, { stream: true });
|
|
63
|
+
const frames = pending.split('\n\n');
|
|
64
|
+
pending = frames.pop() || '';
|
|
65
|
+
for (const frame of frames) {
|
|
66
|
+
const dataLine = frame.split('\n').find(line => line.startsWith('data: '));
|
|
67
|
+
if (!dataLine) continue;
|
|
68
|
+
const event = JSON.parse(dataLine.slice(6));
|
|
69
|
+
if (event.type === 'logs/ready') ready = true;
|
|
70
|
+
if (event.type === 'logs/appended' && event.record) streamedIds.add(event.record.id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
controller.abort();
|
|
74
|
+
assert(ready, 'SSE must mark the replay/live handoff');
|
|
75
|
+
assert(expectedGapIds.every(id => streamedIds.has(id)), 'GET-to-SSE handoff must replay every durable gap record exactly by id');
|
|
76
|
+
|
|
77
|
+
const requestLogs = repository.query({ q: 'http.request_', pageSize: 5000 }).entries;
|
|
78
|
+
assert(requestLogs.some(entry => entry.event === 'http.request_started'));
|
|
79
|
+
assert(requestLogs.some(entry => entry.event === 'http.request_completed' && entry.durationMs >= 0));
|
|
80
|
+
assert(requestLogs.some(entry => entry.event === 'http.request_failed' && entry.operationId));
|
|
81
|
+
console.log('logging-sse-no-gap-test PASS');
|
|
82
|
+
} finally {
|
|
83
|
+
await spawned.cleanup();
|
|
84
|
+
}
|
|
85
|
+
})().catch(error => {
|
|
86
|
+
console.error(error.stack || error.message);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
4
|
+
const { requestJson, waitFor } = require('./helpers/cdp-browser');
|
|
5
|
+
const { createLoggingUiFixture } = require('./helpers/logging-ui-fixture');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
8
|
+
const port = 8750 + (process.pid % 180);
|
|
9
|
+
|
|
10
|
+
function baseParams(fixture, pageSize) {
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
params.set('from', '2000-01-01');
|
|
13
|
+
params.set('to', '2999-12-31');
|
|
14
|
+
params.set('levels', 'trace,debug,info,warn,error,fatal');
|
|
15
|
+
params.set('projectId', fixture.projectId);
|
|
16
|
+
params.set('pageSize', String(pageSize || 10));
|
|
17
|
+
return params;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function jsonResponse(url) {
|
|
21
|
+
const response = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
22
|
+
const body = await response.json();
|
|
23
|
+
return { response, body };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
(async () => {
|
|
27
|
+
const fixture = await createLoggingUiFixture({ totalLogs: 64 });
|
|
28
|
+
const server = spawnServer({ root: ROOT, port, dataDir: fixture.dataDir, tag: 'logging-ui-api', stdio: 'ignore' });
|
|
29
|
+
try {
|
|
30
|
+
await waitFor(() => requestJson('http://127.0.0.1:' + port + '/api/health').then(body => body.ok), 'logging API', 20000);
|
|
31
|
+
const baseUrl = 'http://127.0.0.1:' + port;
|
|
32
|
+
const firstParams = baseParams(fixture, 10);
|
|
33
|
+
const first = await jsonResponse(baseUrl + '/api/logs?' + firstParams.toString());
|
|
34
|
+
assert.strictEqual(first.response.status, 200);
|
|
35
|
+
assert.strictEqual(first.body.entries.length, 10);
|
|
36
|
+
assert(first.body.nextCursor, 'first page must return an opaque next cursor');
|
|
37
|
+
assert.strictEqual(first.body.pageSize, 10);
|
|
38
|
+
assert(first.body.health && first.body.health.status === 'ok');
|
|
39
|
+
assert.deepStrictEqual(Object.keys(first.body.counts).sort(), ['debug', 'error', 'fatal', 'info', 'trace', 'warn']);
|
|
40
|
+
|
|
41
|
+
const secondParams = baseParams(fixture, 10);
|
|
42
|
+
secondParams.set('cursor', first.body.nextCursor);
|
|
43
|
+
const second = await jsonResponse(baseUrl + '/api/logs?' + secondParams.toString());
|
|
44
|
+
assert.strictEqual(second.response.status, 200);
|
|
45
|
+
assert.strictEqual(second.body.entries.length, 10);
|
|
46
|
+
assert(!second.body.entries.some(entry => first.body.entries.some(prior => prior.id === entry.id)), 'cursor pages must not overlap');
|
|
47
|
+
|
|
48
|
+
const invalidParams = baseParams(fixture, 10);
|
|
49
|
+
invalidParams.set('cursor', first.body.nextCursor);
|
|
50
|
+
invalidParams.set('q', 'changed-fingerprint');
|
|
51
|
+
const invalid = await jsonResponse(baseUrl + '/api/logs?' + invalidParams.toString());
|
|
52
|
+
assert.strictEqual(invalid.response.status, 409);
|
|
53
|
+
assert.strictEqual(invalid.body.error.code, 'LOG_CURSOR_EXPIRED');
|
|
54
|
+
assert.strictEqual(invalid.body.error.retryable, true);
|
|
55
|
+
assert(!('stack' in invalid.body.error), 'public cursor error must not leak a stack');
|
|
56
|
+
|
|
57
|
+
const filters = [
|
|
58
|
+
['component', 'commit-reconciler'],
|
|
59
|
+
['event', 'fixture.phase.10'],
|
|
60
|
+
['operationId', 'op-flow-visual'],
|
|
61
|
+
['commitSha', fixture.repo.headCommit],
|
|
62
|
+
['q', '<img src=x onerror=window.__xss=1>'],
|
|
63
|
+
['levels', 'fatal'],
|
|
64
|
+
];
|
|
65
|
+
for (const pair of filters) {
|
|
66
|
+
const params = baseParams(fixture, 100);
|
|
67
|
+
params.set(pair[0], pair[1]);
|
|
68
|
+
const result = await jsonResponse(baseUrl + '/api/logs?' + params.toString());
|
|
69
|
+
assert.strictEqual(result.response.status, 200, 'filter failed: ' + pair[0]);
|
|
70
|
+
assert(result.body.entries.length > 0, 'filter returned no entries: ' + pair[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const exportParams = baseParams(fixture, 100);
|
|
74
|
+
exportParams.delete('pageSize');
|
|
75
|
+
exportParams.set('operationId', 'op-flow-visual');
|
|
76
|
+
exportParams.set('component', 'commit-reconciler');
|
|
77
|
+
exportParams.set('event', 'fixture.phase.3');
|
|
78
|
+
exportParams.set('commitSha', fixture.repo.headCommit);
|
|
79
|
+
const exported = await fetch(baseUrl + '/api/logs/export?' + exportParams.toString(), { headers: { Accept: 'application/x-ndjson' } });
|
|
80
|
+
assert.strictEqual(exported.status, 200);
|
|
81
|
+
assert((exported.headers.get('content-type') || '').includes('application/x-ndjson'));
|
|
82
|
+
assert((exported.headers.get('content-disposition') || '').includes('attachment'));
|
|
83
|
+
const lines = (await exported.text()).trim().split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
|
|
84
|
+
assert.strictEqual(lines.length, 1);
|
|
85
|
+
assert(lines.every(entry => entry.operationId === 'op-flow-visual' && entry.component === 'commit-reconciler' && entry.event === 'fixture.phase.3'));
|
|
86
|
+
|
|
87
|
+
const rootMutation = await fetch(baseUrl + '/api/settings', {
|
|
88
|
+
method: 'PATCH',
|
|
89
|
+
headers: { 'content-type': 'application/json' },
|
|
90
|
+
body: JSON.stringify({ logging: { rootPath: path.join(fixture.dataDir, 'elsewhere') } }),
|
|
91
|
+
});
|
|
92
|
+
assert.strictEqual(rootMutation.status, 409, 'logging root must not be mutable through the API');
|
|
93
|
+
const persistedSettings = JSON.parse(require('fs').readFileSync(fixture.layout.getSettingsPath(), 'utf8'));
|
|
94
|
+
assert(!Object.prototype.hasOwnProperty.call(persistedSettings.logging, 'rootPath'));
|
|
95
|
+
|
|
96
|
+
console.log('logging UI test PASS');
|
|
97
|
+
} finally {
|
|
98
|
+
await server.cleanup();
|
|
99
|
+
fixture.cleanup();
|
|
100
|
+
}
|
|
101
|
+
})().catch(error => {
|
|
102
|
+
console.error(error.stack || error.message);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const AtomicFile = require('../lib/atomic-file');
|
|
6
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
7
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
8
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
9
|
+
const { KnowledgeRetrievalService } = require('../lib/knowledge-retrieval-service');
|
|
10
|
+
const { CHUNKER_VERSION } = require('../lib/markdown-knowledge-indexer');
|
|
11
|
+
const { sha256 } = require('../lib/knowledge-schema');
|
|
12
|
+
|
|
13
|
+
(async () => {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-delta-overlay-'));
|
|
15
|
+
const layout = new StorageLayout({ dataDir: path.join(root, 'data') });
|
|
16
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
17
|
+
const projects = new ProjectStore({ layout });
|
|
18
|
+
await registry.initialize();
|
|
19
|
+
const projectId = 'project-delta';
|
|
20
|
+
const knowledgePath = path.join(root, 'knowledge');
|
|
21
|
+
fs.mkdirSync(path.join(knowledgePath, 'modules'), { recursive: true });
|
|
22
|
+
const changedPath = path.join(knowledgePath, 'modules', 'changed.md');
|
|
23
|
+
const deletedPath = path.join(knowledgePath, 'modules', 'deleted.md');
|
|
24
|
+
const oldChanged = '# Changed\n\nStale token policy from C0.\n';
|
|
25
|
+
const oldDeleted = '# Deleted\n\nStale deleted knowledge.\n';
|
|
26
|
+
fs.writeFileSync(changedPath, oldChanged, 'utf8');
|
|
27
|
+
fs.writeFileSync(deletedPath, oldDeleted, 'utf8');
|
|
28
|
+
await projects.create(projectId, {
|
|
29
|
+
displayName: 'Delta', storageName: 'delta', repoPath: root, knowledgePath,
|
|
30
|
+
}, { index: { dirty: true, generation: 2, sinceCommit: '1'.repeat(40) } });
|
|
31
|
+
await registry.add(projectId, { displayNameSnapshot: 'Delta' });
|
|
32
|
+
AtomicFile.writeJsonAtomic(layout.getRuntimePath('index-sources', `${projectId}.json`), {
|
|
33
|
+
schema: 'knowledge-index-source-manifest/v1',
|
|
34
|
+
projectId,
|
|
35
|
+
generation: 1,
|
|
36
|
+
entries: {
|
|
37
|
+
'modules/changed.md': { entryId: 'modules/changed.md', documentHash: sha256(`chunker:${CHUNKER_VERSION}\n${oldChanged}`), sourceCommit: '0'.repeat(40) },
|
|
38
|
+
'modules/deleted.md': { entryId: 'modules/deleted.md', documentHash: sha256(`chunker:${CHUNKER_VERSION}\n${oldDeleted}`), sourceCommit: '0'.repeat(40) },
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const currentChanged = `---\nsourcePaths: [src/token-policy.js]\n---\n# Changed\n\nC1 current rotating token policy truth.\n`;
|
|
43
|
+
fs.writeFileSync(changedPath, currentChanged, 'utf8');
|
|
44
|
+
fs.unlinkSync(deletedPath);
|
|
45
|
+
fs.writeFileSync(path.join(knowledgePath, 'modules', 'new.md'), `---\ntags: [token-policy]\n---\n# New\n\nC1 newly documented policy.\n`, 'utf8');
|
|
46
|
+
fs.mkdirSync(layout.getIndexPath(), { recursive: true });
|
|
47
|
+
|
|
48
|
+
const staleRows = [{
|
|
49
|
+
record_id: 'project:project-delta:modules/changed.md:0',
|
|
50
|
+
space_id: 'project:project-delta', entry_id: 'modules/changed.md', chunk_id: 'modules/changed.md:0', chunk_order: 0,
|
|
51
|
+
title: 'Changed', chunk_text: oldChanged, document_hash: sha256(`chunker:${CHUNKER_VERSION}\n${oldChanged}`),
|
|
52
|
+
relevance_score: 1, match_channels: ['semantic'],
|
|
53
|
+
}, {
|
|
54
|
+
record_id: 'project:project-delta:modules/deleted.md:0',
|
|
55
|
+
space_id: 'project:project-delta', entry_id: 'modules/deleted.md', chunk_id: 'modules/deleted.md:0', chunk_order: 0,
|
|
56
|
+
title: 'Deleted', chunk_text: oldDeleted, document_hash: sha256(`chunker:${CHUNKER_VERSION}\n${oldDeleted}`),
|
|
57
|
+
relevance_score: 2, match_channels: ['semantic'],
|
|
58
|
+
}];
|
|
59
|
+
const service = new KnowledgeRetrievalService({
|
|
60
|
+
layout, registryStore: registry, projectStore: projects,
|
|
61
|
+
databaseProvider: () => ({ hybridSearch: async () => staleRows }),
|
|
62
|
+
embedderProvider: () => ({ embedQuery: async () => [1] }),
|
|
63
|
+
});
|
|
64
|
+
const result = await service.retrieveForCommit({
|
|
65
|
+
projectId,
|
|
66
|
+
conversationSnapshot: { turns: [{ userEvents: [{ content: 'Update rotating token policy.' }], assistantEvents: [] }] },
|
|
67
|
+
commitEvidence: { commitSha: '1'.repeat(40), subject: 'token policy', files: [{ status: 'M', path: 'src/token-policy.js' }] },
|
|
68
|
+
});
|
|
69
|
+
const changed = result.entries.find(entry => entry.path === 'modules/changed.md');
|
|
70
|
+
const added = result.entries.find(entry => entry.path === 'modules/new.md');
|
|
71
|
+
assert(changed && /C1 current/.test(changed.content), 'stale index content must be replaced by current Markdown truth');
|
|
72
|
+
assert(!result.entries.some(entry => /Stale token policy/.test(entry.content)), 'stale chunk text must never reach the prompt context');
|
|
73
|
+
assert(!result.entries.some(entry => entry.path === 'modules/deleted.md'), 'deleted Markdown must be removed from stale candidates');
|
|
74
|
+
assert(changed.selectionSignals.includes('markdown-delta-overlay'));
|
|
75
|
+
assert(added && added.selectionSignals.includes('markdown-delta-overlay'), 'new Markdown must enter the in-memory delta overlay immediately');
|
|
76
|
+
assert.strictEqual(result.manifest.health.scopes[0].overlayEntries, 2);
|
|
77
|
+
assert.strictEqual(result.manifest.health.scopes[0].deletedEntries, 1);
|
|
78
|
+
assert.strictEqual(result.manifest.backend, 'ok', 'a stale index may contribute recall while current Markdown remains truth');
|
|
79
|
+
assert.strictEqual(changed.documentHash, sha256(`chunker:${CHUNKER_VERSION}\n${currentChanged}`), 'retrieval manifest must freeze the current document hash');
|
|
80
|
+
assert.strictEqual(projects.readState(projectId).index.dirty, true, 'retrieval must not wait for or mutate index completion state');
|
|
81
|
+
console.log('markdown-delta-overlay-test PASS');
|
|
82
|
+
})().catch(error => {
|
|
83
|
+
console.error(error.stack || error.message);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
9
|
+
const PORT = 7932;
|
|
10
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
11
|
+
const TEMP = fs.mkdtempSync(path.join(os.tmpdir(), 'index-maintenance-api-v2-'));
|
|
12
|
+
const DATA_DIR = path.join(TEMP, 'data');
|
|
13
|
+
const REPO = path.join(TEMP, 'repo');
|
|
14
|
+
|
|
15
|
+
function git(args) { return execFileSync('git', args, { cwd: REPO, encoding: 'utf8' }).trim(); }
|
|
16
|
+
|
|
17
|
+
async function json(method, route, body) {
|
|
18
|
+
const response = await fetch(`${BASE_URL}${route}`, {
|
|
19
|
+
method,
|
|
20
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
21
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
});
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
const data = text ? JSON.parse(text) : {};
|
|
25
|
+
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(data)}`);
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
(async () => {
|
|
30
|
+
fs.mkdirSync(REPO, { recursive: true });
|
|
31
|
+
git(['init', '--initial-branch=main']);
|
|
32
|
+
git(['config', 'user.name', 'Index Test']);
|
|
33
|
+
git(['config', 'user.email', 'index@example.test']);
|
|
34
|
+
fs.writeFileSync(path.join(REPO, 'README.md'), '# source\n', 'utf8');
|
|
35
|
+
git(['add', '.']);
|
|
36
|
+
git(['commit', '-m', 'baseline']);
|
|
37
|
+
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir: DATA_DIR, tag: 'index-maintenance-v2', extraEnv: { KB_EMBEDDING_FAKE: '1' } });
|
|
38
|
+
try {
|
|
39
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
40
|
+
try { await json('GET', '/api/health'); break; } catch {
|
|
41
|
+
if (attempt === 99) throw new Error('server did not start');
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(TEMP, 'knowledge') } });
|
|
46
|
+
const imported = await json('POST', '/api/projects/import', { projectId: 'maintenance-demo', localPath: REPO, displayName: 'Maintenance demo' });
|
|
47
|
+
const kbPath = imported.config.knowledgePath;
|
|
48
|
+
fs.mkdirSync(path.join(kbPath, 'modules'), { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(kbPath, 'GOAL.md'), '# Goal\n\nBuild reliable derived indexes.\n', 'utf8');
|
|
50
|
+
fs.writeFileSync(path.join(kbPath, 'modules', 'core.md'), '# Core\n\nSingle writer atomic rebuild content.\n', 'utf8');
|
|
51
|
+
const markdownBefore = fs.readFileSync(path.join(kbPath, 'modules', 'core.md'), 'utf8');
|
|
52
|
+
|
|
53
|
+
const before = await json('GET', '/api/knowledge/maintenance');
|
|
54
|
+
assert.strictEqual(before.indexPath, path.join(DATA_DIR, 'index', 'knowledge.lancedb'));
|
|
55
|
+
const rebuilt = await json('POST', '/api/knowledge/maintenance/rebuild', {});
|
|
56
|
+
assert(rebuilt.ok && rebuilt.validation.ok, JSON.stringify(rebuilt));
|
|
57
|
+
assert(fs.existsSync(before.indexPath));
|
|
58
|
+
assert.strictEqual(fs.readFileSync(path.join(kbPath, 'modules', 'core.md'), 'utf8'), markdownBefore, 'index maintenance must not rewrite authoritative Markdown');
|
|
59
|
+
let search = await json('POST', '/api/knowledge/search', { projectId: 'maintenance-demo', query: 'atomic rebuild', limit: 5 });
|
|
60
|
+
assert.strictEqual(search.source, 'knowledge-retrieval-service');
|
|
61
|
+
assert.strictEqual(search.backend, 'hybrid+markdown-truth');
|
|
62
|
+
assert(search.results.some(item => item.entry_id === 'modules/core.md'));
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(path.join(kbPath, 'modules', 'core.md'), `${markdownBefore}\nSecond rebuild evidence.\n`, 'utf8');
|
|
65
|
+
const second = await json('POST', '/api/knowledge/maintenance/rebuild', {});
|
|
66
|
+
assert(second.ok && second.backup && fs.existsSync(second.backup), 'replacing a live index should retain the previous index under recovery');
|
|
67
|
+
search = await json('POST', '/api/knowledge/search', { projectId: 'maintenance-demo', query: 'Second rebuild evidence', limit: 5 });
|
|
68
|
+
assert(search.results.some(item => /Second rebuild evidence/.test(item.chunk_text)));
|
|
69
|
+
const after = await json('GET', '/api/knowledge/maintenance');
|
|
70
|
+
assert(after.projects.every(project => project.index.dirty === false));
|
|
71
|
+
console.log('markdown-maintenance-api-test: PASS');
|
|
72
|
+
} finally {
|
|
73
|
+
spawned.child.kill();
|
|
74
|
+
}
|
|
75
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { canonicalRelativePath, validateMarkdown } = require('../lib/knowledge-promotion');
|
|
3
|
+
|
|
4
|
+
assert.strictEqual(canonicalRelativePath('modules/core.md'), 'modules/core.md');
|
|
5
|
+
for (const invalid of ['../outside.md', '/absolute.md', 'modules/00-index.md', 'module.txt', 'CON.md', 'modules/file.md:stream']) {
|
|
6
|
+
assert.throws(() => canonicalRelativePath(invalid), undefined, `unsafe promotion path should fail: ${invalid}`);
|
|
7
|
+
}
|
|
8
|
+
assert.doesNotThrow(() => validateMarkdown('# Verified fact\n\nEvidence-backed content.\n', 'modules/core.md'));
|
|
9
|
+
assert.throws(() => validateMarkdown('# Placeholder\n\nTODO: invent this later.\n', 'modules/core.md'), /placeholder/i);
|
|
10
|
+
assert.throws(() => validateMarkdown('# Title\n\n\u0000invalid', 'modules/core.md'), /control/i);
|
|
11
|
+
console.log('markdown-maintenance-test: PASS');
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn, spawnSync } = require('child_process');
|
|
6
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
7
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
8
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
9
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
10
|
+
const { ConversationStore } = require('../lib/conversation-store');
|
|
11
|
+
const { KnowledgeDatabase } = require('../lib/knowledge-db');
|
|
12
|
+
const { EMBEDDING_DIMENSIONS } = require('../lib/knowledge-schema');
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
15
|
+
const MCP_BIN = path.join(ROOT, 'bin', 'project-knowledge-mcp.js');
|
|
16
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-mcp-v2-'));
|
|
17
|
+
const dataDir = path.join(temp, 'data');
|
|
18
|
+
const repo = path.join(temp, 'repo');
|
|
19
|
+
const vectorRepo = path.join(temp, 'vector-repo');
|
|
20
|
+
const markdownKb = path.join(temp, 'knowledge-markdown');
|
|
21
|
+
const vectorKb = path.join(temp, 'knowledge-vector');
|
|
22
|
+
|
|
23
|
+
function write(filePath, content) {
|
|
24
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
25
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function git(cwd, args) {
|
|
29
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
30
|
+
assert.strictEqual(result.status, 0, result.stderr);
|
|
31
|
+
return String(result.stdout || '').trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fakeVector(text) {
|
|
35
|
+
const vector = new Array(EMBEDDING_DIMENSIONS).fill(0);
|
|
36
|
+
for (let index = 0; index < String(text).length; index += 1) vector[String(text).charCodeAt(index) % vector.length] += 1;
|
|
37
|
+
const norm = Math.sqrt(vector.reduce((sum, item) => sum + item * item, 0)) || 1;
|
|
38
|
+
return vector.map(item => item / norm);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createMcpClient() {
|
|
42
|
+
const child = spawn(process.execPath, [MCP_BIN], {
|
|
43
|
+
cwd: repo,
|
|
44
|
+
env: { ...process.env, KB_DATA_DIR: dataDir, KB_SKIP_MIGRATION: '1', KB_EMBEDDING_FAKE: '1' },
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
});
|
|
48
|
+
let stderr = '';
|
|
49
|
+
let buffer = '';
|
|
50
|
+
let nextId = 1;
|
|
51
|
+
const pending = new Map();
|
|
52
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
53
|
+
child.stdout.on('data', chunk => {
|
|
54
|
+
buffer += chunk;
|
|
55
|
+
let newline;
|
|
56
|
+
while ((newline = buffer.indexOf('\n')) >= 0) {
|
|
57
|
+
const line = buffer.slice(0, newline).trim();
|
|
58
|
+
buffer = buffer.slice(newline + 1);
|
|
59
|
+
if (!line) continue;
|
|
60
|
+
const message = JSON.parse(line);
|
|
61
|
+
const entry = pending.get(message.id);
|
|
62
|
+
if (entry) { entry(message); pending.delete(message.id); }
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const request = (method, params = {}) => new Promise((resolve, reject) => {
|
|
66
|
+
const id = nextId++;
|
|
67
|
+
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`MCP timeout: ${method}\n${stderr}`)); }, 15_000);
|
|
68
|
+
pending.set(id, message => { clearTimeout(timer); resolve(message); });
|
|
69
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
70
|
+
});
|
|
71
|
+
await request('initialize', { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'test', version: '1' } });
|
|
72
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} })}\n`);
|
|
73
|
+
return { child, request, stderr: () => stderr };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
(async () => {
|
|
77
|
+
for (const cwd of [repo, vectorRepo]) {
|
|
78
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
79
|
+
git(cwd, ['init', '--initial-branch=main']);
|
|
80
|
+
git(cwd, ['config', 'user.email', 'mcp@example.test']);
|
|
81
|
+
git(cwd, ['config', 'user.name', 'MCP Test']);
|
|
82
|
+
write(path.join(cwd, 'README.md'), '# source\n');
|
|
83
|
+
git(cwd, ['add', 'README.md']);
|
|
84
|
+
git(cwd, ['commit', '-m', 'baseline']);
|
|
85
|
+
}
|
|
86
|
+
write(path.join(markdownKb, 'README.md'), '# Demo knowledge\n\nThis project uses rotating refresh tokens.\n');
|
|
87
|
+
write(path.join(markdownKb, 'modules', 'auth.md'), '# Authentication\n\nRefresh tokens rotate after every successful renewal.\n');
|
|
88
|
+
write(path.join(markdownKb, 'changes', 'auth.md'), '# Authentication update\n\nIntroduced rotating refresh tokens.\n');
|
|
89
|
+
write(path.join(vectorKb, 'modules', 'payments.md'), '# Payment tokens\n\nPayment tokens expire after fifteen minutes.\n');
|
|
90
|
+
|
|
91
|
+
const layout = new StorageLayout({ dataDir });
|
|
92
|
+
const settingsStore = new SettingsStore({ layout });
|
|
93
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
94
|
+
const projectStore = new ProjectStore({ layout });
|
|
95
|
+
await settingsStore.initialize({ knowledge: { rootPath: path.join(temp, 'future-root') } });
|
|
96
|
+
await registryStore.initialize();
|
|
97
|
+
await projectStore.create('project-demo', { displayName: 'Demo', storageName: 'demo', repoPath: repo, knowledgePath: markdownKb, legacyExtensions: { slug: 'demo' } }, { trackingStartCommit: git(repo, ['rev-parse', 'HEAD']), index: { dirty: true, generation: 1 } });
|
|
98
|
+
await registryStore.add('project-demo', { displayNameSnapshot: 'Demo' });
|
|
99
|
+
await projectStore.create('project-vector', { displayName: 'Vector', storageName: 'vector', repoPath: vectorRepo, knowledgePath: vectorKb, legacyExtensions: { slug: 'vector' } }, { trackingStartCommit: git(vectorRepo, ['rev-parse', 'HEAD']) });
|
|
100
|
+
await registryStore.add('project-vector', { displayNameSnapshot: 'Vector' });
|
|
101
|
+
|
|
102
|
+
const database = new KnowledgeDatabase({ dbPath: layout.getIndexPath(), maintenancePath: layout.getRuntimePath('index-maintenance.json') });
|
|
103
|
+
await database.replaceEntry('project:project-vector', 'modules/payments.md', [{ chunkOrder: 0, title: 'Payment tokens', chunkText: 'Payment tokens expire after fifteen minutes.', vector: fakeVector('payment tokens'), sourceCommit: 'abc123' }]);
|
|
104
|
+
await database.ensureSearchIndexes();
|
|
105
|
+
await database.close();
|
|
106
|
+
|
|
107
|
+
const client = await createMcpClient();
|
|
108
|
+
try {
|
|
109
|
+
const listed = await client.request('tools/list');
|
|
110
|
+
assert.strictEqual(listed.result.tools.length, 6);
|
|
111
|
+
assert(listed.result.tools.some(tool => tool.name === 'project_knowledge_record_requirement'));
|
|
112
|
+
|
|
113
|
+
let response = await client.request('tools/call', { name: 'project_knowledge_resolve', arguments: { repoPath: repo } });
|
|
114
|
+
assert.strictEqual(response.result.structuredContent.projectId, 'project-demo');
|
|
115
|
+
assert.strictEqual(response.result.structuredContent.knowledgePath, markdownKb);
|
|
116
|
+
assert.strictEqual(response.result.structuredContent.indexPath, layout.getIndexPath());
|
|
117
|
+
|
|
118
|
+
response = await client.request('tools/call', { name: 'project_knowledge_search', arguments: { projectId: 'project-demo', query: 'refresh tokens' } });
|
|
119
|
+
assert.strictEqual(response.result.structuredContent.source, 'knowledge-retrieval-service');
|
|
120
|
+
assert.strictEqual(response.result.structuredContent.backend, 'hybrid+markdown-truth');
|
|
121
|
+
assert.strictEqual(response.result.structuredContent.health.scopes[0].dirty, true);
|
|
122
|
+
assert(response.result.structuredContent.results.some(result => result.entry_id === 'modules/auth.md'));
|
|
123
|
+
|
|
124
|
+
response = await client.request('tools/call', { name: 'project_knowledge_search', arguments: { project: 'vector', query: 'payment tokens' } });
|
|
125
|
+
assert.strictEqual(response.result.structuredContent.source, 'knowledge-retrieval-service');
|
|
126
|
+
assert.strictEqual(response.result.structuredContent.backend, 'hybrid+markdown-truth');
|
|
127
|
+
assert(response.result.structuredContent.results.some(result => result.entry_id === 'modules/payments.md'));
|
|
128
|
+
|
|
129
|
+
response = await client.request('tools/call', { name: 'project_knowledge_get', arguments: { projectId: 'project-demo', entry: 'modules/auth.md' } });
|
|
130
|
+
assert.match(response.result.structuredContent.chunks[0].chunk_text, /rotate/i);
|
|
131
|
+
response = await client.request('tools/call', { name: 'project_knowledge_history', arguments: { projectId: 'project-demo' } });
|
|
132
|
+
assert.strictEqual(response.result.structuredContent.results[0].entry_id, 'changes/auth.md');
|
|
133
|
+
|
|
134
|
+
const stateBefore = fs.readFileSync(layout.getProjectStatePath('project-demo'), 'utf8');
|
|
135
|
+
response = await client.request('tools/call', { name: 'project_knowledge_record_requirement', arguments: { projectId: 'project-demo', repoPath: repo, client: 'codex', sessionId: 'session-mcp', text: 'Keep refresh token rotation documented.' } });
|
|
136
|
+
assert.strictEqual(response.result.structuredContent.projectId, 'project-demo');
|
|
137
|
+
assert.strictEqual(new ConversationStore({ layout, projectStore }).readEvents('project-demo').length, 1);
|
|
138
|
+
assert.strictEqual(fs.existsSync(layout.getProjectRequirementsPath('project-demo')), false, 'explicit MCP adapter must not create legacy requirements.jsonl');
|
|
139
|
+
assert.strictEqual(fs.readFileSync(layout.getProjectStatePath('project-demo'), 'utf8'), stateBefore, 'requirement tool must not trigger analysis or mutate state');
|
|
140
|
+
assert(!fs.existsSync(layout.getRuntimePath('staging')));
|
|
141
|
+
|
|
142
|
+
response = await client.request('tools/call', { name: 'project_knowledge_get', arguments: { projectId: 'project-demo', entry: '../settings.json' } });
|
|
143
|
+
assert.strictEqual(response.result.isError, true);
|
|
144
|
+
console.log('mcp-server-test PASS');
|
|
145
|
+
} finally {
|
|
146
|
+
client.child.stdin.end();
|
|
147
|
+
await new Promise(resolve => client.child.once('exit', resolve));
|
|
148
|
+
}
|
|
149
|
+
})().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => fs.rmSync(temp, { recursive: true, force: true }));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Tests for model-context-windows: lookup table + resolution priority chain.
|
|
2
|
+
// profile.contextWindow (explicit override)
|
|
3
|
+
// → model name table (anthropic / minimax / glm / gpt / deepseek / qwen / gemini)
|
|
4
|
+
// → CLAUDE_CONTEXT_WINDOW env
|
|
5
|
+
// → 200000 fallback
|
|
6
|
+
|
|
7
|
+
const { lookupContextWindow, resolveContextWindow } = require('../lib/model-context-windows');
|
|
8
|
+
|
|
9
|
+
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
10
|
+
|
|
11
|
+
(() => {
|
|
12
|
+
try {
|
|
13
|
+
// ---- lookupContextWindow: family prefix matching ----
|
|
14
|
+
assert(lookupContextWindow('claude-opus-4-7') === 200000, 'claude-opus-4-7 → 200000');
|
|
15
|
+
assert(lookupContextWindow('claude-sonnet-4-6') === 200000, 'claude-sonnet-4-6 → 200000');
|
|
16
|
+
assert(lookupContextWindow('claude-haiku-4-5') === 200000, 'claude-haiku-4-5 → 200000');
|
|
17
|
+
assert(lookupContextWindow('claude-3-5-sonnet-20241022') === 200000, 'claude-3-5 → 200000');
|
|
18
|
+
|
|
19
|
+
assert(lookupContextWindow('glm-5.2') === 1048576, 'glm-5.2 → 1M');
|
|
20
|
+
assert(lookupContextWindow('GLM-4.6') === 1048576, 'GLM-4.6 (uppercase) → 1M');
|
|
21
|
+
assert(lookupContextWindow('glm-4.5-air') === 128000, 'glm-4.5-air → 128000');
|
|
22
|
+
assert(lookupContextWindow('glm-4-plus') === 128000, 'glm-4-plus → 128000');
|
|
23
|
+
|
|
24
|
+
assert(lookupContextWindow('MiniMax-M3') === 1048576, 'MiniMax-M3 → 1M');
|
|
25
|
+
assert(lookupContextWindow('minimax-m2') === 1048576, 'minimax-m2 → 1M');
|
|
26
|
+
|
|
27
|
+
assert(lookupContextWindow('gpt-5-mini') === 200000, 'gpt-5-mini → 200000');
|
|
28
|
+
assert(lookupContextWindow('gpt-4o-2024-08-06') === 128000, 'gpt-4o → 128000');
|
|
29
|
+
assert(lookupContextWindow('gpt-4.1-2025-04-14') === 1047576, 'gpt-4.1 → 1047576');
|
|
30
|
+
|
|
31
|
+
assert(lookupContextWindow('deepseek-v3') === 128000, 'deepseek-v3 → 128000');
|
|
32
|
+
assert(lookupContextWindow('deepseek-r1') === 128000, 'deepseek-r1 → 128000');
|
|
33
|
+
|
|
34
|
+
assert(lookupContextWindow('qwen3-coder') === 131072, 'qwen3 → 131072');
|
|
35
|
+
assert(lookupContextWindow('gemini-2.5-pro') === 1048576, 'gemini-2.5 → 1M');
|
|
36
|
+
|
|
37
|
+
assert(lookupContextWindow('totally-unknown-model') === null, 'unknown → null');
|
|
38
|
+
assert(lookupContextWindow('') === null, 'empty → null');
|
|
39
|
+
assert(lookupContextWindow(null) === null, 'null → null');
|
|
40
|
+
assert(lookupContextWindow(undefined) === null, 'undefined → null');
|
|
41
|
+
|
|
42
|
+
// ---- resolveContextWindow: priority chain ----
|
|
43
|
+
|
|
44
|
+
// 1. profile.contextWindow wins over everything
|
|
45
|
+
assert(resolveContextWindow({ profileContextWindow: 500000, model: 'claude-opus-4' }) === 500000,
|
|
46
|
+
'explicit profileContextWindow must override table');
|
|
47
|
+
|
|
48
|
+
// 2. table wins when profile is empty
|
|
49
|
+
assert(resolveContextWindow({ model: 'glm-5.2' }) === 1048576,
|
|
50
|
+
'table hit should be used when profileContextWindow empty');
|
|
51
|
+
|
|
52
|
+
// 3. env wins when profile + table both miss
|
|
53
|
+
assert(resolveContextWindow({ model: 'unknown-model', env: { CLAUDE_CONTEXT_WINDOW: '99999' } }) === 99999,
|
|
54
|
+
'env should be used when table misses');
|
|
55
|
+
|
|
56
|
+
// 4. fallback (200000 default) when nothing matches
|
|
57
|
+
assert(resolveContextWindow({ model: 'unknown-model' }) === 200000,
|
|
58
|
+
'default fallback should be 200000');
|
|
59
|
+
|
|
60
|
+
// 5. profile.contextWindow = 0 / negative treated as unset
|
|
61
|
+
assert(resolveContextWindow({ profileContextWindow: 0, model: 'glm-5.2' }) === 1048576,
|
|
62
|
+
'profileContextWindow=0 should fall through to table');
|
|
63
|
+
assert(resolveContextWindow({ profileContextWindow: -1, model: 'glm-5.2' }) === 1048576,
|
|
64
|
+
'negative profileContextWindow should fall through to table');
|
|
65
|
+
|
|
66
|
+
// 6. non-finite profileContextWindow (NaN, string) ignored
|
|
67
|
+
assert(resolveContextWindow({ profileContextWindow: NaN, model: 'minimax-m3' }) === 1048576,
|
|
68
|
+
'NaN profileContextWindow should fall through');
|
|
69
|
+
assert(resolveContextWindow({ profileContextWindow: 'abc', model: 'minimax-m3' }) === 1048576,
|
|
70
|
+
'non-numeric string profileContextWindow should fall through');
|
|
71
|
+
|
|
72
|
+
// 7. custom fallback honored when nothing else matches
|
|
73
|
+
assert(resolveContextWindow({ model: 'unknown', fallback: 12345 }) === 12345,
|
|
74
|
+
'custom fallback should be respected');
|
|
75
|
+
|
|
76
|
+
// 8. no args at all → fallback
|
|
77
|
+
assert(resolveContextWindow() === 200000,
|
|
78
|
+
'no-args call should hit default fallback');
|
|
79
|
+
|
|
80
|
+
// 9. fractional context window floored
|
|
81
|
+
assert(resolveContextWindow({ profileContextWindow: 123456.789, model: 'x' }) === 123456,
|
|
82
|
+
'fractional contextWindow should be floored');
|
|
83
|
+
|
|
84
|
+
console.log('model-context-windows test passed');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error('model-context-windows test failed:', e.message);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
})();
|