Files
codegraph/scripts/validation/extensions-runtime.test.cjs

85 lines
5.1 KiB
JavaScript

// Run after npm run build: node --test scripts/validation/extensions-runtime.test.cjs
const { test, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { CodeGraph } = require('../../dist');
const { ExtensionManager } = require('../../dist/plugins/manager');
const { clearProjectConfigCache } = require('../../dist/project-config');
process.env.CODEGRAPH_TELEMETRY = '0';
process.env.CODEGRAPH_PARSE_WORKERS = '2';
process.env.CODEGRAPH_RESOLVE_WORKERS = '2';
process.env.CODEGRAPH_PARALLEL_RESOLVE_MIN = '0';
const roots = [];
after(() => roots.forEach(root => fs.rmSync(root, { recursive: true, force: true })));
function root() {
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cg-compiled-')));
roots.push(dir);
fs.writeFileSync(path.join(dir, 'app.ts'), 'export function entry() {}\nexport function handler() {}\n');
return dir;
}
function bytes(version = '1.0.0', failure = '') {
return Buffer.from(JSON.stringify({ format: 'codegraph-extension-1', package: {
name: '@test/compiled', version, main: 'index.cjs',
codegraph: { id: 'compiled', apiVersion: 1, capabilities: ['frameworks', 'synthPasses'] },
}, files: { 'index.cjs': `const {threadId}=require('node:worker_threads'); module.exports = ctx => ({
frameworks:[{name:'routes',languages:['typescript'],detect:()=>true,resolve:()=>null,
extract(file){if(${JSON.stringify(failure)}==='parse' && threadId>0)throw Error('parse worker rejection');
return {nodes:[{id:'plugin:compiled:'+file,kind:'route',name:ctx.options.route||'/initial',
qualifiedName:file+'::route',filePath:file,language:'typescript',startLine:1,endLine:1,startColumn:0,endColumn:0,
updatedAt:0,docstring:JSON.stringify({threadId,root:ctx.projectRoot})}],references:[]};}}],
synthPasses:[{name:'flow',languages:['typescript'],async run(graph){
if(${JSON.stringify(failure)}==='resolver' && threadId>0)throw Error('resolver worker rejection');
const a=graph.getNodesByName('entry')[0],b=graph.getNodesByName('handler')[0];
return a&&b?[{source:a.id,target:b.id,kind:'calls',metadata:{label:'Compiled test',threadId,root:ctx.projectRoot}}]:[];
}}]});` }}));
}
function inspect(dir) {
const db = new DatabaseSync(path.join(dir, '.codegraph/codegraph.db'), { readOnly: true });
try {
const nodes = db.prepare('SELECT * FROM nodes ORDER BY id').all().map(({ updated_at, ...n }) => n);
const edges = db.prepare('SELECT source,target,kind,metadata,line,col,provenance FROM edges ORDER BY source,target,kind,line,col,metadata').all();
return { nodes, edges, routes: nodes.filter(n => n.kind === 'route'), pluginEdges: edges.filter(e => JSON.parse(e.metadata || '{}').synthesizedBy === 'compiled') };
} finally { db.close(); }
}
test('compiled parse and resolver workers activate, isolate root/options, and clean lifecycle contributions', async () => {
const a = root(), b = root();
const manager = new ExtensionManager(a);
await manager.install({ bytes: bytes() });
let data = inspect(a);
assert.equal(data.routes.length, 1);
assert.ok(JSON.parse(data.routes[0].docstring).threadId > 0, 'parse contribution ran in a worker');
assert.ok(JSON.parse(data.pluginEdges[0].metadata).threadId > 0, 'synthesis ran in a resolver worker');
const config = JSON.parse(fs.readFileSync(path.join(a, 'codegraph.json')));
config.plugins[0].options = { route: '/changed' };
fs.writeFileSync(path.join(a, 'codegraph.json'), JSON.stringify(config));
clearProjectConfigCache();
let graph = await CodeGraph.open(a);
try { await graph.sync(); } finally { graph.close(); }
assert.deepEqual(inspect(a).routes.map(n => n.name), ['/changed']);
await new ExtensionManager(b).install({ bytes: bytes() });
assert.deepEqual(inspect(b).routes.map(n => n.name), ['/initial']);
assert.equal(JSON.parse(inspect(b).routes[0].docstring).root, b);
assert.equal(JSON.parse(inspect(a).routes[0].docstring).root, a);
await manager.setEnabled('compiled', false);
assert.equal(inspect(a).routes.length, 0); assert.equal(inspect(a).pluginEdges.length, 0);
await manager.setEnabled('compiled', true);
assert.deepEqual(inspect(a).routes.map(n => n.name), ['/changed']);
await manager.remove('compiled');
assert.equal(inspect(a).routes.length, 0); assert.equal(inspect(a).pluginEdges.length, 0);
assert.equal(inspect(b).routes.length, 1);
});
test('failed parse/resolver worker updates preserve the prior config and entire graph', async () => {
const dir = root(), manager = new ExtensionManager(dir);
await manager.install({ bytes: bytes() });
const before = inspect(dir), config = fs.readFileSync(path.join(dir, 'codegraph.json'), 'utf8');
for (const stage of ['parse', 'resolver']) {
await assert.rejects(manager.install({ bytes: bytes('2.0.0', stage) }), /activation failed/);
assert.equal(fs.readFileSync(path.join(dir, 'codegraph.json'), 'utf8'), config);
assert.deepEqual(inspect(dir), before);
}
assert.ok(!fs.readdirSync(path.join(dir, '.codegraph')).some(name => name.startsWith('extension-stage-')));
});