Add mock web extension for E2E testing

New extension at extensions/sessions-e2e-mock/ provides:
- Mock GitHub auth provider (fake token, skips sign-in)
- Mock chat participant (canned responses based on input keywords)
- Mock file system for github-remote-file:// (in-memory files)

Server loads the extension when --mock flag is passed. The generate
and test runners both use --mock automatically.

New npm scripts:
- serve:mock — opens Sessions in browser with mocks loaded

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Osvaldo Ortega
2026-03-05 11:34:34 -08:00
parent c677691c87
commit fafc62f5ac
7 changed files with 349 additions and 8 deletions

View File

@@ -15,7 +15,7 @@ const APP_ROOT = path.join(__dirname, '..');
async function main() {
const args = minimist(process.argv.slice(2), {
boolean: ['help', 'no-open', 'skip-welcome'],
boolean: ['help', 'no-open', 'skip-welcome', 'mock'],
string: ['host', 'port'],
});
@@ -24,7 +24,9 @@ async function main() {
'./scripts/code-sessions-web.sh [options]\n' +
' --host <host> Host to bind to (default: localhost)\n' +
' --port <port> Port to bind to (default: 8081)\n' +
' --no-open Do not open browser automatically\n'
' --no-open Do not open browser automatically\n' +
' --skip-welcome Skip the sessions welcome overlay\n' +
' --mock Load mock extension for E2E testing\n'
);
return;
}
@@ -50,7 +52,7 @@ async function main() {
// Serve the sessions workbench HTML at the root
if (url.pathname === '/' || url.pathname === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(getSessionsHTML(HOST, PORT, cssModules));
res.end(getSessionsHTML(HOST, PORT, cssModules, args['mock']));
return;
}
@@ -95,7 +97,7 @@ async function main() {
process.on('SIGTERM', () => { server.close(); process.exit(0); });
}
function getSessionsHTML(host, port, cssModules) {
function getSessionsHTML(host, port, cssModules, useMock) {
const baseUrl = `http://${host}:${port}`;
const fileRoot = `${baseUrl}/out`;
@@ -112,6 +114,11 @@ function getSessionsHTML(host, port, cssModules) {
}
const importMapJson = JSON.stringify({ imports }, null, 2);
// When --mock is passed, load the E2E mock extension
const additionalBuiltinExtensions = useMock
? `additionalBuiltinExtensions: [{ scheme: 'http', authority: '${host}:${port}', path: '/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock' }],`
: '';
return `<!DOCTYPE html>
<html>
<head>
@@ -137,6 +144,7 @@ ${importMapJson}
nameLong: 'Sessions (Web)',
enableTelemetry: false,
},
${additionalBuiltinExtensions}
workspaceProvider: {
workspace: undefined,
open: async () => false,

View File

@@ -90,10 +90,12 @@ function getSnapshot() {
// Server management
// ---------------------------------------------------------------------------
function startServer(port) {
function startServer(port, { mock = false } = {}) {
const args = ['--no-open', '--port', String(port)];
if (mock) { args.push('--mock'); }
const server = cp.spawn(process.execPath, [
path.join(APP_ROOT, 'scripts', 'code-sessions-web.js'),
'--no-open', '--port', String(port),
...args,
], {
cwd: APP_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
/// <reference types="vscode" />
/**
* Mock extension for Sessions E2E testing.
*
* Provides fake implementations of:
* - GitHub authentication (skips sign-in)
* - Chat participant (returns canned responses)
* - File system provider for github-remote-file:// (in-memory files)
*/
// ---------------------------------------------------------------------------
// Mock data
// ---------------------------------------------------------------------------
/** @type {Map<string, Uint8Array>} */
const fileStore = new Map();
// Pre-populate a fake repo
const MOCK_FILES = {
'/src/index.ts': 'export function main() {\n\tconsole.log("Hello from mock repo");\n}\n',
'/src/utils.ts': 'export function add(a: number, b: number): number {\n\treturn a + b;\n}\n',
'/package.json': '{\n\t"name": "mock-repo",\n\t"version": "1.0.0"\n}\n',
'/README.md': '# Mock Repository\n\nThis is a mock repository for E2E testing.\n',
};
for (const [path, content] of Object.entries(MOCK_FILES)) {
fileStore.set(path, new TextEncoder().encode(content));
}
// Canned chat responses keyed by keywords in the user message
const CHAT_RESPONSES = [
{
match: /build|compile/i,
response: [
'I\'ll help you build the project. Here are the changes:',
'',
'```typescript',
'// src/build.ts',
'import { main } from "./index";',
'',
'async function build() {',
'\tconsole.log("Building...");',
'\tmain();',
'\tconsole.log("Build complete!");',
'}',
'',
'build();',
'```',
'',
'I\'ve created a new build script that imports and runs the main function.',
].join('\n'),
},
{
match: /fix|bug/i,
response: [
'I found the issue. Here\'s the fix:',
'',
'```diff',
'- export function add(a: number, b: number): number {',
'+ export function add(a: number, b: number): number {',
'+ if (typeof a !== "number" || typeof b !== "number") {',
'+ throw new TypeError("Both arguments must be numbers");',
'+ }',
' return a + b;',
' }',
'```',
'',
'Added input validation to prevent NaN results.',
].join('\n'),
},
{
match: /explain/i,
response: [
'This project has a simple structure:',
'',
'- **src/index.ts** — Main entry point with a `main()` function',
'- **src/utils.ts** — Utility functions like `add()`',
'- **package.json** — Project metadata',
'',
'The `main()` function logs a greeting to the console.',
].join('\n'),
},
];
const DEFAULT_RESPONSE = [
'I understand your request. Let me work on that.',
'',
'Here\'s what I\'d suggest:',
'',
'1. Review the current codebase structure',
'2. Make the necessary changes',
'3. Run the tests to verify',
'',
'Would you like me to proceed?',
].join('\n');
// ---------------------------------------------------------------------------
// Activation
// ---------------------------------------------------------------------------
/**
* @param {import('vscode').ExtensionContext} context
*/
function activate(context) {
const vscode = require('vscode');
console.log('[sessions-e2e-mock] Activating mock extension');
// 1. Mock GitHub Authentication Provider
context.subscriptions.push(registerMockAuth(vscode));
// 2. Mock Chat Participant
context.subscriptions.push(registerMockChat(vscode));
// 3. Mock File System Provider
context.subscriptions.push(registerMockFileSystem(vscode));
console.log('[sessions-e2e-mock] All mocks registered');
}
// ---------------------------------------------------------------------------
// Mock Authentication Provider
// ---------------------------------------------------------------------------
/**
* @param {typeof import('vscode')} vscode
* @returns {import('vscode').Disposable}
*/
function registerMockAuth(vscode) {
const sessionChangeEmitter = new vscode.EventEmitter();
/** @type {import('vscode').AuthenticationSession} */
const mockSession = {
id: 'mock-session-1',
accessToken: 'gho_mock_e2e_test_token_00000000000000000000',
account: {
id: 'e2e-test-user',
label: 'E2E Test User',
},
scopes: ['read:user', 'repo', 'workflow'],
};
/** @type {import('vscode').AuthenticationProvider} */
const provider = {
onDidChangeSessions: sessionChangeEmitter.event,
async getSessions(_scopes, _options) {
return [mockSession];
},
async createSession(_scopes, _options) {
sessionChangeEmitter.fire({ added: [mockSession], removed: [], changed: [] });
return mockSession;
},
async removeSession(_sessionId) {
sessionChangeEmitter.fire({ added: [], removed: [mockSession], changed: [] });
},
};
console.log('[sessions-e2e-mock] Registering mock GitHub auth provider');
return vscode.authentication.registerAuthenticationProvider('github', 'GitHub (Mock)', provider, {
supportsMultipleAccounts: false,
});
}
// ---------------------------------------------------------------------------
// Mock Chat Participant
// ---------------------------------------------------------------------------
/**
* @param {typeof import('vscode')} vscode
* @returns {import('vscode').Disposable}
*/
function registerMockChat(vscode) {
const participant = vscode.chat.createChatParticipant('copilot', async (request, _context, response, _token) => {
const userMessage = request.prompt || '';
console.log(`[sessions-e2e-mock] Chat request: "${userMessage}"`);
// Find matching canned response
let responseText = DEFAULT_RESPONSE;
for (const entry of CHAT_RESPONSES) {
if (entry.match.test(userMessage)) {
responseText = entry.response;
break;
}
}
// Stream the response with a small delay to simulate typing
const lines = responseText.split('\n');
for (const line of lines) {
response.markdown(line + '\n');
}
return { metadata: { mock: true } };
});
participant.iconPath = new vscode.ThemeIcon('copilot');
console.log('[sessions-e2e-mock] Registered mock chat participant "copilot"');
return participant;
}
// ---------------------------------------------------------------------------
// Mock File System Provider (github-remote-file scheme)
// ---------------------------------------------------------------------------
/**
* @param {typeof import('vscode')} vscode
* @returns {import('vscode').Disposable}
*/
function registerMockFileSystem(vscode) {
const fileChangeEmitter = new vscode.EventEmitter();
/** @type {import('vscode').FileSystemProvider} */
const provider = {
onDidChangeFile: fileChangeEmitter.event,
stat(uri) {
const filePath = uri.path;
if (fileStore.has(filePath)) {
return {
type: vscode.FileType.File,
ctime: 0,
mtime: Date.now(),
size: fileStore.get(filePath).byteLength,
};
}
// Check if it's a directory (any file starts with this path)
const dirPrefix = filePath.endsWith('/') ? filePath : filePath + '/';
const isDir = filePath === '/' || [...fileStore.keys()].some(k => k.startsWith(dirPrefix));
if (isDir) {
return { type: vscode.FileType.Directory, ctime: 0, mtime: Date.now(), size: 0 };
}
throw vscode.FileSystemError.FileNotFound(uri);
},
readDirectory(uri) {
const dirPath = uri.path.endsWith('/') ? uri.path : uri.path + '/';
const entries = new Map();
for (const filePath of fileStore.keys()) {
if (!filePath.startsWith(dirPath)) { continue; }
const relative = filePath.slice(dirPath.length);
const parts = relative.split('/');
if (parts.length === 1) {
entries.set(parts[0], vscode.FileType.File);
} else {
entries.set(parts[0], vscode.FileType.Directory);
}
}
return [...entries.entries()];
},
readFile(uri) {
const content = fileStore.get(uri.path);
if (!content) { throw vscode.FileSystemError.FileNotFound(uri); }
return content;
},
writeFile(uri, content, _options) {
fileStore.set(uri.path, content);
fileChangeEmitter.fire([{ type: vscode.FileChangeType.Changed, uri }]);
},
createDirectory(_uri) { /* no-op for in-memory */ },
delete(uri, _options) {
fileStore.delete(uri.path);
fileChangeEmitter.fire([{ type: vscode.FileChangeType.Deleted, uri }]);
},
rename(oldUri, newUri, _options) {
const content = fileStore.get(oldUri.path);
if (!content) { throw vscode.FileSystemError.FileNotFound(oldUri); }
fileStore.delete(oldUri.path);
fileStore.set(newUri.path, content);
fileChangeEmitter.fire([
{ type: vscode.FileChangeType.Deleted, uri: oldUri },
{ type: vscode.FileChangeType.Created, uri: newUri },
]);
},
watch(_uri, _options) {
return { dispose() { } };
},
};
console.log('[sessions-e2e-mock] Registering mock file system for github-remote-file://');
return vscode.workspace.registerFileSystemProvider('github-remote-file', provider, {
isCaseSensitive: true,
});
}
// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------
module.exports = { activate };

View File

@@ -0,0 +1,27 @@
{
"name": "sessions-e2e-mock",
"displayName": "Sessions E2E Mock",
"description": "Mock auth, chat participant, and file system for E2E testing",
"version": "0.0.1",
"publisher": "vscode",
"engines": {
"vscode": "*"
},
"extensionKind": ["ui", "workspace"],
"browser": "./extension.js",
"activationEvents": ["*"],
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"contributes": {
"authentication": [
{
"id": "github",
"label": "GitHub (Mock)"
}
]
}
}

View File

@@ -155,7 +155,7 @@ async function main() {
// Start web server
console.log(`Starting sessions web server on port ${PORT}`);
const server = startServer(PORT);
const server = startServer(PORT, { mock: true });
await waitForServer(`http://localhost:${PORT}/`, 30_000);
console.log('Server ready.');

View File

@@ -5,6 +5,7 @@
"description": "Automated E2E tests for the Agent Sessions window using playwright-cli",
"scripts": {
"serve": "node ../../../../../scripts/code-sessions-web.js --port 9222 --skip-welcome",
"serve:mock": "node ../../../../../scripts/code-sessions-web.js --port 9222 --skip-welcome --mock",
"generate": "node generate.cjs",
"test": "node test.cjs"
},

View File

@@ -129,7 +129,7 @@ async function main() {
// Start web server
console.log(`Starting sessions web server on port ${PORT}`);
const server = startServer(PORT);
const server = startServer(PORT, { mock: true });
await waitForServer(`http://localhost:${PORT}/`, 30_000);
console.log('Server ready.\n');