Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,11 @@ jobs:
- name: Compile and Bundle Production Assets
run: npm run build

- name: Run Headless E2E Smoke Suite
run: npm run test:e2e

- name: Run 50k DAG Performance Benchmark
run: npm run benchmark

- name: Verify VSIX Package Creation
run: npx vsce package --no-git-tag-version
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@
"dev": "node esbuild.js --watch",
"test": "node -e \"require('esbuild').buildSync({entryPoints:['test/runTests.ts'],outfile:'dist/test.js',bundle:true,platform:'node',alias:{'vscode':'./test/vscodeMock.ts'}}); require('./dist/test.js')\"",
"test:live": "node -e \"require('esbuild').buildSync({entryPoints:['test/verifyLiveIntegration.ts'],outfile:'dist/verifyLive.js',bundle:true,platform:'node',alias:{'vscode':'./test/vscodeMock.ts'}}); require('./dist/verifyLive.js')\"",
"test:e2e": "node scripts/run-e2e-smoke.js",
"benchmark": "node scripts/benchmark-50k.js",
"mcp": "node dist/mcp.js",
"package": "vsce package"
},
Expand Down
132 changes: 132 additions & 0 deletions scripts/benchmark-50k.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { performance } = require('perf_hooks');
const esbuild = require('esbuild');

function loadDagModules() {
const repoRoot = path.resolve(__dirname, '..');
const tmpOut = path.join(os.tmpdir(), `commitflow-bench-${process.pid}-${Date.now()}.js`);
esbuild.buildSync({
stdin: {
contents: `
export { GraphLayout } from './src/graph/graphLayout';
export { DagWorkerEngine } from './src/graph/dagWorkerClient';
`,
resolveDir: repoRoot,
loader: 'ts',
},
bundle: true,
platform: 'node',
format: 'cjs',
outfile: tmpOut,
});

try {
return require(tmpOut);
} finally {
try {
fs.rmSync(tmpOut, { force: true });
} catch {
// ignore cleanup errors on temp file
}
}
}

function generateSyntheticCommits(count) {
const commits = new Array(count);
const baseMs = Date.UTC(2026, 0, 1, 0, 0, 0);
const hexHashes = new Array(count);

for (let i = 0; i < count; i++) {
hexHashes[i] = i.toString(16).padStart(8, '0').padEnd(40, 'a');
}

for (let i = 0; i < count; i++) {
const parentHashes = [];
if (i + 1 < count) {
parentHashes.push(hexHashes[i + 1]);
}
// Every 20th commit forms a merge/branch topology edge
if (i % 20 === 0 && i + 7 < count) {
parentHashes.push(hexHashes[i + 7]);
}

const author = {
name: i % 4 === 0 ? 'Elena' : 'Ignacio',
email: 'dev@commitflow.local',
date: new Date(baseMs - i * 60000),
relativeTime: `${i}m ago`,
avatarUrl: '',
};

commits[i] = {
hash: hexHashes[i],
shortHash: hexHashes[i].slice(0, 7),
parentHashes,
author,
committer: author,
subject: i % 20 === 0 ? `merge: integrate topic-${i} into main` : `feat(dag): commit #${i}`,
body: '',
refs:
i === 0
? [{ type: 'head', name: 'main', isHead: true }]
: i % 2500 === 0
? [{ type: 'tag', name: `v0.${Math.floor(i / 2500)}.0`, isHead: false }]
: [],
};
}

return commits;
}

async function runBenchmark() {
const COMMIT_COUNT = 50000;
const BUDGET_MS = 2500;

console.log(`\n======================================================`);
console.log(`CommitFlow 50k DAG Layout Performance Benchmark`);
console.log(`======================================================`);

const { GraphLayout, DagWorkerEngine } = loadDagModules();
const commits = generateSyntheticCommits(COMMIT_COUNT);

const memBeforeSync = process.memoryUsage().heapUsed;
const t0 = performance.now();
const syncNodes = GraphLayout.compute(commits, 'cyberpunk');
const syncElapsedMs = performance.now() - t0;
const memAfterSync = process.memoryUsage().heapUsed;

assert.strictEqual(syncNodes.length, COMMIT_COUNT, `Expected ${COMMIT_COUNT} nodes from GraphLayout.compute`);
assert.ok(
syncElapsedMs < BUDGET_MS,
`GraphLayout.compute(50k) exceeded ${BUDGET_MS}ms budget: ${syncElapsedMs.toFixed(2)}ms`
);

const engine = DagWorkerEngine.getInstance();
const t1 = performance.now();
const asyncNodes = await engine.computeLayoutAsync(commits, 'cyberpunk');
const asyncElapsedMs = performance.now() - t1;
engine.dispose();

assert.strictEqual(asyncNodes.length, COMMIT_COUNT, `Expected ${COMMIT_COUNT} nodes from DagWorkerEngine`);
assert.ok(
asyncElapsedMs < BUDGET_MS,
`DagWorkerEngine.computeLayoutAsync(50k) exceeded ${BUDGET_MS}ms budget: ${asyncElapsedMs.toFixed(2)}ms`
);

const deltaMb = ((memAfterSync - memBeforeSync) / (1024 * 1024)).toFixed(2);
const throughput = Math.round(COMMIT_COUNT / (syncElapsedMs / 1000));

console.log(` Commits rendered : ${COMMIT_COUNT.toLocaleString()}`);
console.log(` GraphLayout sync : ${syncElapsedMs.toFixed(2)} ms (${throughput.toLocaleString()} commits/sec)`);
console.log(` DagWorkerEngine : ${asyncElapsedMs.toFixed(2)} ms`);
console.log(` Heap delta (sync) : ${deltaMb} MB`);
console.log(` SLA Budget : < ${BUDGET_MS} ms [PASS]\n`);
}

runBenchmark().catch((err) => {
console.error('[CommitFlow] 50k Benchmark failed:', err);
process.exit(1);
});
193 changes: 193 additions & 0 deletions scripts/run-e2e-smoke.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
const assert = require('assert');
const fs = require('fs');
const Module = require('module');
const os = require('os');
const path = require('path');
const esbuild = require('esbuild');

const ROOT = path.resolve(__dirname, '..');

function verifyDistArtifacts() {
const requiredBundles = [
'dist/extension.js',
'dist/webview.js',
'dist/webview.css',
'dist/codicon.css',
'dist/mcp.js',
];

const stats = {};
for (const rel of requiredBundles) {
const abs = path.join(ROOT, rel);
assert.ok(fs.existsSync(abs), `Missing compiled bundle artifact: ${rel}`);
const st = fs.statSync(abs);
assert.ok(st.size > 0, `Empty bundle artifact: ${rel}`);
stats[rel] = st.size;
}
return stats;
}

function verifyManifest() {
const pkgPath = path.join(ROOT, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));

assert.ok(pkg.main && pkg.main.includes('dist/extension.js'), `Unexpected manifest main: ${pkg.main}`);
assert.ok(Array.isArray(pkg.contributes?.commands) && pkg.contributes.commands.length >= 15, 'Missing contributed commands');
assert.ok(pkg.contributes?.views && Object.keys(pkg.contributes.views).length > 0, 'Missing contributed views');
assert.ok(pkg.contributes?.configuration?.properties, 'Missing contributed configuration properties');

return pkg;
}

async function verifyHeadlessExtensionLifecycle(pkg) {
const tmpMockFile = path.join(os.tmpdir(), `cf-vscodemock-${process.pid}-${Date.now()}.js`);
esbuild.buildSync({
entryPoints: [path.join(ROOT, 'test', 'vscodeMock.ts')],
bundle: true,
platform: 'node',
format: 'cjs',
outfile: tmpMockFile,
});

const originalLoad = Module._load;
const registeredCommands = new Set();
const registeredProviders = new Set();

try {
const rawMock = require(tmpMockFile);
const instrumentedVscode = {
...rawMock,
env: {
...rawMock.env,
language: 'en',
},
commands: {
...rawMock.commands,
registerCommand: (name, handler) => {
registeredCommands.add(name);
return rawMock.commands.registerCommand(name, handler);
},
},
window: {
...rawMock.window,
registerWebviewViewProvider: (viewId, provider, options) => {
registeredProviders.add(viewId);
return rawMock.window.registerWebviewViewProvider(viewId, provider, options);
},
},
};

Module._load = function (request, parent, isMain) {
if (request === 'vscode') {
return instrumentedVscode;
}
return originalLoad.apply(this, arguments);
};

const extBundlePath = path.join(ROOT, 'dist', 'extension.js');
delete require.cache[require.resolve(extBundlePath)];
const ext = require(extBundlePath);

assert.strictEqual(typeof ext.activate, 'function', 'dist/extension.js must export activate()');
assert.strictEqual(typeof ext.deactivate, 'function', 'dist/extension.js must export deactivate()');

const secretMap = new Map();
const stateMap = new Map();
const mockContext = {
subscriptions: [],
extensionUri: rawMock.Uri.file(ROOT),
extensionPath: ROOT,
globalState: {
get: (k, d) => (stateMap.has(k) ? stateMap.get(k) : d),
update: (k, v) => {
stateMap.set(k, v);
return Promise.resolve();
},
keys: () => Array.from(stateMap.keys()),
},
workspaceState: {
get: (k, d) => (stateMap.has(k) ? stateMap.get(k) : d),
update: (k, v) => {
stateMap.set(k, v);
return Promise.resolve();
},
keys: () => Array.from(stateMap.keys()),
},
secrets: {
get: (k) => Promise.resolve(secretMap.get(k)),
store: (k, v) => {
secretMap.set(k, v);
return Promise.resolve();
},
delete: (k) => {
secretMap.delete(k);
return Promise.resolve();
},
},
};

await Promise.resolve(ext.activate(mockContext));
// Allow background initialization (e.g., async server startup) to settle before deactivate()
await new Promise((r) => setTimeout(r, 150));

assert.ok(
mockContext.subscriptions.length >= 10,
`Expected >= 10 subscriptions registered during activate(), got ${mockContext.subscriptions.length}`
);
assert.ok(registeredCommands.has('commitflow.openGraph'), 'Missing commitflow.openGraph command registration');
assert.ok(registeredCommands.has('commitflow.connectAi'), 'Missing commitflow.connectAi command registration');
assert.ok(
registeredCommands.size >= pkg.contributes.commands.length - 2,
`Too few commands registered (${registeredCommands.size} vs ${pkg.contributes.commands.length} in manifest)`
);
assert.ok(registeredProviders.size >= 1, 'Expected at least 1 registered WebviewViewProvider');

ext.deactivate();
for (const sub of mockContext.subscriptions) {
try {
if (sub && typeof sub.dispose === 'function') sub.dispose();
} catch {
// ignore mock dispose errors
}
}
await new Promise((r) => setTimeout(r, 80));

return {
subscriptions: mockContext.subscriptions.length,
registeredCommands: registeredCommands.size,
registeredProviders: registeredProviders.size,
};
} finally {
Module._load = originalLoad;
try {
fs.rmSync(tmpMockFile, { force: true });
} catch {
// ignore temp cleanup failure
}
}
}

async function main() {
console.log(`\n======================================================`);
console.log(`CommitFlow Headless E2E Smoke Suite`);
console.log(`======================================================`);

const bundleStats = verifyDistArtifacts();
console.log(` ✔ Bundles verified (${Object.keys(bundleStats).length} artifacts non-empty)`);

const pkg = verifyManifest();
console.log(` ✔ Manifest verified (${pkg.contributes.commands.length} contributed commands)`);

const lifecycle = await verifyHeadlessExtensionLifecycle(pkg);
console.log(
` ✔ Extension activate/deactivate OK (${lifecycle.registeredCommands} commands, ${lifecycle.registeredProviders} webview views, ${lifecycle.subscriptions} disposables)`
);
console.log(`======================================================\n`);
}

main()
.then(() => process.exit(0))
.catch((err) => {
console.error('[CommitFlow] E2E smoke suite failed:', err);
process.exit(1);
});
Loading
Loading