mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
105 lines
6.1 KiB
TypeScript
105 lines
6.1 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { CodeGraph } from '../src';
|
|
import { ExtensionManager } from '../src/plugins/manager';
|
|
import { loadPlugins } from '../src/plugins/loader';
|
|
import { parsePackage, type ExtensionPackage } from '../src/plugins/package';
|
|
import { getAllFrameworkResolvers } from '../src/resolution/frameworks';
|
|
import { withPlugins } from '../src/plugins/registry';
|
|
|
|
const roots: string[] = [];
|
|
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
|
|
function project(): string {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-extensions-'));
|
|
roots.push(root);
|
|
fs.writeFileSync(path.join(root, 'app.ts'), 'export function entry() {}\nexport function handler() {}\n');
|
|
return root;
|
|
}
|
|
function bundle(version = '1.0.0', broken = false): Buffer {
|
|
const p: ExtensionPackage = {
|
|
format: 'codegraph-extension-1',
|
|
package: { name: '@test/routes', version, main: 'index.cjs', codegraph: { id: 'test-routes', apiVersion: 1, capabilities: ['frameworks', 'synthPasses'] } },
|
|
files: { 'index.cjs': `module.exports = ctx => ({
|
|
frameworks: [{name:'routes', languages:['typescript'], detect:()=>true, resolve:()=>null,
|
|
extract(file) { ${broken ? "throw new Error('broken extraction');" : ''}
|
|
return {nodes:[{id:'plugin:test-routes:'+file, kind:'route', name:ctx.options.route || '/test', qualifiedName:file+'::test',
|
|
filePath:file, language:'typescript', startLine:1,endLine:1,startColumn:0,endColumn:0,updatedAt:0}],references:[]};
|
|
}}],
|
|
synthPasses:[{name:'dispatch', languages:['typescript'], async run(ctx) {
|
|
const a=ctx.getNodesByName('entry')[0], b=ctx.getNodesByName('handler')[0];
|
|
return a && b ? [{source:a.id,target:b.id,kind:'calls',metadata:{label:'Test dispatch',registeredAt:'app.ts:1'}}] : [];
|
|
}}]
|
|
});` },
|
|
};
|
|
return Buffer.from(JSON.stringify(p));
|
|
}
|
|
|
|
describe('managed framework extensions', () => {
|
|
it('installs into a non-Node project, activates, disables, enables and removes graph contributions', async () => {
|
|
const root = project();
|
|
fs.writeFileSync(path.join(root, 'codegraph.json'), JSON.stringify({ exclude: ['vendor/'] }));
|
|
const states: string[] = [];
|
|
const messages: string[] = [];
|
|
const manager = new ExtensionManager(root, p => { states.push(p.state); messages.push(p.message); });
|
|
await manager.install({ bytes: bundle() });
|
|
let graph = await CodeGraph.open(root);
|
|
expect(graph.getNodesByKind('route').map(n => n.name)).toEqual(['/test']);
|
|
const entry = graph.getNodesByKind('function').find(n => n.name === 'entry')!;
|
|
expect(graph.getOutgoingEdges(entry.id).some(e => e.metadata?.synthesizedBy === 'test-routes')).toBe(true);
|
|
graph.close();
|
|
expect(states.at(-1)).toBe('ready');
|
|
expect(JSON.parse(fs.readFileSync(path.join(root, 'codegraph.json'), 'utf8')).exclude).toEqual(['vendor/']);
|
|
await manager.setEnabled('test-routes', false);
|
|
expect(messages.at(-1)).toBe('Extension disabled and graph refreshed');
|
|
graph = await CodeGraph.open(root);
|
|
expect(graph.getNodesByKind('route')).toEqual([]);
|
|
expect(graph.getOutgoingEdges(entry.id).some(e => e.metadata?.synthesizedBy === 'test-routes')).toBe(false);
|
|
graph.close();
|
|
await manager.setEnabled('test-routes', true);
|
|
graph = await CodeGraph.open(root); expect(graph.getNodesByKind('route')).toHaveLength(1); graph.close();
|
|
await manager.remove('test-routes');
|
|
expect(messages.at(-1)).toBe('Extension removed and graph refreshed');
|
|
graph = await CodeGraph.open(root); expect(graph.getNodesByKind('route')).toHaveLength(0); graph.close();
|
|
expect(manager.list()).toEqual([]);
|
|
}, 90_000);
|
|
|
|
it('retains the working package, config and graph when an update throws', async () => {
|
|
const root = project(); const manager = new ExtensionManager(root);
|
|
await manager.install({ bytes: bundle() });
|
|
const before = fs.readFileSync(path.join(root, 'codegraph.json'), 'utf8');
|
|
await expect(manager.install({ bytes: bundle('2.0.0', true) })).rejects.toThrow('activation failed');
|
|
expect(fs.readFileSync(path.join(root, 'codegraph.json'), 'utf8')).toBe(before);
|
|
const graph = await CodeGraph.open(root);
|
|
expect(graph.getNodesByKind('route')).toHaveLength(1);
|
|
graph.close();
|
|
}, 90_000);
|
|
|
|
it('isolates replacement and options between roots', async () => {
|
|
const a = project(), b = project();
|
|
await new ExtensionManager(a).install({ bytes: bundle(), replaces: ['drupal'] });
|
|
const ra = await loadPlugins(a), rb = await loadPlugins(b);
|
|
expect(withPlugins(ra, () => getAllFrameworkResolvers().some(r => r.name === 'drupal'))).toBe(false);
|
|
expect(withPlugins(rb, () => getAllFrameworkResolvers().some(r => r.name === 'drupal'))).toBe(true);
|
|
expect(getAllFrameworkResolvers().some(r => r.name === 'test-routes:routes')).toBe(false);
|
|
}, 90_000);
|
|
|
|
it('refuses unsupported manifests, traversal and modified bytes before evaluation', async () => {
|
|
const p = JSON.parse(bundle().toString()); p.files['../escape.cjs'] = 'bad';
|
|
expect(() => parsePackage(Buffer.from(JSON.stringify(p)))).toThrow('unsafe path');
|
|
delete p.files['../escape.cjs']; p.package.codegraph.apiVersion = 2;
|
|
expect(() => parsePackage(Buffer.from(JSON.stringify(p)))).toThrow('unsupported API');
|
|
const root = project(); const manager = new ExtensionManager(root);
|
|
await expect(manager.install({ bytes: bundle(), integrity: '0'.repeat(64) })).rejects.toThrow('integrity');
|
|
await manager.install({ bytes: bundle() });
|
|
const loaded = await loadPlugins(root);
|
|
fs.writeFileSync(loaded.resolved[0]!.entryPath, "throw new Error('MUST NOT EXECUTE')");
|
|
const refused = await loadPlugins(root);
|
|
expect(refused.resolved).toEqual([]);
|
|
expect(refused.diagnostics[0]!.message).toContain('not trusted');
|
|
const graph = await CodeGraph.open(root); // Read path still works and never loads code.
|
|
expect(graph.getNodesByKind('route')).toHaveLength(1); graph.close();
|
|
}, 90_000);
|
|
});
|