'use strict';
const { test, describe, beforeEach } = require('node:test');
const assert = require('node:assert/strict');
const path = require('path');
const Module = require('module');
function makeVscodeMock({ copilotInstalled = true, copilotActive = true } = {}) {
const subscriptions = [];
const registeredCommands = {};
function makeEvent() {
let _handler = null;
const event = (handler) => {
_handler = handler;
return { dispose: () => { _handler = null; } };
};
event.fire = (...args) => _handler && _handler(...args);
return event;
}
const onDidChangeTextDocument = makeEvent();
const onDidSaveTextDocument = makeEvent();
const copilotExt = copilotInstalled
? { isActive: copilotActive }
: undefined;
const mock = {
workspace: {
onDidChangeTextDocument,
onDidSaveTextDocument,
asRelativePath: (fsPath) => fsPath.replace(/^\/tmp\/[^/]+\//, ''),
getConfiguration: (_section) => ({
get: (_key) => null,
}),
},
extensions: {
getExtension: (id) => {
if (id === 'GitHub.copilot' || id === 'GitHub.copilot-chat') {
return copilotExt;
}
return undefined;
},
},
window: {
activeTextEditor: null,
showInformationMessage: () => {},
},
commands: {
registerCommand: (id, fn) => {
registeredCommands[id] = fn;
return { dispose: () => {} };
},
},
Uri: { parse: (s) => ({ scheme: 'file', fsPath: s }) },
_fire: { change: onDidChangeTextDocument.fire, save: onDidSaveTextDocument.fire },
_commands: registeredCommands,
_subscriptions: subscriptions,
};
return mock;
}
const EXT_PATH = path.resolve(__dirname, '../../scripts/vscode-extension/extension.js');
function loadExtension(vscodeMock) {
const fakeVscodeId = require.resolve('path'); const vscodeKey = 'vscode';
const origResolve = Module._resolveFilename;
Module._resolveFilename = function (request, parent, isMain, options) {
if (request === 'vscode') return '__vscode_mock__';
return origResolve.call(this, request, parent, isMain, options);
};
require.cache['__vscode_mock__'] = {
id: '__vscode_mock__',
filename: '__vscode_mock__',
loaded: true,
exports: vscodeMock,
children: [],
paths: [],
};
delete require.cache[EXT_PATH];
let ext;
try {
ext = require(EXT_PATH);
} finally {
Module._resolveFilename = origResolve;
}
return ext;
}
function activateExt(vscodeMock) {
const ext = loadExtension(vscodeMock);
const ctx = { subscriptions: vscodeMock._subscriptions };
ext.activate(ctx);
return { ext, vscode: vscodeMock };
}
function makeChangeEvent(filePath, changes, { scheme = 'file', isActive = true } = {}) {
return {
document: { uri: { scheme, fsPath: filePath } },
contentChanges: changes.map(({ text, startLine = 0 }) => ({
text,
range: { start: { line: startLine } },
})),
};
}
describe('Extension: Copilot detection threshold (MIN_COPILOT_CHANGE_LEN = 50)', () => {
test('does not capture a short single-line insertion (<50 chars)', (t, done) => {
const vscode = makeVscodeMock();
activateExt(vscode);
const shortText = 'x'.repeat(49);
vscode._fire.change(makeChangeEvent('/tmp/repo/src/main.rs', [{ text: shortText }]));
setTimeout(() => done(), 50);
});
test('handles multi-line insertion (>1 newline) regardless of length', (t, done) => {
const vscode = makeVscodeMock();
activateExt(vscode);
vscode._fire.change(makeChangeEvent('/tmp/repo/src/main.rs', [{ text: 'a\nb' }]));
setTimeout(() => done(), 50);
});
test('ignores changes from non-file URI schemes', (t, done) => {
const vscode = makeVscodeMock();
activateExt(vscode);
vscode._fire.change(makeChangeEvent('/tmp/repo/src/main.rs', [{ text: 'x'.repeat(100) }], { scheme: 'git' }));
setTimeout(() => done(), 50);
});
});
describe('Extension: EXCLUDED_PATHS filtering', () => {
test('ignores changes to .agentdiff/ paths', (t, done) => {
const vscode = makeVscodeMock();
activateExt(vscode);
const event = {
document: { uri: { scheme: 'file', fsPath: '/tmp/repo/.agentdiff/ledger.jsonl' } },
contentChanges: [{ text: 'x'.repeat(200), range: { start: { line: 0 } } }],
};
const vscodeReal = require.__spy || vscode;
vscode.workspace.asRelativePath = (_p) => '.agentdiff/ledger.jsonl';
vscode._fire.change(event);
setTimeout(() => done(), 50);
});
test('ignores changes to .git/ paths', (t, done) => {
const vscode = makeVscodeMock();
activateExt(vscode);
const event = {
document: { uri: { scheme: 'file', fsPath: '/tmp/repo/.git/COMMIT_EDITMSG' } },
contentChanges: [{ text: 'x'.repeat(200), range: { start: { line: 0 } } }],
};
vscode.workspace.asRelativePath = (_p) => '.git/COMMIT_EDITMSG';
vscode._fire.change(event);
setTimeout(() => done(), 50);
});
});
describe('Extension: Copilot not installed', () => {
test('activate() returns early without registering listeners when Copilot is absent', () => {
const vscode = makeVscodeMock({ copilotInstalled: false });
const { ext } = activateExt(vscode);
assert.equal(vscode._subscriptions.length, 0,
'Should not register any listeners when Copilot extension is absent');
});
});
describe('Extension: getCopilotModel() fallback', () => {
test('returns "gpt-4o" when copilot-chat extension is present', () => {
const vscode = makeVscodeMock({ copilotInstalled: true, copilotActive: true });
vscode.extensions.getExtension = (id) => {
if (id === 'GitHub.copilot') return { isActive: true };
if (id === 'GitHub.copilot-chat') return { isActive: true };
return undefined;
};
assert.doesNotThrow(() => activateExt(vscode));
});
test('returns "copilot" fallback when no config and no chat ext', () => {
const vscode = makeVscodeMock();
vscode.extensions.getExtension = (id) => {
if (id === 'GitHub.copilot') return { isActive: true };
return undefined;
};
assert.doesNotThrow(() => activateExt(vscode));
});
});
describe('Extension: agentdiff.captureNow command', () => {
test('command is registered after activation', () => {
const vscode = makeVscodeMock();
activateExt(vscode);
assert.ok('agentdiff.captureNow' in vscode._commands,
'agentdiff.captureNow command should be registered');
});
test('captureNow shows message when no active editor', async () => {
const vscode = makeVscodeMock();
activateExt(vscode);
let shown = null;
vscode.window.showInformationMessage = (msg) => { shown = msg; };
vscode.window.activeTextEditor = null;
await vscode._commands['agentdiff.captureNow']();
assert.ok(shown && shown.includes('agentdiff'), `Expected info message, got: ${shown}`);
});
});
describe('Extension: deactivate', () => {
test('deactivate() does not throw', () => {
const vscode = makeVscodeMock();
const { ext } = activateExt(vscode);
assert.doesNotThrow(() => ext.deactivate());
});
});