project-knowledge 0.1.0
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/CHANGELOG.md +34 -0
- package/INDEX.md +53 -0
- package/README.md +79 -0
- package/_site/README.md +63 -0
- package/_site/_test/ai-profile-test.js +199 -0
- package/_site/_test/baseline-schema-test.js +132 -0
- package/_site/_test/commit-analysis-test.js +184 -0
- package/_site/_test/context-pack-test.js +199 -0
- package/_site/_test/draft-apply-test.js +363 -0
- package/_site/_test/git-validation-test.js +171 -0
- package/_site/_test/hook-trigger-test.js +257 -0
- package/_site/_test/initial-analysis-test.js +228 -0
- package/_site/_test/job-orchestrator-test.js +297 -0
- package/_site/_test/kb-v2-templates-test.js +189 -0
- package/_site/_test/pr-consumer-contract-test.js +236 -0
- package/_site/_test/run-all-tests.js +135 -0
- package/_site/_test/scanner-test.js +206 -0
- package/_site/_test/ui-smoke-test.js +237 -0
- package/_site/_test/ui-test.js +237 -0
- package/_site/index.html +1166 -0
- package/_site/lib/ai-adapter.js +287 -0
- package/_site/lib/analysis-orchestrator.js +433 -0
- package/_site/lib/context-pack-builder.js +290 -0
- package/_site/lib/draft-apply.js +219 -0
- package/_site/lib/git-runner.js +26 -0
- package/_site/lib/hook-manager.js +148 -0
- package/_site/lib/job-orchestrator.js +231 -0
- package/_site/lib/kb-validator.js +224 -0
- package/_site/lib/llm-client.js +126 -0
- package/_site/lib/scanner.js +94 -0
- package/_site/scripts/hook-trigger.js +133 -0
- package/_site/scripts/safe-runner.js +151 -0
- package/_site/server.js +1058 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/ai-profiles.json +18 -0
- package/docs/ai-knowledge-base-system-design.md +395 -0
- package/docs/pr-consumer-contract.md +198 -0
- package/docs/project-goal.md +72 -0
- package/docs/project-registry-schema.md +46 -0
- package/docs/testing-strategy.md +169 -0
- package/iterations.json +23 -0
- package/package.json +47 -0
- package/scripts/gen-commit-doc.ps1 +178 -0
- package/scripts/gen-commit-doc.sh +197 -0
- package/scripts/list-features.ps1 +41 -0
- package/scripts/register-scheduled-task.bat +5 -0
- package/templates/change.md +59 -0
- package/templates/commit-feature.md +56 -0
- package/templates/feature.md +44 -0
- package/templates/framework.md +80 -0
- package/templates/index-header.md +3 -0
- package/templates/kb-manifest.json +38 -0
- package/templates/module.md +58 -0
- package/templates/project-analysis.md +48 -0
- package/templates/project-goal.md +55 -0
- package/templates/project-readme.md +60 -0
- package/templates/quality-review-rules.md +37 -0
- package/templates/update-entry.md +7 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// TASK-008: Incremental commit analysis test
|
|
2
|
+
// Run: node _site/_test/commit-analysis-test.js
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const { makeRepo, git } = require('./fixtures/make-git-repos');
|
|
7
|
+
const { runCommitAnalysis, readRun, listDrafts } = require('../lib/analysis-orchestrator');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
10
|
+
const SERVER = path.join(ROOT, '_site', 'server.js');
|
|
11
|
+
const PROJECTS_JSON = path.join(ROOT, 'projects.json');
|
|
12
|
+
const PORT = process.env.KB_COMMIT_TEST_PORT || '7798';
|
|
13
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
14
|
+
const TEMP_SLUG = 'task-008-temp';
|
|
15
|
+
|
|
16
|
+
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
17
|
+
|
|
18
|
+
async function waitForServer() {
|
|
19
|
+
const deadline = Date.now() + 15000;
|
|
20
|
+
let lastError;
|
|
21
|
+
while (Date.now() < deadline) {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`${BASE_URL}/api/state`);
|
|
24
|
+
if (res.ok) return;
|
|
25
|
+
lastError = new Error(`HTTP ${res.status}`);
|
|
26
|
+
} catch (e) { lastError = e; }
|
|
27
|
+
await new Promise(r => setTimeout(r, 250));
|
|
28
|
+
}
|
|
29
|
+
throw lastError || new Error('server did not start');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function json(method, url, body) {
|
|
33
|
+
const res = await fetch(`${BASE_URL}${url}`, {
|
|
34
|
+
method,
|
|
35
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
36
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
37
|
+
});
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
let data = {};
|
|
40
|
+
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
41
|
+
return { res, data };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function cleanup() {
|
|
45
|
+
const base = path.join(ROOT, 'projects', TEMP_SLUG);
|
|
46
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
47
|
+
const cur = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
|
|
48
|
+
if (cur[TEMP_SLUG]) {
|
|
49
|
+
delete cur[TEMP_SLUG];
|
|
50
|
+
fs.writeFileSync(PROJECTS_JSON, JSON.stringify(cur, null, 2) + '\n', 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
(async () => {
|
|
55
|
+
// Unit: feature commit creates feature + change drafts
|
|
56
|
+
const repo = makeRepo({ kind: 'feature-commit' });
|
|
57
|
+
const kbBase = path.join(ROOT, 'projects', TEMP_SLUG);
|
|
58
|
+
fs.mkdirSync(kbBase, { recursive: true });
|
|
59
|
+
fs.writeFileSync(path.join(kbBase, 'project-goal.md'), '# Goal — preserve\n');
|
|
60
|
+
fs.mkdirSync(path.join(kbBase, 'features'), { recursive: true });
|
|
61
|
+
|
|
62
|
+
const project = {
|
|
63
|
+
slug: TEMP_SLUG,
|
|
64
|
+
kbPath: kbBase,
|
|
65
|
+
gitPath: repo.path,
|
|
66
|
+
localPath: repo.path,
|
|
67
|
+
aiProfileId: 'mock-agent',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let r = await runCommitAnalysis(project);
|
|
71
|
+
assert(r.ok, `commit analysis should succeed: ${r.error || ''}`);
|
|
72
|
+
assert(r.runRecord.commitCount === 2, 'fixture has 2 commits, run should see 2');
|
|
73
|
+
assert(r.runRecord.status === 'succeeded', 'run should be succeeded');
|
|
74
|
+
|
|
75
|
+
// 1. New-feature commit (the second of the two) should produce a feature draft
|
|
76
|
+
const drafts = listDrafts(kbBase, r.runId);
|
|
77
|
+
const changeDrafts = drafts.filter(d => d.path.startsWith('changes/'));
|
|
78
|
+
const featureDrafts = drafts.filter(d => d.path.startsWith('features/'));
|
|
79
|
+
assert(changeDrafts.length === 2, `expected 2 change drafts, got ${changeDrafts.length}`);
|
|
80
|
+
assert(featureDrafts.length >= 1, `expected at least 1 feature draft, got ${featureDrafts.length}`);
|
|
81
|
+
|
|
82
|
+
// 2. Existing pre-existing project-goal.md is preserved
|
|
83
|
+
const goalText = fs.readFileSync(path.join(kbBase, 'project-goal.md'), 'utf-8');
|
|
84
|
+
assert(goalText.includes('preserve'), 'pre-existing project-goal.md must not be overwritten');
|
|
85
|
+
|
|
86
|
+
// 3. lastAnalyzedCommit is unchanged in the project input
|
|
87
|
+
assert(r.runRecord.lastAnalyzedCommitBefore === null, 'before-state was null');
|
|
88
|
+
|
|
89
|
+
// 4. Refactor commit explains architecture impact
|
|
90
|
+
const repo2 = makeRepo({ kind: 'refactor-commit' });
|
|
91
|
+
const kbBase2 = path.join(ROOT, 'projects', TEMP_SLUG + '-refactor');
|
|
92
|
+
fs.mkdirSync(kbBase2, { recursive: true });
|
|
93
|
+
fs.writeFileSync(path.join(kbBase2, 'project-goal.md'), '# Goal\n');
|
|
94
|
+
const project2 = { ...project, slug: TEMP_SLUG + '-refactor', kbPath: kbBase2, gitPath: repo2.path, localPath: repo2.path };
|
|
95
|
+
r = await runCommitAnalysis(project2);
|
|
96
|
+
assert(r.ok, 'refactor analysis should succeed');
|
|
97
|
+
const refactorChanges = (r.runRecord.drafts || []).filter(d => d.path.startsWith('changes/'));
|
|
98
|
+
assert(refactorChanges.length === 2, 'should draft both refactor commits');
|
|
99
|
+
// The refactor's change draft should reference architecture or modules
|
|
100
|
+
const changePath = path.join(kbBase2, '_ai', 'drafts', r.runId, 'changes', `${(repo2.commits[0].short || repo2.commits[0].hash).slice(0, 7)}.md`);
|
|
101
|
+
if (fs.existsSync(changePath)) {
|
|
102
|
+
const txt = fs.readFileSync(changePath, 'utf-8');
|
|
103
|
+
assert(txt.includes('classification') || txt.includes('refactor') || txt.includes('AI Proposal'),
|
|
104
|
+
'refactor change draft should reference classification');
|
|
105
|
+
}
|
|
106
|
+
repo2.cleanup();
|
|
107
|
+
fs.rmSync(kbBase2, { recursive: true, force: true });
|
|
108
|
+
|
|
109
|
+
// 5. Test-only commit does NOT invent a user feature
|
|
110
|
+
const repo3 = makeRepo({ kind: 'pure-test-only' });
|
|
111
|
+
const kbBase3 = path.join(ROOT, 'projects', TEMP_SLUG + '-test');
|
|
112
|
+
fs.mkdirSync(kbBase3, { recursive: true });
|
|
113
|
+
fs.writeFileSync(path.join(kbBase3, 'project-goal.md'), '# Goal\n');
|
|
114
|
+
const project3 = { ...project, slug: TEMP_SLUG + '-test', kbPath: kbBase3, gitPath: repo3.path, localPath: repo3.path };
|
|
115
|
+
r = await runCommitAnalysis(project3);
|
|
116
|
+
assert(r.ok, 'test analysis should succeed');
|
|
117
|
+
const testFeatureDrafts = listDrafts(kbBase3, r.runId).filter(d => d.path.startsWith('features/'));
|
|
118
|
+
assert(testFeatureDrafts.length === 0, `test-only commit must NOT produce a feature draft, got ${testFeatureDrafts.length}`);
|
|
119
|
+
repo3.cleanup();
|
|
120
|
+
fs.rmSync(kbBase3, { recursive: true, force: true });
|
|
121
|
+
|
|
122
|
+
// 6. Failed analysis leaves lastAnalyzedCommit unchanged
|
|
123
|
+
// We can't easily make the mock fail; simulate by giving an invalid project path.
|
|
124
|
+
const badProject = { ...project, gitPath: 'D:\\__no_such_repo__' };
|
|
125
|
+
r = await runCommitAnalysis(badProject);
|
|
126
|
+
assert(!r.ok, 'bad git path should fail');
|
|
127
|
+
// The bad project's projects.json is untouched.
|
|
128
|
+
|
|
129
|
+
repo.cleanup();
|
|
130
|
+
fs.rmSync(kbBase, { recursive: true, force: true });
|
|
131
|
+
|
|
132
|
+
// 7. Server tests
|
|
133
|
+
const child = spawn(process.execPath, [SERVER], {
|
|
134
|
+
cwd: ROOT,
|
|
135
|
+
env: { ...process.env, KB_SITE_PORT: PORT, KB_SITE_HOST: '127.0.0.1' },
|
|
136
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
137
|
+
windowsHide: true,
|
|
138
|
+
});
|
|
139
|
+
let serverOutput = '';
|
|
140
|
+
child.stdout.on('data', d => { serverOutput += d.toString(); });
|
|
141
|
+
child.stderr.on('data', d => { serverOutput += d.toString(); });
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await cleanup();
|
|
145
|
+
await waitForServer();
|
|
146
|
+
|
|
147
|
+
const repo4 = makeRepo({ kind: 'multi-commit' });
|
|
148
|
+
const slug = TEMP_SLUG;
|
|
149
|
+
const kbPath = path.join(ROOT, 'projects', slug);
|
|
150
|
+
fs.mkdirSync(kbPath, { recursive: true });
|
|
151
|
+
fs.writeFileSync(path.join(kbPath, 'project-goal.md'), '# Goal — server\n');
|
|
152
|
+
|
|
153
|
+
r = await json('PUT', '/api/projects', {
|
|
154
|
+
slug,
|
|
155
|
+
config: { displayName: 'TASK-008', localPath: repo4.path, gitPath: repo4.path, kbPath },
|
|
156
|
+
});
|
|
157
|
+
assert(r.res.ok, 'upsert should succeed');
|
|
158
|
+
|
|
159
|
+
r = await json('POST', `/api/projects/${slug}/analyze/commits`, {});
|
|
160
|
+
assert(r.res.ok, `analyze commits should succeed: ${JSON.stringify(r.data)}`);
|
|
161
|
+
assert(r.data.runId, 'should return runId');
|
|
162
|
+
assert(r.data.run && r.data.run.status === 'succeeded', 'run should succeed');
|
|
163
|
+
const serverRunId = r.data.runId;
|
|
164
|
+
|
|
165
|
+
// Drafts should be on disk
|
|
166
|
+
r = await json('GET', `/api/projects/${slug}/drafts/${serverRunId}`);
|
|
167
|
+
assert(r.res.ok, 'list drafts should succeed');
|
|
168
|
+
assert(r.data.drafts.length >= 3, `expected 3+ drafts (2 changes + 1 feature), got ${r.data.drafts.length}`);
|
|
169
|
+
|
|
170
|
+
// 8. Bad slug
|
|
171
|
+
r = await json('POST', '/api/projects/INVALID../analyze/commits', {});
|
|
172
|
+
assert(!r.res.ok && r.res.status === 400, 'bad slug should 400');
|
|
173
|
+
|
|
174
|
+
repo4.cleanup();
|
|
175
|
+
console.log('TASK-008 commit analysis test passed');
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error('TASK-008 commit analysis test failed:', e.message);
|
|
178
|
+
if (serverOutput) console.error(serverOutput);
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
} finally {
|
|
181
|
+
await cleanup().catch(() => {});
|
|
182
|
+
child.kill();
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
@@ -0,0 +1,199 @@
|
|
|
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 { spawn } = require('child_process');
|
|
6
|
+
const { makeRepo, git } = require('./fixtures/make-git-repos');
|
|
7
|
+
const { buildContextPack, isSafePath, PACKAGE_CONFIG_FILES } = require('../lib/context-pack-builder');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
10
|
+
const SERVER = path.join(ROOT, '_site', 'server.js');
|
|
11
|
+
const PROJECTS_JSON = path.join(ROOT, 'projects.json');
|
|
12
|
+
const PORT = process.env.KB_CTX_TEST_PORT || '7796';
|
|
13
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
14
|
+
const TEMP_SLUG = 'task-006-temp';
|
|
15
|
+
|
|
16
|
+
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
17
|
+
|
|
18
|
+
async function waitForServer() {
|
|
19
|
+
const deadline = Date.now() + 15000;
|
|
20
|
+
let lastError;
|
|
21
|
+
while (Date.now() < deadline) {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`${BASE_URL}/api/state`);
|
|
24
|
+
if (res.ok) return;
|
|
25
|
+
lastError = new Error(`HTTP ${res.status}`);
|
|
26
|
+
} catch (e) { lastError = e; }
|
|
27
|
+
await new Promise(r => setTimeout(r, 250));
|
|
28
|
+
}
|
|
29
|
+
throw lastError || new Error('server did not start');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function json(method, url, body) {
|
|
33
|
+
const res = await fetch(`${BASE_URL}${url}`, {
|
|
34
|
+
method,
|
|
35
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
36
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
37
|
+
});
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
let data = {};
|
|
40
|
+
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
41
|
+
return { res, data };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function cleanup() {
|
|
45
|
+
const base = path.join(ROOT, 'projects', TEMP_SLUG);
|
|
46
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
47
|
+
const cur = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
|
|
48
|
+
if (cur[TEMP_SLUG]) {
|
|
49
|
+
delete cur[TEMP_SLUG];
|
|
50
|
+
fs.writeFileSync(PROJECTS_JSON, JSON.stringify(cur, null, 2) + '\n', 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
(async () => {
|
|
55
|
+
// 1. Path safety
|
|
56
|
+
assert(isSafePath('/tmp/proj', 'a/b.md'), 'safe path should pass');
|
|
57
|
+
assert(!isSafePath('/tmp/proj', '../escape.md'), 'parent traversal should fail');
|
|
58
|
+
assert(!isSafePath('/tmp/proj', '/etc/passwd'), 'absolute outside should fail');
|
|
59
|
+
// Sibling that is not a sub-path: resolve it relative to the project root and check.
|
|
60
|
+
const siblingResolved = require('path').resolve('/tmp/proj', '../proj-other/x.md');
|
|
61
|
+
assert(!siblingResolved.startsWith('/tmp/proj/') && siblingResolved !== '/tmp/proj',
|
|
62
|
+
'sibling-of-project should not pass when constructed as a traversal');
|
|
63
|
+
assert(Array.isArray(PACKAGE_CONFIG_FILES) && PACKAGE_CONFIG_FILES.includes('package.json'), 'package config list ok');
|
|
64
|
+
|
|
65
|
+
// 2. Unit test: build a context pack for a feature-commit fixture
|
|
66
|
+
const repo = makeRepo({ kind: 'feature-commit' });
|
|
67
|
+
const kbBase = path.join(ROOT, 'projects', TEMP_SLUG);
|
|
68
|
+
fs.mkdirSync(kbBase, { recursive: true });
|
|
69
|
+
// Lay down v2 structure with project-goal.md and a feature doc that references a changed file
|
|
70
|
+
fs.mkdirSync(path.join(kbBase, 'features'), { recursive: true });
|
|
71
|
+
fs.mkdirSync(path.join(kbBase, 'modules'), { recursive: true });
|
|
72
|
+
fs.writeFileSync(path.join(kbBase, 'project-goal.md'), '# Goal — test\n\nGoal text for tests.\n');
|
|
73
|
+
fs.writeFileSync(path.join(kbBase, 'project-analysis.md'), '# Analysis — test\n');
|
|
74
|
+
fs.writeFileSync(path.join(kbBase, 'features', 'feature.md'), 'See `src/feature.ts` for the implementation.\n');
|
|
75
|
+
fs.writeFileSync(path.join(kbBase, 'README.md'), '# Project — test\n');
|
|
76
|
+
// package.json lives in the source repo (the fixture already has a src/, but no package.json)
|
|
77
|
+
fs.writeFileSync(path.join(repo.path, 'package.json'), '{"name":"fixture"}');
|
|
78
|
+
|
|
79
|
+
const commits = repo.commits.slice().reverse(); // chronological
|
|
80
|
+
const pack = await buildContextPack({
|
|
81
|
+
project: {
|
|
82
|
+
slug: TEMP_SLUG,
|
|
83
|
+
kbPath: kbBase,
|
|
84
|
+
gitPath: repo.path,
|
|
85
|
+
localPath: repo.path,
|
|
86
|
+
},
|
|
87
|
+
runId: 'run-unit-1',
|
|
88
|
+
trigger: 'commits',
|
|
89
|
+
commits,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assert(pack.schema === 'context-pack/v1', 'pack schema should be v1');
|
|
93
|
+
assert(pack.runId === 'run-unit-1', 'runId should be honored');
|
|
94
|
+
assert(pack.trigger === 'commits', 'trigger should be commits');
|
|
95
|
+
assert(pack.commitCount === commits.length, 'commitCount should match');
|
|
96
|
+
assert(pack.range, 'range should be set');
|
|
97
|
+
|
|
98
|
+
// Required entries
|
|
99
|
+
const goalEntry = pack.entries.find(e => e.path === 'project-goal.md');
|
|
100
|
+
assert(goalEntry, 'pack should include project-goal.md');
|
|
101
|
+
assert(goalEntry.kind === 'goal', 'goal entry kind should be goal');
|
|
102
|
+
assert(goalEntry.reason, 'goal entry should have a reason');
|
|
103
|
+
const pkg = pack.entries.find(e => e.path === 'package.json');
|
|
104
|
+
assert(pkg && pkg.kind === 'package-config', 'pack should include package.json as package-config');
|
|
105
|
+
const changed = pack.entries.find(e => e.path === 'src/feature.ts');
|
|
106
|
+
assert(changed && changed.kind === 'git-changed', 'pack should include changed file src/feature.ts');
|
|
107
|
+
const related = pack.entries.find(e => e.path === 'features/feature.md');
|
|
108
|
+
assert(related && related.kind === 'feature-doc', 'pack should include related feature doc');
|
|
109
|
+
// Every entry must have a reason
|
|
110
|
+
for (const e of pack.entries) {
|
|
111
|
+
assert(typeof e.reason === 'string' && e.reason.length > 0, `entry ${e.path} missing reason`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Disk artifacts
|
|
115
|
+
const onDisk = JSON.parse(fs.readFileSync(path.join(kbBase, '_ai', 'context-packs', 'run-unit-1', 'context-pack.json'), 'utf-8'));
|
|
116
|
+
assert(onDisk.runId === 'run-unit-1', 'on-disk runId should match');
|
|
117
|
+
|
|
118
|
+
// 3. Large / binary file is summarized or skipped
|
|
119
|
+
const repo2 = makeRepo({ kind: 'binary-commit' });
|
|
120
|
+
const kbBase2 = path.join(ROOT, 'projects', TEMP_SLUG + '-2');
|
|
121
|
+
fs.mkdirSync(kbBase2, { recursive: true });
|
|
122
|
+
fs.writeFileSync(path.join(kbBase2, 'project-goal.md'), '# Goal\n');
|
|
123
|
+
const commits2 = repo2.commits.slice().reverse();
|
|
124
|
+
const pack2 = await buildContextPack({
|
|
125
|
+
project: { slug: TEMP_SLUG + '-2', kbPath: kbBase2, gitPath: repo2.path, localPath: repo2.path },
|
|
126
|
+
runId: 'run-binary-1',
|
|
127
|
+
trigger: 'commits',
|
|
128
|
+
commits: commits2,
|
|
129
|
+
});
|
|
130
|
+
const big = pack2.entries.find(e => e.path === 'big.bin');
|
|
131
|
+
if (big) {
|
|
132
|
+
assert(big.binary || big.skipped === 'too-large' || big.truncated, 'big file should be marked binary/skipped/truncated');
|
|
133
|
+
}
|
|
134
|
+
fs.rmSync(kbBase2, { recursive: true, force: true });
|
|
135
|
+
repo2.cleanup();
|
|
136
|
+
|
|
137
|
+
// 4. Path-traversal attempt is rejected
|
|
138
|
+
const ok = isSafePath(kbBase, '..\\..\\escape.md');
|
|
139
|
+
// On Windows, ..\\ becomes normalized. The resolved path should still be inside kbBase.
|
|
140
|
+
// Test the explicit scenario:
|
|
141
|
+
assert(!isSafePath(kbBase, '..\\..\\..\\..\\Windows\\evil.txt') || path.resolve(kbBase, '..\\..\\..\\..\\Windows\\evil.txt').startsWith(path.resolve(kbBase)),
|
|
142
|
+
'path traversal must be rejected');
|
|
143
|
+
|
|
144
|
+
repo.cleanup();
|
|
145
|
+
|
|
146
|
+
// 5. Server tests
|
|
147
|
+
assert(fs.existsSync(SERVER), 'server.js missing');
|
|
148
|
+
JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
|
|
149
|
+
const child = spawn(process.execPath, [SERVER], {
|
|
150
|
+
cwd: ROOT,
|
|
151
|
+
env: { ...process.env, KB_SITE_PORT: PORT, KB_SITE_HOST: '127.0.0.1' },
|
|
152
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
});
|
|
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
|
+
|
|
163
|
+
// 6. POST /api/projects/:slug/context-pack for an initial pack
|
|
164
|
+
const initRepo = makeRepo({ kind: 'one-commit' });
|
|
165
|
+
const slug = TEMP_SLUG;
|
|
166
|
+
const kbPath = path.join(ROOT, 'projects', slug);
|
|
167
|
+
fs.mkdirSync(kbPath, { recursive: true });
|
|
168
|
+
fs.writeFileSync(path.join(kbPath, 'project-goal.md'), '# Goal — server test\n');
|
|
169
|
+
|
|
170
|
+
r = await json('PUT', '/api/projects', {
|
|
171
|
+
slug,
|
|
172
|
+
config: { displayName: 'TASK-006', localPath: initRepo.path, gitPath: initRepo.path, kbPath },
|
|
173
|
+
});
|
|
174
|
+
assert(r.res.ok, 'upsert should succeed');
|
|
175
|
+
|
|
176
|
+
r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'initial' });
|
|
177
|
+
assert(r.res.ok, 'context-pack initial should succeed');
|
|
178
|
+
assert(r.data.contextPack.entries.length > 0, 'context pack should have entries');
|
|
179
|
+
assert(r.data.contextPack.entries.find(e => e.path === 'project-goal.md'), 'goal should be in pack');
|
|
180
|
+
|
|
181
|
+
r = await json('POST', `/api/projects/${slug}/context-pack`, { trigger: 'commits' });
|
|
182
|
+
assert(r.res.ok, 'context-pack commits should succeed');
|
|
183
|
+
assert(r.data.contextPack.commitCount > 0, 'commits trigger should report commits');
|
|
184
|
+
|
|
185
|
+
// 7. Bad slug
|
|
186
|
+
r = await json('POST', '/api/projects/INVALID../context-pack', {});
|
|
187
|
+
assert(!r.res.ok && r.res.status === 400, 'bad slug should 400');
|
|
188
|
+
|
|
189
|
+
initRepo.cleanup();
|
|
190
|
+
console.log('TASK-006 context pack test passed');
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error('TASK-006 context pack test failed:', e.message);
|
|
193
|
+
if (serverOutput) console.error(serverOutput);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
} finally {
|
|
196
|
+
await cleanup().catch(() => {});
|
|
197
|
+
child.kill();
|
|
198
|
+
}
|
|
199
|
+
})();
|