mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-26 06:43:15 +08:00
Add source-backed repository navigation, selection context and a shared change analysis workspace for symbols, working-tree changes and Git comparisons. Persist agent events and project logs through the existing daemon with SQLite schema 4, bounded polling, retention and restart-safe deduplication. Repair compact RPC decoding, path-search races, source pagination and identity, large map responses, live error propagation and explicit local diagnosis flows. Include the real-repository browser runners and focused regression coverage. Validation: 2473 frontend tests, 203 acceptance checks, production build, style/promise gates, 5 reference-boundary checks and 7 local hook tests pass. The complete repository-map browser flow also passes after daemon restart. Global C lint and cross-platform release validation remain open and are recorded in the accompanying handoff report. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
208 lines
16 KiB
JavaScript
208 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
// Reproducible, explicitly synthetic repository fixture; real daemon and indexer.
|
|
// Requires the existing isolated daemon at 127.0.0.1:9749. No server is started.
|
|
import assert from 'node:assert/strict';
|
|
import { mkdir, writeFile, readFile, readdir, chmod } from 'node:fs/promises';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import { dirname, resolve, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { chromium } from 'playwright';
|
|
|
|
const UI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const OUT = join(UI_ROOT, 'verification/pr-2068');
|
|
const ORIGIN = 'http://127.0.0.1:9749';
|
|
const CACHE = '/tmp/cbm-2068-cache';
|
|
const FIXTURE = '/tmp/cbm-2068-error-fixture';
|
|
const PROJECT = 'pr2068-error-fixture';
|
|
const proofPrefix = process.env.CBM_VERIFICATION_PREFIX ?? '';
|
|
if (!/^[a-z0-9-]*$/.test(proofPrefix)) throw new Error('Invalid local proof prefix');
|
|
const marker = 'PR 2068 EXPLICIT TEST FIXTURE - synthetic files for local error verification\n';
|
|
const report = { startedAt: new Date().toISOString(), origin: ORIGIN, fixture: FIXTURE, project: PROJECT, syntheticFixture: true,
|
|
actions: [], directRequests: [], browserRequests: [], blockedExternalRequests: [], screenshots: [], checks: {}, cleanup: [] };
|
|
let browser;
|
|
let ownsFixture = false;
|
|
let rpcId = 0;
|
|
const delay = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
const saveReport = () => writeFile(join(OUT, `${proofPrefix}errors-e2e.json`), JSON.stringify(report, null, 2) + '\n');
|
|
|
|
async function json(path, body) {
|
|
const response = await fetch(ORIGIN + path, { method: body ? 'POST' : 'GET', ...(body ? { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {}) });
|
|
const text = await response.text();
|
|
let payload;
|
|
try { payload = JSON.parse(text); } catch { payload = { raw: text }; }
|
|
report.directRequests.push({ at: new Date().toISOString(), method: body ? 'POST' : 'GET', path, status: response.status });
|
|
if (!response.ok) throw new Error(`${path}: HTTP ${response.status}: ${text.slice(0, 300)}`);
|
|
return payload;
|
|
}
|
|
|
|
async function rpc(name, args) {
|
|
const response = await json('/rpc', { jsonrpc: '2.0', id: ++rpcId, method: 'tools/call', params: { name, arguments: { ...args, format: 'json' } } });
|
|
if (response.error || response.result?.isError) throw new Error(JSON.stringify(response));
|
|
return JSON.parse(response.result.content[0].text);
|
|
}
|
|
|
|
async function index(project) {
|
|
const begin = await json('/api/index', { root_path: FIXTURE, project_name: project });
|
|
report.actions.push({ action: 'POST /api/index', project, root: FIXTURE, acknowledgement: begin });
|
|
const deadline = Date.now() + 180000;
|
|
while (Date.now() < deadline) {
|
|
const jobs = await json('/api/index-status');
|
|
const job = jobs.find((item) => item.slot === begin.slot);
|
|
if (job && job.status !== 'indexing') {
|
|
report.actions.push({ action: 'Index terminal state', project, job });
|
|
return job;
|
|
}
|
|
await delay(500);
|
|
}
|
|
throw new Error(`Index job timed out: ${project}`);
|
|
}
|
|
|
|
async function capture(page, name) {
|
|
name = proofPrefix + name;
|
|
await page.screenshot({ path: join(OUT, name), fullPage: false });
|
|
report.screenshots.push({ file: name, url: page.url(), at: new Date().toISOString(), sha256: createHash('sha256').update(await readFile(join(OUT, name))).digest('hex') });
|
|
}
|
|
|
|
async function fixturePrepare() {
|
|
await mkdir(FIXTURE, { recursive: true });
|
|
const old = await readFile(join(FIXTURE, 'TEST-FIXTURE.txt'), 'utf8').catch((error) => { if (error.code === 'ENOENT') return null; throw error; });
|
|
if (old !== null && old !== marker) throw new Error('Refusing to modify an unrecognized existing fixture directory');
|
|
if (old === null && (await readdir(FIXTURE)).length > 0) throw new Error('Refusing to overwrite files in an unmarked existing directory');
|
|
await writeFile(join(FIXTURE, 'TEST-FIXTURE.txt'), marker);
|
|
ownsFixture = true;
|
|
await writeFile(join(FIXTURE, 'readable.c'), '/* EXPLICIT TEST FIXTURE */\nint fixture_readable(void) { return 42; }\n');
|
|
await writeFile(join(FIXTURE, 'partial.c'), '/* EXPLICIT TEST FIXTURE: malformed declaration */\nint fixture_broken( {\n');
|
|
await writeFile(join(FIXTURE, 'excluded.c'), '/* EXPLICIT TEST FIXTURE intentionally excluded */\nint excluded(void) { return 0; }\n');
|
|
await writeFile(join(FIXTURE, '.cbmignore'), 'excluded.c\n');
|
|
const unreadable = join(FIXTURE, 'unreadable.c');
|
|
await chmod(unreadable, 0o600).catch((error) => { if (error.code !== 'ENOENT') throw error; });
|
|
await writeFile(unreadable, '/* EXPLICIT TEST FIXTURE: chmod 000 during this run */\nint inaccessible(void) { return 1; }\n');
|
|
report.actions.push({ action: 'Created synthetic C fixtures initially readable, including malformed partial.c and excluded.c', path: FIXTURE });
|
|
}
|
|
|
|
async function failedJobWhileWatching(page, workspace, project) {
|
|
await page.bringToFront();
|
|
await page.locator(`[data-workspace-tab="${workspace}"]`).click();
|
|
await page.locator('.daemon-alerts-heading > span').filter({ hasText: 'Last checked' }).waitFor({ timeout: 20000 });
|
|
const before = await json('/api/logs?lines=500&min_level=error');
|
|
const job = await index(project);
|
|
assert.equal(job.status, 'error', 'Unreadable fixture input must fail a real daemon index job');
|
|
let event;
|
|
const deadline = Date.now() + 20000;
|
|
while (Date.now() < deadline) {
|
|
const logs = await json('/api/logs?lines=500&min_level=error');
|
|
event = logs.records?.find((entry) => entry.id > (before.cursor ?? 0) && entry.message.includes('ui.index.done') && entry.message.includes(FIXTURE));
|
|
if (event) break;
|
|
await delay(250);
|
|
}
|
|
assert.ok(event, 'A real failed index completion must reach persisted daemon errors');
|
|
const exact = page.locator('.daemon-alert-error').filter({ hasText: `Event #${event.id}` });
|
|
await exact.waitFor({ state: 'visible', timeout: 20000 });
|
|
assert.ok((await exact.innerText()).includes('ERROR'));
|
|
report.checks[`${workspace}LiveError`] = { event, observedWithoutPageReload: true, text: await exact.innerText(), priorityPanelBounds: await page.locator('[data-testid="daemon-alerts"]').boundingBox() };
|
|
if (workspace === 'system') {
|
|
await page.locator('#system-tab-indexes').click();
|
|
await page.locator('.system-job-error').first().waitFor({ timeout: 15000 });
|
|
await page.locator('#system-tab-logs').click();
|
|
await page.getByLabel('Log severity', { exact: true }).selectOption('error');
|
|
await page.locator('.system-log-record.system-log-error').first().waitFor({ timeout: 15000 });
|
|
const daemonErrors = await json('/api/logs?lines=200&min_level=error');
|
|
await page.locator('.system-log-meta').filter({ hasText: `${daemonErrors.total} matching retained events` }).waitFor({ timeout: 15000 });
|
|
report.checks.systemFiltersBeforeLimit = { total: daemonErrors.total, visibleErrorRows: await page.locator('.system-log-record.system-log-error').count(), text: await page.locator('.system-log-meta').innerText() };
|
|
assert.ok(daemonErrors.total > 0, 'Retained historical errors cannot disappear behind newer info logs');
|
|
await page.getByLabel('Log scope', { exact: true }).selectOption('project');
|
|
const scopedErrors = await json(`/api/logs?lines=200&min_level=error&project=${encodeURIComponent(project)}`);
|
|
await page.locator('.system-log-meta').filter({ hasText: `${scopedErrors.total} matching retained events` }).waitFor({ timeout: 15000 });
|
|
await page.locator('.system-log-record.system-log-error').filter({ hasText: `Event #${event.id}` }).waitFor({ timeout: 15000 });
|
|
report.checks.systemProjectScope = { total: scopedErrors.total, project, text: await page.locator('.system-log-meta').innerText() };
|
|
await page.getByPlaceholder('Find a source, path or message…').fill('semantic_manifest.err');
|
|
const searched = await json(`/api/logs?lines=200&min_level=error&project=${encodeURIComponent(project)}&q=semantic_manifest.err`);
|
|
await page.locator('.system-log-meta').filter({ hasText: `${searched.total} matching retained events` }).waitFor({ timeout: 15000 });
|
|
report.checks.systemSearchBeforeLimit = { total: searched.total, query: searched.query, text: await page.locator('.system-log-meta').innerText() };
|
|
assert.ok(searched.total > 0);
|
|
await capture(page, 'errors-system-filtered-history.png');
|
|
await page.getByPlaceholder('Find a source, path or message…').fill('');
|
|
}
|
|
await capture(page, `errors-after-${workspace}.png`);
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(OUT, { recursive: true });
|
|
await fixturePrepare();
|
|
report.indexWithCoverage = await index(PROJECT);
|
|
assert.equal(report.indexWithCoverage.status, 'done', 'Initial readable fixture must establish a real partial index');
|
|
report.indexStatus = await rpc('index_status', { project: PROJECT });
|
|
report.coverage = await rpc('check_index_coverage', { project: PROJECT, scopes: ['.'], paths: ['unreadable.c', 'partial.c', 'excluded.c'] });
|
|
report.checks.coverageContainsPartial = JSON.stringify(report.coverage).includes('parse_partial');
|
|
report.checks.coverageContainsExclusion = JSON.stringify(report.coverage).includes('cbmignore');
|
|
assert.ok(report.checks.coverageContainsPartial, 'Malformed fixture must produce actual parse_partial coverage');
|
|
assert.ok(report.checks.coverageContainsExclusion, '.cbmignore fixture must produce actual exclusion evidence');
|
|
await chmod(join(FIXTURE, 'unreadable.c'), 0);
|
|
await assert.rejects(readFile(join(FIXTURE, 'unreadable.c')), { code: 'EACCES' });
|
|
report.actions.push({ action: 'Set unreadable.c chmod 000; real EACCES verified; next reindex must fail', path: FIXTURE });
|
|
browser = await chromium.launch({ headless: true, args: ['--disable-background-networking', '--disable-component-update', '--disable-sync', '--no-first-run', '--use-angle=swiftshader'] });
|
|
const context = await browser.newContext({ viewport: { width: 1560, height: 1100 }, reducedMotion: 'reduce' });
|
|
await context.route('**/*', async (route) => {
|
|
const req = route.request(); const url = req.url();
|
|
report.browserRequests.push({ at: new Date().toISOString(), method: req.method(), url, ...(url === `${ORIGIN}/rpc` ? { tool: req.postDataJSON()?.params?.name } : {}) });
|
|
if (url.startsWith(ORIGIN + '/') || url.startsWith('data:') || url.startsWith('blob:')) return route.continue();
|
|
report.blockedExternalRequests.push(url); return route.abort();
|
|
});
|
|
const page = await context.newPage();
|
|
page.on('pageerror', (error) => { (report.browserErrors ??= []).push(error.message); });
|
|
await page.goto(`${ORIGIN}/?project=${PROJECT}`, { waitUntil: 'domcontentloaded' });
|
|
await page.locator('[data-testid="daemon-alerts"]').waitFor({ timeout: 60000 });
|
|
if (await page.locator('.cbm-welcome-backdrop').isVisible()) {
|
|
await page.locator('.cbm-welcome .cbm-primary').click();
|
|
report.actions.push({ action: 'Closed first-visit welcome via its normal start button' });
|
|
}
|
|
await failedJobWhileWatching(page, 'system', PROJECT);
|
|
await failedJobWhileWatching(page, 'galaxy', PROJECT);
|
|
report.coverageAfterFailedRefresh = await rpc('check_index_coverage', { project: PROJECT, scopes: ['.'], paths: ['unreadable.c', 'partial.c', 'excluded.c'] });
|
|
const beforeDiagnosis = report.browserRequests.length;
|
|
await page.getByRole('button', { name: 'Inspect index coverage', exact: true }).click();
|
|
const panel = page.getByRole('dialog', { name: 'Index coverage diagnosis', exact: true });
|
|
await panel.waitFor();
|
|
assert.equal(await panel.locator('textarea').count(), 0, 'Opening diagnosis must not generate a report');
|
|
report.checks.beforeExplicitDiagnosis = { reportAbsent: true, requests: report.browserRequests.slice(beforeDiagnosis) };
|
|
const diagnosisStart = report.browserRequests.length;
|
|
await panel.getByRole('button', { name: 'Run local diagnosis', exact: true }).click();
|
|
await panel.getByRole('textbox', { name: 'Editable local diagnostic report' }).waitFor({ timeout: 30000 });
|
|
report.localReport = await panel.getByRole('textbox', { name: 'Editable local diagnostic report' }).inputValue();
|
|
assert.ok(report.localReport.includes('Source lines: 2-2'), 'Actual parser source range must survive the shared coverage join and local report');
|
|
report.checks.localReportPreservesParserSourceRanges = true;
|
|
report.checks.localDiagnosis = { text: await panel.innerText(), requests: report.browserRequests.slice(diagnosisStart) };
|
|
await capture(page, 'errors-local-diagnosis.png');
|
|
await panel.getByRole('textbox', { name: 'Editable local diagnostic report' }).scrollIntoViewIfNeeded();
|
|
await capture(page, 'errors-local-report.png');
|
|
const draft = 'EXPLICIT TEST FIXTURE: locally reviewed and redacted draft. No publication requested.';
|
|
await panel.getByRole('textbox', { name: 'Editable local diagnostic report' }).fill(draft);
|
|
const downloadPromise = page.waitForEvent('download');
|
|
await panel.getByRole('button', { name: 'Download edited report', exact: true }).click();
|
|
const download = await downloadPromise;
|
|
const draftPath = join(OUT, `${proofPrefix}errors-reviewed-local-report.txt`); await download.saveAs(draftPath);
|
|
assert.equal(await readFile(draftPath, 'utf8'), draft);
|
|
report.checks.downloadContainsOnlyEditedText = true;
|
|
report.checks.onlySameDaemonOrigin = report.browserRequests.filter((entry) => !entry.url.startsWith('blob:') && !entry.url.startsWith('data:')).every((entry) => new URL(entry.url).origin === ORIGIN);
|
|
report.sqliteEvidence = JSON.parse(execFileSync('python3', ['-c', `import sqlite3,json\na=sqlite3.connect('file:${CACHE}/activity.db?mode=ro',uri=True)\ns=sqlite3.connect('file:${CACHE}/${PROJECT}.db?mode=ro',uri=True)\nprint(json.dumps({'logs':a.execute("SELECT id,ts,level,source,message FROM daemon_logs WHERE message LIKE '%cbm-2068-error-fixture%' ORDER BY id").fetchall(),'coverage':s.execute("SELECT project,rel_path,kind,detail FROM index_coverage WHERE project=?",('${PROJECT}',)).fetchall()}))`], { encoding: 'utf8' }));
|
|
const cutoff = report.checks.systemLiveError.event.id;
|
|
report.checks.filterCountsVerifiedInSqlite = JSON.parse(execFileSync('python3', ['-c', `import sqlite3,json,sys
|
|
conn=sqlite3.connect('file:${CACHE}/activity.db?mode=ro',uri=True)
|
|
cutoff=int(sys.argv[1]); project=sys.argv[2]
|
|
print(json.dumps({'as_of_event_id':cutoff,'daemon_errors':conn.execute('SELECT count(*) FROM daemon_logs WHERE level>=3 AND id<=?',(cutoff,)).fetchone()[0],'project_errors':conn.execute('SELECT count(*) FROM daemon_logs WHERE level>=3 AND id<=? AND project=?',(cutoff,project)).fetchone()[0],'project_manifest_errors':conn.execute("SELECT count(*) FROM daemon_logs WHERE level>=3 AND id<=? AND project=? AND instr(lower(message),'semantic_manifest.err')>0",(cutoff,project)).fetchone()[0]}))`, String(cutoff), PROJECT], { encoding: 'utf8' }));
|
|
assert.equal(report.checks.filterCountsVerifiedInSqlite.daemon_errors, report.checks.systemFiltersBeforeLimit.total);
|
|
assert.equal(report.checks.filterCountsVerifiedInSqlite.project_errors, report.checks.systemProjectScope.total);
|
|
assert.equal(report.checks.filterCountsVerifiedInSqlite.project_manifest_errors, report.checks.systemSearchBeforeLimit.total);
|
|
report.completedAt = new Date().toISOString();
|
|
}
|
|
|
|
try { await main(); }
|
|
catch (error) { report.failure = error.stack ?? String(error); process.exitCode = 1; }
|
|
finally {
|
|
if (ownsFixture) await chmod(join(FIXTURE, 'unreadable.c'), 0o600).then(() => report.cleanup.push('unreadable.c restored to 0600'), () => {});
|
|
if (browser) await browser.close();
|
|
await saveReport();
|
|
console.log(JSON.stringify({ report: join(OUT, `${proofPrefix}errors-e2e.json`), completed: !!report.completedAt, failure: report.failure, screenshots: report.screenshots, cleanup: report.cleanup }, null, 2));
|
|
}
|