project-knowledge 0.1.0 → 1.0.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/CHANGELOG.md +41 -0
- package/README.md +201 -58
- package/_site/_test/ai-profile-test.js +59 -1
- package/_site/_test/baseline-schema-test.js +4 -3
- package/_site/_test/claude-workbench-test.js +72 -0
- package/_site/_test/draft-apply-test.js +12 -6
- package/_site/_test/kb-v2-templates-test.js +31 -43
- package/_site/_test/knowledge-store-logs-supervision-test.js +143 -0
- package/_site/_test/package-startup-test.js +108 -0
- package/_site/_test/project-control-panel-task14-test.js +151 -0
- package/_site/_test/task15-20-integration-test.js +194 -0
- package/_site/_test/task15-20-ui-flow-test.js +144 -0
- package/_site/_test/ui-smoke-test.js +2 -2
- package/_site/index.html +1640 -90
- package/_site/lib/ai-adapter.js +3 -3
- package/_site/lib/ai-workspace.js +120 -0
- package/_site/lib/analysis-orchestrator.js +117 -32
- package/_site/lib/claude-cli-runner.js +862 -0
- package/_site/lib/context-pack-builder.js +19 -11
- package/_site/lib/draft-apply.js +80 -31
- package/_site/lib/index-builder.js +100 -0
- package/_site/lib/job-orchestrator.js +15 -11
- package/_site/lib/kb-v3.js +188 -0
- package/_site/lib/kb-validator.js +84 -0
- package/_site/lib/knowledge-store.js +141 -0
- package/_site/lib/llm-client.js +103 -56
- package/_site/lib/prompt-registry.js +102 -0
- package/_site/lib/structured-logger.js +120 -0
- package/_site/lib/supervision.js +103 -0
- package/_site/server.js +887 -30
- package/_site/vendor/tailwind-browser.js +947 -0
- package/_site/vendor/vue.global.prod.js +9 -0
- package/ai-profiles.json +13 -3
- package/bin/project-knowledge.js +51 -0
- package/docs/development-progress.md +141 -0
- package/package.json +11 -2
- package/scripts/gen-commit-doc.ps1 +1 -1
- package/scripts/list-features.ps1 +1 -1
- package/scripts/register-scheduled-task.bat +3 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const { makeRepo } = require('./fixtures/make-git-repos');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
7
|
+
const SERVER = path.join(ROOT, '_site', 'server.js');
|
|
8
|
+
const PROJECTS_JSON = path.join(ROOT, 'projects.json');
|
|
9
|
+
const KNOWLEDGE_STORE_JSON = path.join(ROOT, 'knowledge-store.json');
|
|
10
|
+
const LOGGING_JSON = path.join(ROOT, 'logging.json');
|
|
11
|
+
const PORT = process.env.KB_TASK_15_20_PORT || '7820';
|
|
12
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
13
|
+
const TEMP_ROOT = path.join(ROOT, '.tmp-task-15-20');
|
|
14
|
+
const SLUG = 'task-015-020-temp';
|
|
15
|
+
const LEGACY_SLUG = 'task-020-legacy-temp';
|
|
16
|
+
|
|
17
|
+
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
18
|
+
function backup(file) { return fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : null; }
|
|
19
|
+
function restore(file, content) {
|
|
20
|
+
if (content == null) fs.rmSync(file, { force: true });
|
|
21
|
+
else fs.writeFileSync(file, content, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
24
|
+
async function rmWithRetry(target) {
|
|
25
|
+
for (let i = 0; i < 8; i++) {
|
|
26
|
+
try { fs.rmSync(target, { recursive: true, force: true }); return; }
|
|
27
|
+
catch (e) { if (i === 7) throw e; await sleep(250); }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function waitForServer() {
|
|
31
|
+
const deadline = Date.now() + 15000;
|
|
32
|
+
let lastError;
|
|
33
|
+
while (Date.now() < deadline) {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${BASE_URL}/api/state`);
|
|
36
|
+
if (res.ok) return;
|
|
37
|
+
lastError = new Error(`HTTP ${res.status}`);
|
|
38
|
+
} catch (e) { lastError = e; }
|
|
39
|
+
await sleep(250);
|
|
40
|
+
}
|
|
41
|
+
throw lastError || new Error('server did not start');
|
|
42
|
+
}
|
|
43
|
+
async function json(method, url, body) {
|
|
44
|
+
const res = await fetch(`${BASE_URL}${url}`, {
|
|
45
|
+
method,
|
|
46
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
47
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
48
|
+
});
|
|
49
|
+
const text = await res.text();
|
|
50
|
+
let data = {};
|
|
51
|
+
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
52
|
+
return { res, data };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
(async () => {
|
|
56
|
+
const projectsBackup = backup(PROJECTS_JSON);
|
|
57
|
+
const storeBackup = backup(KNOWLEDGE_STORE_JSON);
|
|
58
|
+
const loggingBackup = backup(LOGGING_JSON);
|
|
59
|
+
fs.rmSync(TEMP_ROOT, { recursive: true, force: true });
|
|
60
|
+
fs.mkdirSync(TEMP_ROOT, { recursive: true });
|
|
61
|
+
const kbRoot = path.join(TEMP_ROOT, 'knowledge');
|
|
62
|
+
const logsRoot = path.join(TEMP_ROOT, 'logs');
|
|
63
|
+
const aiRoot = path.join(ROOT, '_site', '_ai');
|
|
64
|
+
fs.rmSync(path.join(aiRoot, SLUG), { recursive: true, force: true });
|
|
65
|
+
fs.rmSync(path.join(aiRoot, LEGACY_SLUG), { recursive: true, force: true });
|
|
66
|
+
|
|
67
|
+
const child = spawn(process.execPath, [SERVER], {
|
|
68
|
+
cwd: ROOT,
|
|
69
|
+
env: { ...process.env, KB_SITE_PORT: PORT, KB_SITE_HOST: '127.0.0.1' },
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
let serverOutput = '';
|
|
74
|
+
child.stdout.on('data', d => { serverOutput += d.toString(); });
|
|
75
|
+
child.stderr.on('data', d => { serverOutput += d.toString(); });
|
|
76
|
+
|
|
77
|
+
let repo = null;
|
|
78
|
+
try {
|
|
79
|
+
await waitForServer();
|
|
80
|
+
let r = await json('PUT', '/api/knowledge-store/config', { rootPath: kbRoot, git: { enabled: false } });
|
|
81
|
+
assert(r.res.ok, 'knowledge-store config should save');
|
|
82
|
+
r = await json('PUT', '/api/logging/config', { rootPath: logsRoot, retentionDays: 14 });
|
|
83
|
+
assert(r.res.ok, 'logging config should save');
|
|
84
|
+
|
|
85
|
+
repo = makeRepo({ kind: 'feature-commit' });
|
|
86
|
+
r = await json('PUT', '/api/projects', {
|
|
87
|
+
slug: SLUG,
|
|
88
|
+
config: {
|
|
89
|
+
displayName: 'TASK 015-020 Temp',
|
|
90
|
+
localPath: repo.path,
|
|
91
|
+
gitPath: repo.path,
|
|
92
|
+
aiProfileId: 'mock-agent',
|
|
93
|
+
goalStatus: 'accepted',
|
|
94
|
+
kbSchemaVersion: 'v3',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
assert(r.res.ok, 'project import should succeed');
|
|
98
|
+
|
|
99
|
+
r = await json('POST', `/api/projects/${SLUG}/init`);
|
|
100
|
+
assert(r.res.ok && r.data.kbSchemaVersion === 'v3', 'init should create v3 KB');
|
|
101
|
+
const projects = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
|
|
102
|
+
const kbPath = projects[SLUG].kbPath;
|
|
103
|
+
const top = fs.readdirSync(kbPath).filter(name => !name.startsWith('.')).sort();
|
|
104
|
+
assert(JSON.stringify(top) === JSON.stringify(['ARCHITECTURE.md', 'GOAL.md', 'README.md', 'changes', 'modules']), `unexpected v3 top-level: ${top.join(', ')}`);
|
|
105
|
+
assert(!fs.existsSync(path.join(kbPath, '_ai')), 'v3 KB must not contain _ai');
|
|
106
|
+
assert(!fs.existsSync(path.join(kbPath, 'kb-manifest.json')), 'v3 KB must not contain manifest');
|
|
107
|
+
|
|
108
|
+
r = await json('POST', `/api/projects/${SLUG}/validate-kb`);
|
|
109
|
+
assert(r.res.ok && r.data.ok, `v3 validate should pass: ${JSON.stringify(r.data)}`);
|
|
110
|
+
r = await json('GET', `/api/projects/${SLUG}/pr-context`);
|
|
111
|
+
assert(r.res.ok && r.data.pack && r.data.pack.goal.path === 'GOAL.md', 'PR context should read V3 GOAL');
|
|
112
|
+
|
|
113
|
+
r = await json('POST', `/api/projects/${SLUG}/analyze/commits`);
|
|
114
|
+
assert(r.res.ok, `commit analysis should pass: ${JSON.stringify(r.data)}`);
|
|
115
|
+
const runId = r.data.runId;
|
|
116
|
+
assert(r.data.run.sourceBranch !== undefined, 'run should include sourceBranch key');
|
|
117
|
+
assert(r.data.run.sourceDefaultBranch !== undefined, 'run should include sourceDefaultBranch key');
|
|
118
|
+
assert(r.data.run.sourceRemote !== undefined, 'run should include sourceRemote key');
|
|
119
|
+
assert(fs.existsSync(path.join(aiRoot, SLUG, 'runs', `${runId}.json`)), 'run should live in _site/_ai/<slug>');
|
|
120
|
+
assert(fs.existsSync(path.join(aiRoot, SLUG, 'context-packs', runId, 'context-pack.json')), 'context pack should live in _site/_ai/<slug>');
|
|
121
|
+
|
|
122
|
+
const branch = r.data.run.sourceBranch || 'unknown (pre-TASK-016)';
|
|
123
|
+
r = await json('GET', `/api/projects/${SLUG}/drafts-by-branch?branch=${encodeURIComponent(branch)}`);
|
|
124
|
+
assert(r.res.ok && r.data.drafts.length > 0, 'drafts-by-branch should return branch drafts');
|
|
125
|
+
assert(r.data.drafts.every(d => d.sourceBranch === branch), 'drafts-by-branch should filter by branch');
|
|
126
|
+
|
|
127
|
+
const payloads = [];
|
|
128
|
+
for (const d of r.data.drafts.filter(d => d.path.startsWith('changes/'))) {
|
|
129
|
+
const raw = await json('GET', `/api/projects/${SLUG}/drafts/${d.runId}/raw?path=${encodeURIComponent(d.path)}`);
|
|
130
|
+
assert(raw.res.ok, `raw draft should load: ${d.path}`);
|
|
131
|
+
payloads.push({ path: d.path, content: raw.data.content, sourceBranch: d.sourceBranch, sourceHeadCommit: d.sourceHeadCommit });
|
|
132
|
+
}
|
|
133
|
+
assert(payloads.length > 0, 'should have changes draft payloads');
|
|
134
|
+
r = await json('POST', `/api/projects/${SLUG}/drafts/${runId}/apply`, { drafts: payloads, allowGoalEdit: false });
|
|
135
|
+
assert(r.res.ok && r.data.applied.length === payloads.length, 'V3 changes drafts should auto-apply');
|
|
136
|
+
const applied = fs.readFileSync(path.join(kbPath, payloads[0].path), 'utf-8');
|
|
137
|
+
assert(applied.includes('sourceBranch:'), 'applied V3 doc should include sourceBranch');
|
|
138
|
+
assert(fs.existsSync(path.join(kbPath, 'changes', '00-index.md')), 'changes index should regenerate');
|
|
139
|
+
|
|
140
|
+
r = await json('POST', `/api/projects/${SLUG}/drafts/${runId}/apply`, {
|
|
141
|
+
drafts: [{ path: 'GOAL.md', content: '# bad silent goal edit' }],
|
|
142
|
+
allowGoalEdit: false,
|
|
143
|
+
});
|
|
144
|
+
assert(!r.res.ok && r.res.status === 409, 'GOAL.md apply should require review');
|
|
145
|
+
|
|
146
|
+
const legacyKb = path.join(kbRoot, LEGACY_SLUG);
|
|
147
|
+
fs.mkdirSync(path.join(legacyKb, '_ai', 'runs'), { recursive: true });
|
|
148
|
+
fs.mkdirSync(path.join(legacyKb, 'commits'), { recursive: true });
|
|
149
|
+
fs.writeFileSync(path.join(legacyKb, 'README.md'), '# legacy\n');
|
|
150
|
+
fs.writeFileSync(path.join(legacyKb, 'project-goal.md'), '# legacy goal\n');
|
|
151
|
+
fs.writeFileSync(path.join(legacyKb, '_ai', 'runs', 'old.json'), '{"schema":"ai-run/v1"}\n');
|
|
152
|
+
for (let i = 0; i < 6; i++) fs.writeFileSync(path.join(legacyKb, 'commits', `2026-01-0${i + 1}_aaaaaa${i}_test.md`), `# commit ${i}\n`);
|
|
153
|
+
r = await json('PUT', '/api/projects', { slug: LEGACY_SLUG, config: { displayName: 'legacy', localPath: repo.path, gitPath: repo.path, kbPath: legacyKb, kbSchemaVersion: 'v2' } });
|
|
154
|
+
assert(r.res.ok, 'legacy project import should succeed');
|
|
155
|
+
r = await json('POST', `/api/projects/${LEGACY_SLUG}/migrate-v3`);
|
|
156
|
+
assert(r.res.ok, `migrate-v3 should pass: ${JSON.stringify(r.data)}`);
|
|
157
|
+
assert(!fs.existsSync(path.join(legacyKb, '_ai')), 'migrate-v3 should remove KB-local _ai');
|
|
158
|
+
assert(!fs.existsSync(path.join(legacyKb, 'commits')), 'migrate-v3 should remove legacy commits dir');
|
|
159
|
+
assert(fs.existsSync(path.join(aiRoot, LEGACY_SLUG, 'legacy-commits')), 'legacy commits should be backed up to app AI workspace');
|
|
160
|
+
assert(fs.readdirSync(path.join(legacyKb, 'changes')).some(name => /^legacy-change-/.test(name)), 'legacy commits should consolidate into changes');
|
|
161
|
+
|
|
162
|
+
r = await json('GET', `/api/projects/${SLUG}/remove-preview`);
|
|
163
|
+
assert(r.res.ok && r.data.preview.fileCount > 0, 'remove preview should include file count');
|
|
164
|
+
r = await json('POST', `/api/projects/${SLUG}/remove`, { deleteKb: false, reason: 'integration soft remove' });
|
|
165
|
+
assert(r.res.ok && r.data.removedKb === false, 'soft remove should not delete KB');
|
|
166
|
+
assert(fs.existsSync(kbPath), 'soft remove should keep KB on disk');
|
|
167
|
+
r = await json('PUT', '/api/projects', { slug: SLUG, config: { displayName: 'reimport', localPath: repo.path, gitPath: repo.path, kbPath, kbSchemaVersion: 'v3' } });
|
|
168
|
+
assert(r.res.ok, 'reimport after soft remove should work');
|
|
169
|
+
r = await json('POST', `/api/projects/${SLUG}/remove`, { deleteKb: true, reason: 'integration hard remove' });
|
|
170
|
+
assert(r.res.ok && r.data.removedKb === true, 'hard remove should delete KB');
|
|
171
|
+
assert(!fs.existsSync(kbPath), 'hard remove should remove KB path');
|
|
172
|
+
|
|
173
|
+
const logFiles = fs.readdirSync(logsRoot).filter(name => name.endsWith('.log'));
|
|
174
|
+
assert(logFiles.length > 0, 'structured log should be written');
|
|
175
|
+
const logText = logFiles.map(file => fs.readFileSync(path.join(logsRoot, file), 'utf-8')).join('\n');
|
|
176
|
+
assert(logText.includes('project_removed'), 'project removal should be logged');
|
|
177
|
+
|
|
178
|
+
console.log('TASK-015..020 integration test passed');
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error('TASK-015..020 integration test failed:', e.message);
|
|
181
|
+
if (serverOutput) console.error(serverOutput);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
} finally {
|
|
184
|
+
child.kill();
|
|
185
|
+
await sleep(500);
|
|
186
|
+
try { if (repo && repo.cleanup) repo.cleanup(); } catch {}
|
|
187
|
+
restore(PROJECTS_JSON, projectsBackup);
|
|
188
|
+
restore(KNOWLEDGE_STORE_JSON, storeBackup);
|
|
189
|
+
restore(LOGGING_JSON, loggingBackup);
|
|
190
|
+
fs.rmSync(path.join(aiRoot, SLUG), { recursive: true, force: true });
|
|
191
|
+
fs.rmSync(path.join(aiRoot, LEGACY_SLUG), { recursive: true, force: true });
|
|
192
|
+
await rmWithRetry(TEMP_ROOT).catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// UI flow coverage for TASK-015..020.
|
|
2
|
+
// Uses a real Chromium via CDP and writes screenshots to _site/_test/ui-screenshots.
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const WebSocket = require('ws');
|
|
9
|
+
|
|
10
|
+
const CHROME = 'C:\\Users\\SanQian\\AppData\\Local\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe';
|
|
11
|
+
const TARGET_URL = process.argv[2] || 'http://127.0.0.1:7777/';
|
|
12
|
+
const OUT_DIR = path.join(__dirname, 'ui-screenshots');
|
|
13
|
+
const PROFILE = path.join(OUT_DIR, 'task15-20-profile');
|
|
14
|
+
const PORT = 9341;
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
17
|
+
fs.rmSync(PROFILE, { recursive: true, force: true });
|
|
18
|
+
fs.mkdirSync(PROFILE, { recursive: true });
|
|
19
|
+
|
|
20
|
+
function assert(cond, msg) { if (!cond) throw new Error('ASSERT: ' + msg); }
|
|
21
|
+
function fetchJson(url) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
http.get(url, res => {
|
|
24
|
+
let body = '';
|
|
25
|
+
res.on('data', chunk => body += chunk);
|
|
26
|
+
res.on('end', () => {
|
|
27
|
+
try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
|
|
28
|
+
});
|
|
29
|
+
}).on('error', reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function waitFor(fn, label, ms = 15000) {
|
|
33
|
+
const deadline = Date.now() + ms;
|
|
34
|
+
let last;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
try {
|
|
37
|
+
const got = await fn();
|
|
38
|
+
if (got) return got;
|
|
39
|
+
} catch (e) { last = e; }
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`timeout: ${label}${last ? ` :: ${last.message}` : ''}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
(async () => {
|
|
46
|
+
const child = spawn(CHROME, [
|
|
47
|
+
'--headless=new',
|
|
48
|
+
'--disable-gpu',
|
|
49
|
+
'--no-sandbox',
|
|
50
|
+
'--disable-dev-shm-usage',
|
|
51
|
+
`--remote-debugging-port=${PORT}`,
|
|
52
|
+
`--user-data-dir=${PROFILE}`,
|
|
53
|
+
'--window-size=1440,980',
|
|
54
|
+
'about:blank',
|
|
55
|
+
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
56
|
+
|
|
57
|
+
const errors = [];
|
|
58
|
+
try {
|
|
59
|
+
const pages = await waitFor(() => fetchJson(`http://127.0.0.1:${PORT}/json/list`), 'chrome page list');
|
|
60
|
+
const page = pages.find(p => p.type === 'page') || pages[0];
|
|
61
|
+
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
62
|
+
await new Promise((resolve, reject) => { ws.on('open', resolve); ws.on('error', reject); });
|
|
63
|
+
|
|
64
|
+
let nextId = 1;
|
|
65
|
+
const pending = new Map();
|
|
66
|
+
ws.on('message', m => {
|
|
67
|
+
const msg = JSON.parse(m.toString());
|
|
68
|
+
if (msg.id && pending.has(msg.id)) {
|
|
69
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
70
|
+
pending.delete(msg.id);
|
|
71
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
72
|
+
else resolve(msg.result);
|
|
73
|
+
} else if (msg.method === 'Runtime.consoleAPICalled' && msg.params.type === 'error') {
|
|
74
|
+
errors.push((msg.params.args || []).map(arg => arg.value || arg.description || '').join(' '));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
function send(method, params = {}) {
|
|
78
|
+
const id = nextId++;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
pending.set(id, { resolve, reject });
|
|
81
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function evalJs(expression) {
|
|
85
|
+
const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
|
86
|
+
return r.result.value;
|
|
87
|
+
}
|
|
88
|
+
async function screenshot(name) {
|
|
89
|
+
const r = await send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true });
|
|
90
|
+
const file = path.join(OUT_DIR, name);
|
|
91
|
+
fs.writeFileSync(file, Buffer.from(r.data, 'base64'));
|
|
92
|
+
return file;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await send('Runtime.enable');
|
|
96
|
+
await send('Page.enable');
|
|
97
|
+
await send('Network.enable');
|
|
98
|
+
await send('Page.navigate', { url: TARGET_URL });
|
|
99
|
+
await waitFor(() => evalJs('document.readyState === "complete"'), 'document ready');
|
|
100
|
+
await evalJs('localStorage.setItem("kb-ui-language", "en"); location.reload()');
|
|
101
|
+
await waitFor(() => evalJs('document.querySelector("#app") && document.querySelector("#app").innerText.includes("Project Supervision")'), 'app english dashboard');
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
103
|
+
const dashboardShot = await screenshot('task15-20-dashboard.png');
|
|
104
|
+
|
|
105
|
+
await evalJs(`[...document.querySelectorAll('button')].find(b => b.innerText.includes('Pending'))?.click()`);
|
|
106
|
+
await waitFor(() => evalJs('document.body.innerText.includes("Pending commit details")'), 'pending panel open');
|
|
107
|
+
const pendingItems = await evalJs('document.querySelectorAll("section details").length');
|
|
108
|
+
if (pendingItems > 0) assert(pendingItems > 0, 'pending panel should render collapsible details when it has items');
|
|
109
|
+
assert(!(await evalJs('document.body.innerText.includes("Project Operations")')), 'project detail should be hidden while summary panel is open');
|
|
110
|
+
const pendingShot = await screenshot('task15-20-pending-panel.png');
|
|
111
|
+
|
|
112
|
+
await evalJs(`[...document.querySelectorAll('button')].find(b => b.innerText.includes('Pending'))?.click()`);
|
|
113
|
+
await evalJs(`[...document.querySelectorAll('button')].find(b => b.innerText.includes('Recent issues') || b.innerText.includes('Issues'))?.click()`);
|
|
114
|
+
await waitFor(() => evalJs('document.body.innerText.includes("Issue center")'), 'issues panel open');
|
|
115
|
+
const issueGroups = await evalJs('document.querySelectorAll("section details").length');
|
|
116
|
+
if (await evalJs('!document.body.innerText.includes("No blocking issue detected.")')) {
|
|
117
|
+
assert(issueGroups > 0, 'issues panel should render per-project collapsible details when issues exist');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await evalJs(`document.querySelector('aside .min-h-0 button')?.click()`);
|
|
121
|
+
await waitFor(() => evalJs('!document.body.innerText.includes("Pending commit details") && !document.body.innerText.includes("Issue center")'), 'summary panel closes on project switch');
|
|
122
|
+
|
|
123
|
+
await evalJs(`[...document.querySelectorAll('button')].find(b => b.innerText.includes('Remove Project'))?.click()`);
|
|
124
|
+
await waitFor(() => evalJs('document.body.innerText.includes("Also delete KB files")'), 'remove modal open');
|
|
125
|
+
await waitFor(() => evalJs('document.body.innerText.includes("D:\\\\SanQian.Xu\\\\knowledge") && !document.body.innerText.includes("Not found")'), 'remove preview loaded');
|
|
126
|
+
assert(await evalJs('document.body.innerText.includes("KB path")'), 'remove modal should show KB path');
|
|
127
|
+
const removeShot = await screenshot('task15-20-remove-modal.png');
|
|
128
|
+
await evalJs(`[...document.querySelectorAll('button')].find(b => b.innerText.trim() === '×')?.click()`);
|
|
129
|
+
|
|
130
|
+
await evalJs(`[...document.querySelectorAll('button')].find(b => b.innerText.includes('Runs / Drafts'))?.click()`);
|
|
131
|
+
await waitFor(() => evalJs('document.body.innerText.includes("Runs and Drafts")'), 'runs view');
|
|
132
|
+
assert(await evalJs('document.body.innerText.includes("All branches") || document.querySelector("select")'), 'runs view should expose branch filtering controls when a run is selected or controls area exists');
|
|
133
|
+
const runsShot = await screenshot('task15-20-runs-view.png');
|
|
134
|
+
|
|
135
|
+
assert(errors.length === 0, `console errors: ${errors.join('\\n')}`);
|
|
136
|
+
console.log('TASK-015..020 UI flow test passed');
|
|
137
|
+
console.log(`screenshots:\n${dashboardShot}\n${pendingShot}\n${removeShot}\n${runsShot}`);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.error('TASK-015..020 UI flow test failed:', e.message);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
} finally {
|
|
142
|
+
child.kill();
|
|
143
|
+
}
|
|
144
|
+
})();
|
|
@@ -210,10 +210,10 @@ function assert(cond, msg) {
|
|
|
210
210
|
});
|
|
211
211
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
212
212
|
r = await send('Runtime.evaluate', {
|
|
213
|
-
expression: '/Knowledge output language|知识库输出语言/.test(document.body.innerText)',
|
|
213
|
+
expression: '/Knowledge output language|知识库输出语言/.test(document.body.innerText) && /Test model|测试模型/.test(document.body.innerText) && /Base URL|Base 地址/.test(document.body.innerText)',
|
|
214
214
|
returnByValue: true,
|
|
215
215
|
});
|
|
216
|
-
assert(r.result.value, 'AI
|
|
216
|
+
assert(r.result.value, 'AI model settings did not render');
|
|
217
217
|
|
|
218
218
|
const shot = await send('Page.captureScreenshot', { format: 'png', fullPage: true });
|
|
219
219
|
fs.writeFileSync(path.join(OUT_DIR, 'smoke-final.png'), Buffer.from(shot.data, 'base64'));
|