@sanqianx/project-knowledge 5.0.19 → 5.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,200 +1,200 @@
|
|
|
1
|
-
// TASK-006: Context pack builder test
|
|
2
|
-
// Run: node _site/_test/context-pack-test.js
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const { spawn } = require('child_process');
|
|
7
|
-
const { spawnServer } = require('./helpers/spawn-server');
|
|
8
|
-
const freePort = require('./helpers/free-port');
|
|
9
|
-
const { makeRepo, git } = require('./fixtures/make-git-repos');
|
|
10
|
-
|
|
11
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
12
|
-
const SERVER = path.join(ROOT, '_site', 'server.js');
|
|
13
|
-
// Pre-create a temp data dir and seed it BEFORE requiring any lib modules
|
|
14
|
-
// that capture getDataDir() at module load. Both the test process and
|
|
15
|
-
// the spawned server will use this dir.
|
|
16
|
-
const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), `kb-data-context-pack-${process.pid}-`));
|
|
17
|
-
process.env.KB_DATA_DIR = DATA_DIR;
|
|
18
|
-
require('../lib/data-dir')._resetCache();
|
|
19
|
-
try { fs.copyFileSync(path.join(ROOT, 'claude-prompts.json'), path.join(DATA_DIR, 'claude-prompts.json')); } catch {}
|
|
20
|
-
|
|
21
|
-
const { buildContextPack, isSafePath, PACKAGE_CONFIG_FILES } = require('../lib/context-pack-builder');
|
|
22
|
-
let PROJECTS_JSON = path.join(DATA_DIR, 'projects.json');
|
|
23
|
-
let PORT, BASE_URL;
|
|
24
|
-
const TEMP_SLUG = 'task-006-temp';
|
|
25
|
-
|
|
26
|
-
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
27
|
-
|
|
28
|
-
async function waitForServer() {
|
|
29
|
-
const deadline = Date.now() + 15000;
|
|
30
|
-
let lastError;
|
|
31
|
-
while (Date.now() < deadline) {
|
|
32
|
-
try {
|
|
33
|
-
const res = await fetch(`${BASE_URL}/api/state`);
|
|
34
|
-
if (res.ok) return;
|
|
35
|
-
lastError = new Error(`HTTP ${res.status}`);
|
|
36
|
-
} catch (e) { lastError = e; }
|
|
37
|
-
await new Promise(r => setTimeout(r, 250));
|
|
38
|
-
}
|
|
39
|
-
throw lastError || new Error('server did not start');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function json(method, url, body) {
|
|
43
|
-
const res = await fetch(`${BASE_URL}${url}`, {
|
|
44
|
-
method,
|
|
45
|
-
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
46
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
47
|
-
});
|
|
48
|
-
const text = await res.text();
|
|
49
|
-
let data = {};
|
|
50
|
-
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
51
|
-
return { res, data };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function cleanup() {
|
|
55
|
-
// The whole test data directory is isolated and removed after the server exits.
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
(async () => {
|
|
59
|
-
PORT = process.env.KB_CTX_TEST_PORT || await freePort();
|
|
60
|
-
BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
61
|
-
// 1. Path safety
|
|
62
|
-
assert(isSafePath('/tmp/proj', 'a/b.md'), 'safe path should pass');
|
|
63
|
-
assert(!isSafePath('/tmp/proj', '../escape.md'), 'parent traversal should fail');
|
|
64
|
-
assert(!isSafePath('/tmp/proj', '/etc/passwd'), 'absolute outside should fail');
|
|
65
|
-
// Sibling that is not a sub-path: resolve it relative to the project root and check.
|
|
66
|
-
const siblingResolved = require('path').resolve('/tmp/proj', '../proj-other/x.md');
|
|
67
|
-
assert(!siblingResolved.startsWith('/tmp/proj/') && siblingResolved !== '/tmp/proj',
|
|
68
|
-
'sibling-of-project should not pass when constructed as a traversal');
|
|
69
|
-
assert(Array.isArray(PACKAGE_CONFIG_FILES) && PACKAGE_CONFIG_FILES.includes('package.json'), 'package config list ok');
|
|
70
|
-
|
|
71
|
-
// 2. Unit test: build a context pack for a feature-commit fixture
|
|
72
|
-
const repo = makeRepo({ kind: 'feature-commit' });
|
|
73
|
-
const kbBase = path.join(DATA_DIR, 'projects', TEMP_SLUG);
|
|
74
|
-
fs.mkdirSync(kbBase, { recursive: true });
|
|
75
|
-
// Lay down final minimal KB structure with a module doc that references a changed file.
|
|
76
|
-
fs.mkdirSync(path.join(kbBase, 'modules'), { recursive: true });
|
|
77
|
-
fs.mkdirSync(path.join(kbBase, 'changes'), { recursive: true });
|
|
78
|
-
fs.writeFileSync(path.join(kbBase, 'GOAL.md'), '# Goal — test\n\nGoal text for tests.\n');
|
|
79
|
-
fs.writeFileSync(path.join(kbBase, 'ARCHITECTURE.md'), '# Analysis — test\n');
|
|
80
|
-
fs.writeFileSync(path.join(kbBase, 'modules', 'feature.md'), 'See `src/feature.ts` for the implementation.\n');
|
|
81
|
-
fs.writeFileSync(path.join(kbBase, 'README.md'), '# Project — test\n');
|
|
82
|
-
// package.json lives in the source repo (the fixture already has a src/, but no package.json)
|
|
83
|
-
fs.writeFileSync(path.join(repo.path, 'package.json'), '{"name":"fixture"}');
|
|
84
|
-
|
|
85
|
-
const commits = repo.commits.slice().reverse(); // chronological
|
|
86
|
-
const pack = await buildContextPack({
|
|
87
|
-
project: {
|
|
88
|
-
slug: TEMP_SLUG,
|
|
89
|
-
kbPath: kbBase,
|
|
90
|
-
gitPath: repo.path,
|
|
91
|
-
localPath: repo.path,
|
|
92
|
-
},
|
|
93
|
-
runId: 'run-unit-1',
|
|
94
|
-
trigger: 'commits',
|
|
95
|
-
commits,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
assert(pack.schema === 'context-pack/v1', 'pack schema should be v1');
|
|
99
|
-
assert(pack.runId === 'run-unit-1', 'runId should be honored');
|
|
100
|
-
assert(pack.trigger === 'commits', 'trigger should be commits');
|
|
101
|
-
assert(pack.commitCount === commits.length, 'commitCount should match');
|
|
102
|
-
assert(pack.range, 'range should be set');
|
|
103
|
-
|
|
104
|
-
// Required entries
|
|
105
|
-
const goalEntry = pack.entries.find(e => e.path === 'GOAL.md');
|
|
106
|
-
assert(goalEntry, 'pack should include GOAL.md');
|
|
107
|
-
assert(goalEntry.kind === 'goal', 'goal entry kind should be goal');
|
|
108
|
-
assert(goalEntry.reason, 'goal entry should have a reason');
|
|
109
|
-
const pkg = pack.entries.find(e => e.path === 'package.json');
|
|
110
|
-
assert(pkg && pkg.kind === 'package-config', 'pack should include package.json as package-config');
|
|
111
|
-
const changed = pack.entries.find(e => e.path === 'src/feature.ts');
|
|
112
|
-
assert(changed && changed.kind === 'git-changed', 'pack should include changed file src/feature.ts');
|
|
113
|
-
const related = pack.entries.find(e => e.path === 'modules/feature.md');
|
|
114
|
-
assert(related && related.kind === 'module-doc', 'pack should include related module doc');
|
|
115
|
-
// Every entry must have a reason
|
|
116
|
-
for (const e of pack.entries) {
|
|
117
|
-
assert(typeof e.reason === 'string' && e.reason.length > 0, `entry ${e.path} missing reason`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Disk artifacts
|
|
121
|
-
const onDisk = JSON.parse(fs.readFileSync(path.join(DATA_DIR, '_ai', TEMP_SLUG, 'context-packs', 'run-unit-1', 'context-pack.json'), 'utf-8'));
|
|
122
|
-
assert(onDisk.runId === 'run-unit-1', 'on-disk runId should match');
|
|
123
|
-
|
|
124
|
-
// 3. Large / binary file is summarized or skipped
|
|
125
|
-
const repo2 = makeRepo({ kind: 'binary-commit' });
|
|
126
|
-
const kbBase2 = path.join(DATA_DIR, 'projects', TEMP_SLUG + '-2');
|
|
127
|
-
fs.mkdirSync(kbBase2, { recursive: true });
|
|
128
|
-
fs.writeFileSync(path.join(kbBase2, 'GOAL.md'), '# Goal\n');
|
|
129
|
-
const commits2 = repo2.commits.slice().reverse();
|
|
130
|
-
const pack2 = await buildContextPack({
|
|
131
|
-
project: { slug: TEMP_SLUG + '-2', kbPath: kbBase2, gitPath: repo2.path, localPath: repo2.path },
|
|
132
|
-
runId: 'run-binary-1',
|
|
133
|
-
trigger: 'commits',
|
|
134
|
-
commits: commits2,
|
|
135
|
-
});
|
|
136
|
-
const big = pack2.entries.find(e => e.path === 'big.bin');
|
|
137
|
-
if (big) {
|
|
138
|
-
assert(big.binary || big.skipped === 'too-large' || big.truncated, 'big file should be marked binary/skipped/truncated');
|
|
139
|
-
}
|
|
140
|
-
fs.rmSync(kbBase2, { recursive: true, force: true });
|
|
141
|
-
repo2.cleanup();
|
|
142
|
-
|
|
143
|
-
// 4. Path-traversal attempt is rejected
|
|
144
|
-
const ok = isSafePath(kbBase, '..\\..\\escape.md');
|
|
145
|
-
// On Windows, ..\\ becomes normalized. The resolved path should still be inside kbBase.
|
|
146
|
-
// Test the explicit scenario:
|
|
147
|
-
assert(!isSafePath(kbBase, '..\\..\\..\\..\\Windows\\evil.txt') || path.resolve(kbBase, '..\\..\\..\\..\\Windows\\evil.txt').startsWith(path.resolve(kbBase)),
|
|
148
|
-
'path traversal must be rejected');
|
|
149
|
-
|
|
150
|
-
repo.cleanup();
|
|
151
|
-
|
|
152
|
-
// 5. Server tests
|
|
153
|
-
const _spawned = spawnServer({ root: ROOT, port: Number(PORT), dataDir: DATA_DIR, tag: 'context-pack', });
|
|
154
|
-
const child = _spawned.child;
|
|
155
|
-
let serverOutput = '';
|
|
156
|
-
child.stdout.on('data', d => { serverOutput += d.toString(); });
|
|
157
|
-
child.stderr.on('data', d => { serverOutput += d.toString(); });
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
await cleanup();
|
|
161
|
-
await waitForServer();
|
|
162
|
-
let r = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(DATA_DIR, 'knowledge') } });
|
|
163
|
-
assert(r.res.ok, `knowledge root setup should succeed: ${JSON.stringify(r.data)}`);
|
|
164
|
-
|
|
165
|
-
// 6. POST /api/projects/:slug/context-pack for an initial pack
|
|
166
|
-
const initRepo = makeRepo({ kind: 'one-commit' });
|
|
167
|
-
const slug = TEMP_SLUG;
|
|
168
|
-
r = await json('POST', '/api/projects/import', {
|
|
169
|
-
projectId: slug,
|
|
170
|
-
displayName: 'TASK-006',
|
|
171
|
-
localPath: initRepo.path,
|
|
172
|
-
});
|
|
173
|
-
assert(r.res.ok, `v2 import should succeed: ${JSON.stringify(r.data)}`);
|
|
174
|
-
const kbPath = r.data.config.knowledgePath;
|
|
175
|
-
fs.writeFileSync(path.join(kbPath, 'GOAL.md'), '# Goal — server test\n');
|
|
176
|
-
|
|
177
|
-
r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'initial' });
|
|
178
|
-
assert(r.res.ok, 'context-pack initial should succeed');
|
|
179
|
-
assert(r.data.contextPack.entries.length > 0, 'context pack should have entries');
|
|
180
|
-
assert(r.data.contextPack.entries.find(e => e.path === 'GOAL.md'), 'goal should be in pack');
|
|
181
|
-
|
|
182
|
-
r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'commits' });
|
|
183
|
-
assert(r.res.ok, 'context-pack commits should succeed');
|
|
184
|
-
assert(r.data.contextPack.commitCount === 0, 'commits trigger should not treat pre-import history as pending');
|
|
185
|
-
|
|
186
|
-
// 7. Bad slug
|
|
187
|
-
r = await json('POST', '/api/projects/%5Ebad/context-pack', {});
|
|
188
|
-
assert(!r.res.ok && r.res.status === 400, 'bad project id should 400');
|
|
189
|
-
|
|
190
|
-
initRepo.cleanup();
|
|
191
|
-
console.log('TASK-006 context pack test passed');
|
|
192
|
-
} catch (e) {
|
|
193
|
-
console.error('TASK-006 context pack test failed:', e.message);
|
|
194
|
-
if (serverOutput) console.error(serverOutput);
|
|
195
|
-
process.exitCode = 1;
|
|
196
|
-
} finally {
|
|
197
|
-
await cleanup().catch(() => {});
|
|
198
|
-
child.kill();
|
|
199
|
-
}
|
|
200
|
-
})();
|
|
1
|
+
// TASK-006: Context pack builder test
|
|
2
|
+
// Run: node _site/_test/context-pack-test.js
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
8
|
+
const freePort = require('./helpers/free-port');
|
|
9
|
+
const { makeRepo, git } = require('./fixtures/make-git-repos');
|
|
10
|
+
|
|
11
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
12
|
+
const SERVER = path.join(ROOT, '_site', 'server.js');
|
|
13
|
+
// Pre-create a temp data dir and seed it BEFORE requiring any lib modules
|
|
14
|
+
// that capture getDataDir() at module load. Both the test process and
|
|
15
|
+
// the spawned server will use this dir.
|
|
16
|
+
const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), `kb-data-context-pack-${process.pid}-`));
|
|
17
|
+
process.env.KB_DATA_DIR = DATA_DIR;
|
|
18
|
+
require('../lib/data-dir')._resetCache();
|
|
19
|
+
try { fs.copyFileSync(path.join(ROOT, 'claude-prompts.json'), path.join(DATA_DIR, 'claude-prompts.json')); } catch {}
|
|
20
|
+
|
|
21
|
+
const { buildContextPack, isSafePath, PACKAGE_CONFIG_FILES } = require('../lib/context-pack-builder');
|
|
22
|
+
let PROJECTS_JSON = path.join(DATA_DIR, 'projects.json');
|
|
23
|
+
let PORT, BASE_URL;
|
|
24
|
+
const TEMP_SLUG = 'task-006-temp';
|
|
25
|
+
|
|
26
|
+
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
27
|
+
|
|
28
|
+
async function waitForServer() {
|
|
29
|
+
const deadline = Date.now() + 15000;
|
|
30
|
+
let lastError;
|
|
31
|
+
while (Date.now() < deadline) {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${BASE_URL}/api/state`);
|
|
34
|
+
if (res.ok) return;
|
|
35
|
+
lastError = new Error(`HTTP ${res.status}`);
|
|
36
|
+
} catch (e) { lastError = e; }
|
|
37
|
+
await new Promise(r => setTimeout(r, 250));
|
|
38
|
+
}
|
|
39
|
+
throw lastError || new Error('server did not start');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function json(method, url, body) {
|
|
43
|
+
const res = await fetch(`${BASE_URL}${url}`, {
|
|
44
|
+
method,
|
|
45
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
46
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
47
|
+
});
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
let data = {};
|
|
50
|
+
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
51
|
+
return { res, data };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function cleanup() {
|
|
55
|
+
// The whole test data directory is isolated and removed after the server exits.
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
(async () => {
|
|
59
|
+
PORT = process.env.KB_CTX_TEST_PORT || await freePort();
|
|
60
|
+
BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
61
|
+
// 1. Path safety
|
|
62
|
+
assert(isSafePath('/tmp/proj', 'a/b.md'), 'safe path should pass');
|
|
63
|
+
assert(!isSafePath('/tmp/proj', '../escape.md'), 'parent traversal should fail');
|
|
64
|
+
assert(!isSafePath('/tmp/proj', '/etc/passwd'), 'absolute outside should fail');
|
|
65
|
+
// Sibling that is not a sub-path: resolve it relative to the project root and check.
|
|
66
|
+
const siblingResolved = require('path').resolve('/tmp/proj', '../proj-other/x.md');
|
|
67
|
+
assert(!siblingResolved.startsWith('/tmp/proj/') && siblingResolved !== '/tmp/proj',
|
|
68
|
+
'sibling-of-project should not pass when constructed as a traversal');
|
|
69
|
+
assert(Array.isArray(PACKAGE_CONFIG_FILES) && PACKAGE_CONFIG_FILES.includes('package.json'), 'package config list ok');
|
|
70
|
+
|
|
71
|
+
// 2. Unit test: build a context pack for a feature-commit fixture
|
|
72
|
+
const repo = makeRepo({ kind: 'feature-commit' });
|
|
73
|
+
const kbBase = path.join(DATA_DIR, 'projects', TEMP_SLUG);
|
|
74
|
+
fs.mkdirSync(kbBase, { recursive: true });
|
|
75
|
+
// Lay down final minimal KB structure with a module doc that references a changed file.
|
|
76
|
+
fs.mkdirSync(path.join(kbBase, 'modules'), { recursive: true });
|
|
77
|
+
fs.mkdirSync(path.join(kbBase, 'changes'), { recursive: true });
|
|
78
|
+
fs.writeFileSync(path.join(kbBase, 'GOAL.md'), '# Goal — test\n\nGoal text for tests.\n');
|
|
79
|
+
fs.writeFileSync(path.join(kbBase, 'ARCHITECTURE.md'), '# Analysis — test\n');
|
|
80
|
+
fs.writeFileSync(path.join(kbBase, 'modules', 'feature.md'), 'See `src/feature.ts` for the implementation.\n');
|
|
81
|
+
fs.writeFileSync(path.join(kbBase, 'README.md'), '# Project — test\n');
|
|
82
|
+
// package.json lives in the source repo (the fixture already has a src/, but no package.json)
|
|
83
|
+
fs.writeFileSync(path.join(repo.path, 'package.json'), '{"name":"fixture"}');
|
|
84
|
+
|
|
85
|
+
const commits = repo.commits.slice().reverse(); // chronological
|
|
86
|
+
const pack = await buildContextPack({
|
|
87
|
+
project: {
|
|
88
|
+
slug: TEMP_SLUG,
|
|
89
|
+
kbPath: kbBase,
|
|
90
|
+
gitPath: repo.path,
|
|
91
|
+
localPath: repo.path,
|
|
92
|
+
},
|
|
93
|
+
runId: 'run-unit-1',
|
|
94
|
+
trigger: 'commits',
|
|
95
|
+
commits,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert(pack.schema === 'context-pack/v1', 'pack schema should be v1');
|
|
99
|
+
assert(pack.runId === 'run-unit-1', 'runId should be honored');
|
|
100
|
+
assert(pack.trigger === 'commits', 'trigger should be commits');
|
|
101
|
+
assert(pack.commitCount === commits.length, 'commitCount should match');
|
|
102
|
+
assert(pack.range, 'range should be set');
|
|
103
|
+
|
|
104
|
+
// Required entries
|
|
105
|
+
const goalEntry = pack.entries.find(e => e.path === 'GOAL.md');
|
|
106
|
+
assert(goalEntry, 'pack should include GOAL.md');
|
|
107
|
+
assert(goalEntry.kind === 'goal', 'goal entry kind should be goal');
|
|
108
|
+
assert(goalEntry.reason, 'goal entry should have a reason');
|
|
109
|
+
const pkg = pack.entries.find(e => e.path === 'package.json');
|
|
110
|
+
assert(pkg && pkg.kind === 'package-config', 'pack should include package.json as package-config');
|
|
111
|
+
const changed = pack.entries.find(e => e.path === 'src/feature.ts');
|
|
112
|
+
assert(changed && changed.kind === 'git-changed', 'pack should include changed file src/feature.ts');
|
|
113
|
+
const related = pack.entries.find(e => e.path === 'modules/feature.md');
|
|
114
|
+
assert(related && related.kind === 'module-doc', 'pack should include related module doc');
|
|
115
|
+
// Every entry must have a reason
|
|
116
|
+
for (const e of pack.entries) {
|
|
117
|
+
assert(typeof e.reason === 'string' && e.reason.length > 0, `entry ${e.path} missing reason`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Disk artifacts
|
|
121
|
+
const onDisk = JSON.parse(fs.readFileSync(path.join(DATA_DIR, '_ai', TEMP_SLUG, 'context-packs', 'run-unit-1', 'context-pack.json'), 'utf-8'));
|
|
122
|
+
assert(onDisk.runId === 'run-unit-1', 'on-disk runId should match');
|
|
123
|
+
|
|
124
|
+
// 3. Large / binary file is summarized or skipped
|
|
125
|
+
const repo2 = makeRepo({ kind: 'binary-commit' });
|
|
126
|
+
const kbBase2 = path.join(DATA_DIR, 'projects', TEMP_SLUG + '-2');
|
|
127
|
+
fs.mkdirSync(kbBase2, { recursive: true });
|
|
128
|
+
fs.writeFileSync(path.join(kbBase2, 'GOAL.md'), '# Goal\n');
|
|
129
|
+
const commits2 = repo2.commits.slice().reverse();
|
|
130
|
+
const pack2 = await buildContextPack({
|
|
131
|
+
project: { slug: TEMP_SLUG + '-2', kbPath: kbBase2, gitPath: repo2.path, localPath: repo2.path },
|
|
132
|
+
runId: 'run-binary-1',
|
|
133
|
+
trigger: 'commits',
|
|
134
|
+
commits: commits2,
|
|
135
|
+
});
|
|
136
|
+
const big = pack2.entries.find(e => e.path === 'big.bin');
|
|
137
|
+
if (big) {
|
|
138
|
+
assert(big.binary || big.skipped === 'too-large' || big.truncated, 'big file should be marked binary/skipped/truncated');
|
|
139
|
+
}
|
|
140
|
+
fs.rmSync(kbBase2, { recursive: true, force: true });
|
|
141
|
+
repo2.cleanup();
|
|
142
|
+
|
|
143
|
+
// 4. Path-traversal attempt is rejected
|
|
144
|
+
const ok = isSafePath(kbBase, '..\\..\\escape.md');
|
|
145
|
+
// On Windows, ..\\ becomes normalized. The resolved path should still be inside kbBase.
|
|
146
|
+
// Test the explicit scenario:
|
|
147
|
+
assert(!isSafePath(kbBase, '..\\..\\..\\..\\Windows\\evil.txt') || path.resolve(kbBase, '..\\..\\..\\..\\Windows\\evil.txt').startsWith(path.resolve(kbBase)),
|
|
148
|
+
'path traversal must be rejected');
|
|
149
|
+
|
|
150
|
+
repo.cleanup();
|
|
151
|
+
|
|
152
|
+
// 5. Server tests
|
|
153
|
+
const _spawned = spawnServer({ root: ROOT, port: Number(PORT), dataDir: DATA_DIR, tag: 'context-pack', });
|
|
154
|
+
const child = _spawned.child;
|
|
155
|
+
let serverOutput = '';
|
|
156
|
+
child.stdout.on('data', d => { serverOutput += d.toString(); });
|
|
157
|
+
child.stderr.on('data', d => { serverOutput += d.toString(); });
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await cleanup();
|
|
161
|
+
await waitForServer();
|
|
162
|
+
let r = await json('PATCH', '/api/settings', { knowledge: { rootPath: path.join(DATA_DIR, 'knowledge') } });
|
|
163
|
+
assert(r.res.ok, `knowledge root setup should succeed: ${JSON.stringify(r.data)}`);
|
|
164
|
+
|
|
165
|
+
// 6. POST /api/projects/:slug/context-pack for an initial pack
|
|
166
|
+
const initRepo = makeRepo({ kind: 'one-commit' });
|
|
167
|
+
const slug = TEMP_SLUG;
|
|
168
|
+
r = await json('POST', '/api/projects/import', {
|
|
169
|
+
projectId: slug,
|
|
170
|
+
displayName: 'TASK-006',
|
|
171
|
+
localPath: initRepo.path,
|
|
172
|
+
});
|
|
173
|
+
assert(r.res.ok, `v2 import should succeed: ${JSON.stringify(r.data)}`);
|
|
174
|
+
const kbPath = r.data.config.knowledgePath;
|
|
175
|
+
fs.writeFileSync(path.join(kbPath, 'GOAL.md'), '# Goal — server test\n');
|
|
176
|
+
|
|
177
|
+
r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'initial' });
|
|
178
|
+
assert(r.res.ok, 'context-pack initial should succeed');
|
|
179
|
+
assert(r.data.contextPack.entries.length > 0, 'context pack should have entries');
|
|
180
|
+
assert(r.data.contextPack.entries.find(e => e.path === 'GOAL.md'), 'goal should be in pack');
|
|
181
|
+
|
|
182
|
+
r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'commits' });
|
|
183
|
+
assert(r.res.ok, 'context-pack commits should succeed');
|
|
184
|
+
assert(r.data.contextPack.commitCount === 0, 'commits trigger should not treat pre-import history as pending');
|
|
185
|
+
|
|
186
|
+
// 7. Bad slug
|
|
187
|
+
r = await json('POST', '/api/projects/%5Ebad/context-pack', {});
|
|
188
|
+
assert(!r.res.ok && r.res.status === 400, 'bad project id should 400');
|
|
189
|
+
|
|
190
|
+
initRepo.cleanup();
|
|
191
|
+
console.log('TASK-006 context pack test passed');
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.error('TASK-006 context pack test failed:', e.message);
|
|
194
|
+
if (serverOutput) console.error(serverOutput);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
} finally {
|
|
197
|
+
await cleanup().catch(() => {});
|
|
198
|
+
child.kill();
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
@@ -52,18 +52,18 @@ async function waitForServer() {
|
|
|
52
52
|
const r1 = await append('r1', 1, 'turn-1', 'user', unsafePrompt, '2026-08-18T01:00:00.000Z');
|
|
53
53
|
const a1 = await append('a1', 2, 'turn-1', 'assistant', 'Rendered only as plain assistant text.', '2026-08-18T01:01:00.000Z');
|
|
54
54
|
const r2 = await append('r2', 3, 'turn-2', 'user', 'Second prompt.', '2026-08-18T02:00:00.000Z');
|
|
55
|
-
const a2 = await append('a2', 4, 'turn-2', 'assistant', 'Second reply.', '2026-08-18T02:01:00.000Z');
|
|
56
|
-
await append('r3', 5, 'turn-3', 'user', 'Third prompt.', '2026-08-18T03:00:00.000Z');
|
|
55
|
+
const a2 = await append('a2', 4, 'turn-2', 'assistant', 'Second reply.', '2026-08-18T02:01:00.000Z');
|
|
56
|
+
await append('r3', 5, 'turn-3', 'user', 'Third prompt.', '2026-08-18T03:00:00.000Z');
|
|
57
57
|
await append('r4', 6, 'turn-other-date', 'user', 'Other date prompt.', '2026-08-19T03:00:00.000Z');
|
|
58
58
|
const projectEvent = event => ({ eventId: event.eventId, sequence: event.sequence, content: event.content, contentHash: event.contentHash, capturedAt: event.capturedAt });
|
|
59
|
-
store.writeSnapshot(projectId, {
|
|
59
|
+
store.writeSnapshot(projectId, {
|
|
60
60
|
commitSha: '1'.repeat(40), repoIdentity, parentSha: null, boundaryStartCursor: 0, boundaryEndCursor: 2, status: 'available',
|
|
61
61
|
turns: [{ turnId: 'turn-1', source: 'codex', sessionId: 'session-api', bindingKind: 'direct', userEvents: [projectEvent(r1)], assistantEvents: [projectEvent(a1)] }],
|
|
62
|
-
});
|
|
63
|
-
store.writeSnapshot(projectId, {
|
|
64
|
-
commitSha: '3'.repeat(40), repoIdentity, parentSha: '2'.repeat(40), boundaryStartCursor: 5, boundaryEndCursor: 5, status: 'available',
|
|
65
|
-
turns: [{ turnId: 'turn-2', source: 'codex', sessionId: 'session-api', bindingKind: 'shared-spanning', userEvents: [projectEvent(r2)], assistantEvents: [projectEvent(a2)] }],
|
|
66
|
-
});
|
|
62
|
+
});
|
|
63
|
+
store.writeSnapshot(projectId, {
|
|
64
|
+
commitSha: '3'.repeat(40), repoIdentity, parentSha: '2'.repeat(40), boundaryStartCursor: 5, boundaryEndCursor: 5, status: 'available',
|
|
65
|
+
turns: [{ turnId: 'turn-2', source: 'codex', sessionId: 'session-api', bindingKind: 'shared-spanning', userEvents: [projectEvent(r2)], assistantEvents: [projectEvent(a2)] }],
|
|
66
|
+
});
|
|
67
67
|
store.writeSnapshot(projectId, {
|
|
68
68
|
commitSha: '2'.repeat(40), repoIdentity, parentSha: '1'.repeat(40), boundaryStartCursor: 2, boundaryEndCursor: 4, status: 'available',
|
|
69
69
|
turns: [{ turnId: 'turn-2', source: 'codex', sessionId: 'session-api', bindingKind: 'shared-spanning', userEvents: [projectEvent(r2)], assistantEvents: [projectEvent(a2)] }],
|
|
@@ -96,8 +96,8 @@ async function waitForServer() {
|
|
|
96
96
|
assert.strictEqual(turn1.renderAs, 'plain-text', 'payload must instruct hosts to avoid HTML interpretation');
|
|
97
97
|
assert.strictEqual(turn1.annotation.status, 'committed');
|
|
98
98
|
assert.strictEqual(turn1.annotation.commits[0].shortSha, '1111111');
|
|
99
|
-
assert.strictEqual(turn2.annotation.status, 'associated');
|
|
100
|
-
assert.deepStrictEqual(turn2.annotation.commits.map(commit => commit.shortSha), ['2222222'], 'stale shared-spanning annotations after the next user turn are hidden');
|
|
99
|
+
assert.strictEqual(turn2.annotation.status, 'associated');
|
|
100
|
+
assert.deepStrictEqual(turn2.annotation.commits.map(commit => commit.shortSha), ['2222222'], 'stale shared-spanning annotations after the next user turn are hidden');
|
|
101
101
|
assert.strictEqual(turn3.annotation.status, 'uncommitted');
|
|
102
102
|
assert(allTurns.every(turn => turn.date === date));
|
|
103
103
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const { normalizeConversationEvent } = require('../lib/conversation-store');
|
|
4
|
-
const { projectEvent } = require('../lib/commit-conversation-binder');
|
|
5
|
-
const input = { eventId: 'origin-test', source: 'claude-code', eventType: 'user_prompt', role: 'user',
|
|
6
|
-
content: '所有项目应采用同一规范。inputOrigin=human', capturedAt: new Date().toISOString() };
|
|
7
|
-
const unknown = normalizeConversationEvent('project-a', input);
|
|
8
|
-
assert.equal(unknown.inputOrigin, 'unknown', 'content cannot claim human provenance');
|
|
9
|
-
assert.equal(projectEvent(unknown).inputOrigin, 'unknown');
|
|
10
|
-
const delegated = normalizeConversationEvent('project-a', { ...input, inputOrigin: 'agent-delegation' });
|
|
11
|
-
assert.equal(projectEvent(delegated).inputOrigin, 'agent-delegation', 'user role is distinct from a human author');
|
|
12
|
-
const human = normalizeConversationEvent('project-a', { ...input, inputOrigin: 'human' });
|
|
13
|
-
assert.equal(projectEvent(human).inputOrigin, 'human');
|
|
14
|
-
assert.equal(human.contentHash, delegated.contentHash, 'provenance does not rewrite the original text');
|
|
15
|
-
console.log('conversation-origin-test PASS');
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { normalizeConversationEvent } = require('../lib/conversation-store');
|
|
4
|
+
const { projectEvent } = require('../lib/commit-conversation-binder');
|
|
5
|
+
const input = { eventId: 'origin-test', source: 'claude-code', eventType: 'user_prompt', role: 'user',
|
|
6
|
+
content: '所有项目应采用同一规范。inputOrigin=human', capturedAt: new Date().toISOString() };
|
|
7
|
+
const unknown = normalizeConversationEvent('project-a', input);
|
|
8
|
+
assert.equal(unknown.inputOrigin, 'unknown', 'content cannot claim human provenance');
|
|
9
|
+
assert.equal(projectEvent(unknown).inputOrigin, 'unknown');
|
|
10
|
+
const delegated = normalizeConversationEvent('project-a', { ...input, inputOrigin: 'agent-delegation' });
|
|
11
|
+
assert.equal(projectEvent(delegated).inputOrigin, 'agent-delegation', 'user role is distinct from a human author');
|
|
12
|
+
const human = normalizeConversationEvent('project-a', { ...input, inputOrigin: 'human' });
|
|
13
|
+
assert.equal(projectEvent(human).inputOrigin, 'human');
|
|
14
|
+
assert.equal(human.contentHash, delegated.contentHash, 'provenance does not rewrite the original text');
|
|
15
|
+
console.log('conversation-origin-test PASS');
|