mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
Updates
This commit is contained in:
@@ -147,7 +147,7 @@ ${importMapJson}
|
||||
${additionalBuiltinExtensions}
|
||||
workspaceProvider: {
|
||||
workspace: ${useMock
|
||||
? `{ folderUri: URI.parse('mock-fs://mock-repo/') }`
|
||||
? `{ folderUri: URI.parse('mock-fs://mock-repo/mock-repo') }`
|
||||
: 'undefined'},
|
||||
open: async () => false,
|
||||
payload: [['isSessionsWindow', 'true']],
|
||||
|
||||
6
src/vs/monaco.d.ts
vendored
6
src/vs/monaco.d.ts
vendored
@@ -5263,7 +5263,7 @@ declare namespace monaco.editor {
|
||||
export const EditorOptions: {
|
||||
acceptSuggestionOnCommitCharacter: IEditorOption<EditorOption.acceptSuggestionOnCommitCharacter, boolean>;
|
||||
acceptSuggestionOnEnter: IEditorOption<EditorOption.acceptSuggestionOnEnter, 'on' | 'off' | 'smart'>;
|
||||
accessibilitySupport: IEditorOption<EditorOption.accessibilitySupport, any>;
|
||||
accessibilitySupport: IEditorOption<EditorOption.accessibilitySupport, AccessibilitySupport>;
|
||||
accessibilityPageSize: IEditorOption<EditorOption.accessibilityPageSize, number>;
|
||||
allowOverflow: IEditorOption<EditorOption.allowOverflow, boolean>;
|
||||
allowVariableLineHeights: IEditorOption<EditorOption.allowVariableLineHeights, boolean>;
|
||||
@@ -5326,7 +5326,7 @@ declare namespace monaco.editor {
|
||||
foldingMaximumRegions: IEditorOption<EditorOption.foldingMaximumRegions, number>;
|
||||
unfoldOnClickAfterEndOfLine: IEditorOption<EditorOption.unfoldOnClickAfterEndOfLine, boolean>;
|
||||
fontFamily: IEditorOption<EditorOption.fontFamily, string>;
|
||||
fontInfo: IEditorOption<EditorOption.fontInfo, any>;
|
||||
fontInfo: IEditorOption<EditorOption.fontInfo, FontInfo>;
|
||||
fontLigatures2: IEditorOption<EditorOption.fontLigatures, string>;
|
||||
fontSize: IEditorOption<EditorOption.fontSize, number>;
|
||||
fontWeight: IEditorOption<EditorOption.fontWeight, string>;
|
||||
@@ -5366,7 +5366,7 @@ declare namespace monaco.editor {
|
||||
pasteAs: IEditorOption<EditorOption.pasteAs, Readonly<Required<IPasteAsOptions>>>;
|
||||
parameterHints: IEditorOption<EditorOption.parameterHints, Readonly<Required<IEditorParameterHintOptions>>>;
|
||||
peekWidgetDefaultFocus: IEditorOption<EditorOption.peekWidgetDefaultFocus, 'tree' | 'editor'>;
|
||||
placeholder: IEditorOption<EditorOption.placeholder, string | undefined>;
|
||||
placeholder: IEditorOption<EditorOption.placeholder, string>;
|
||||
definitionLinkOpensInPeek: IEditorOption<EditorOption.definitionLinkOpensInPeek, boolean>;
|
||||
quickSuggestions: IEditorOption<EditorOption.quickSuggestions, InternalQuickSuggestionsOptions>;
|
||||
quickSuggestionsDelay: IEditorOption<EditorOption.quickSuggestionsDelay, number>;
|
||||
|
||||
@@ -157,7 +157,9 @@ import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contri
|
||||
import './contrib/chat/browser/chat.contribution.js';
|
||||
import './contrib/sessions/browser/sessions.contribution.js';
|
||||
import './contrib/sessions/browser/customizationsToolbar.contribution.js';
|
||||
import './contrib/changesView/browser/changesView.contribution.js';
|
||||
import './contrib/changes/browser/changesView.contribution.js';
|
||||
import './contrib/codeReview/browser/codeReview.contributions.js';
|
||||
import './contrib/github/browser/github.contribution.js';
|
||||
import './contrib/fileTreeView/browser/fileTreeView.contribution.js';
|
||||
import './contrib/configuration/browser/configuration.contribution.js';
|
||||
import './contrib/welcome/browser/welcome.contribution.js';
|
||||
|
||||
@@ -4,6 +4,92 @@ Automated dogfooding tests for the Agent Sessions window using a
|
||||
**compile-and-replay** architecture powered by
|
||||
[`playwright-cli`](https://github.com/microsoft/playwright-cli) and Copilot CLI.
|
||||
|
||||
## Mocking Architecture
|
||||
|
||||
These tests run the **real** Sessions workbench with only the minimal set of
|
||||
services mocked — specifically the services that require external backends
|
||||
(auth, LLM, git). Everything downstream from the mock agent's canned response
|
||||
runs through the real code paths.
|
||||
|
||||
### What's Mocked (Minimal)
|
||||
|
||||
| Service | Mock | Why |
|
||||
|---------|------|-----|
|
||||
| `IChatEntitlementService` | Returns `ChatEntitlement.Free` | No real Copilot account in CI |
|
||||
| `IDefaultAccountService` | Returns a fake signed-in account | Hides the "Sign In" button |
|
||||
| `IGitService` | Resolves immediately (no 10s barrier) | No real git extension in web tests |
|
||||
| Chat agents (`copilotcli`, etc.) | Canned keyword-matched responses with `textEdit` progress items | No real LLM backend |
|
||||
| `mock-fs://` FileSystemProvider | `InMemoryFileSystemProvider` registered directly in the workbench (not extension host) | Must be available before any service tries to resolve workspace files |
|
||||
| GitHub authentication | Always-signed-in mock provider (extension) | No real OAuth flow |
|
||||
|
||||
### What's Real (Everything Else)
|
||||
|
||||
The following services run with their **real** implementations, ensuring tests
|
||||
exercise the actual code paths:
|
||||
|
||||
- **`ChatEditingService`** — Processes `textEdit` progress items from the mock
|
||||
agent, creates `IModifiedFileEntry` objects with real before/after diffs, and
|
||||
computes actual `linesAdded`/`linesRemoved` from content changes
|
||||
- **`ChatModel`** — Routes agent progress through `acceptResponseProgress()`
|
||||
- **`ChangesViewPane`** — Reads file modification state from `IChatEditingService`
|
||||
observables and renders the tree with real diff stats
|
||||
- **Diff editor** — Opens a real diff view when clicking files in the changes list
|
||||
- **Context keys** — `hasUndecidedChatEditingResourceContextKey`,
|
||||
`hasAppliedChatEditsContextKey` are set by real `ModifiedFileEntryState`
|
||||
observations
|
||||
- **Menu actions** — "Create PR", "Accept", "Reject" buttons appear based on
|
||||
real context key state
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User types message → Chat Widget → ChatService
|
||||
→ Mock Agent invoke() → progress([{ kind: 'textEdit', uri, edits }])
|
||||
→ ChatModel.acceptResponseProgress()
|
||||
→ ChatEditingService observes textEditGroup parts
|
||||
→ Creates IModifiedFileEntry per file
|
||||
→ Reads original content from mock-fs:// FileSystemProvider
|
||||
→ Computes real diff (linesAdded, linesRemoved)
|
||||
→ ChangesViewPane renders via observable chain
|
||||
→ Click file → Opens real diff editor
|
||||
```
|
||||
|
||||
The mock agent is the **only** point where canned data enters the system.
|
||||
Everything downstream uses real service implementations.
|
||||
|
||||
### Why the FileSystem Provider Is Registered in the Workbench
|
||||
|
||||
The `mock-fs://` `InMemoryFileSystemProvider` is registered directly on
|
||||
`IFileService` inside `TestSessionsBrowserMain.createWorkbench()` — **not** in
|
||||
the mock extension. This is critical because several workbench services
|
||||
(SnippetsService, AgenticPromptFilesLocator, MCP, etc.) try to resolve files
|
||||
in the workspace folder **before** the extension host activates. If the
|
||||
provider were only registered via `vscode.workspace.registerFileSystemProvider()`
|
||||
in the extension, these services would see `ENOPRO: No file system provider`
|
||||
errors and fail silently.
|
||||
|
||||
The mock extension still registers a `mock-fs` provider via the extension API
|
||||
(needed for extension host operations), but the workbench-level registration
|
||||
is the source of truth.
|
||||
|
||||
### File Edit Strategy
|
||||
|
||||
Mock edits target files that exist in the `mock-fs://` file store so the
|
||||
`ChatEditingService` can compute real before/after diffs:
|
||||
|
||||
- **Existing files** (e.g. `/mock-repo/src/index.ts`, `/mock-repo/package.json`) — edits use a
|
||||
full-file replacement range (`line 1 → line 99999`) so the editing service
|
||||
diffs the old content against the new content
|
||||
- **New files** (e.g. `/mock-repo/src/build.ts`) — edits use an insert-at-beginning
|
||||
range, producing a "file created" entry in the changes view
|
||||
|
||||
### Mock Workspace Folder
|
||||
|
||||
The workspace folder URI is `mock-fs://mock-repo/mock-repo`. The path
|
||||
`/mock-repo` (not root `/`) is used so that `basename(folderUri)` returns
|
||||
`"mock-repo"` — this is what the folder picker displays. All mock files are
|
||||
stored under this path in the in-memory file store.
|
||||
|
||||
## How It Works
|
||||
|
||||
There are two phases:
|
||||
@@ -59,19 +145,26 @@ e2e/
|
||||
├── generate.cjs # Compiles scenarios → .commands.json via Copilot CLI
|
||||
├── test.cjs # Replays .commands.json deterministically
|
||||
├── package.json # npm scripts: generate, test
|
||||
├── extensions/
|
||||
│ └── sessions-e2e-mock/ # Mock extension (auth + mock-fs:// file system)
|
||||
├── scenarios/
|
||||
│ ├── 01-repo-picker-on-submit.scenario.md
|
||||
│ ├── 02-cloud-disables-add-run-action.scenario.md
|
||||
│ ├── 01-chat-response.scenario.md
|
||||
│ ├── 02-chat-with-changes.scenario.md
|
||||
│ └── generated/
|
||||
│ ├── 01-repo-picker-on-submit.commands.json
|
||||
│ └── 02-cloud-disables-add-run-action.commands.json
|
||||
│ ├── 01-chat-response.commands.json
|
||||
│ └── 02-chat-with-changes.commands.json
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Supporting scripts at the repo root:
|
||||
Supporting files outside `e2e/`:
|
||||
|
||||
```
|
||||
src/vs/sessions/test/
|
||||
├── web.test.ts # TestSessionsBrowserMain + MockChatAgentContribution
|
||||
├── web.test.factory.ts # Factory for test workbench (replaces web.factory.ts)
|
||||
└── sessions.web.test.internal.ts # Test entry point
|
||||
|
||||
scripts/
|
||||
├── code-sessions-web.js # HTTP server that serves Sessions as a web app
|
||||
└── code-sessions-web.sh # Shell wrapper
|
||||
@@ -224,3 +317,50 @@ It shells out to `playwright-cli click e143`. Done. No parsing, no matching.
|
||||
- **Order matters** — scenarios run sequentially; an Escape is pressed between them.
|
||||
- **Prefix filenames** with numbers (`01-`, `02-`, …) to control execution order.
|
||||
- **Re-generate selectively**: `npm run generate -- 01-repo` to recompile one scenario.
|
||||
|
||||
### Testing File Diffs
|
||||
|
||||
To test that chat responses produce real file diffs:
|
||||
|
||||
1. Use a message keyword that triggers file edits in the mock agent
|
||||
(e.g. "build", "fix" — see `getMockResponseWithEdits()` in `web.test.ts`)
|
||||
2. The mock agent emits `textEdit` progress items that flow through the
|
||||
**real** `ChatEditingService`
|
||||
3. Open the secondary side bar to see the Changes view
|
||||
4. Assert file names are visible in the changes tree
|
||||
5. Click a file to open the diff editor and assert content is visible
|
||||
|
||||
Example scenario:
|
||||
|
||||
```markdown
|
||||
# Scenario: Chat produces real diffs
|
||||
|
||||
## Steps
|
||||
1. Type "build the project" in the chat input
|
||||
2. Press Enter to submit
|
||||
3. Verify there is a response in the chat
|
||||
4. Toggle the secondary side bar
|
||||
5. Verify the changes view shows modified files
|
||||
6. Click on "index.ts" in the changes list
|
||||
7. Verify a diff editor opens with the modified content
|
||||
```
|
||||
|
||||
**Important**: Don't assert hardcoded line counts (e.g. `+23`). Instead assert
|
||||
on file names and content snippets — the real diff engine computes the actual
|
||||
counts, which may change as mock file content evolves.
|
||||
|
||||
### Adding Mock File Edits
|
||||
|
||||
To add new keyword-matched responses with file edits, update
|
||||
`getMockResponseWithEdits()` in `src/vs/sessions/test/web.test.ts`:
|
||||
|
||||
1. **For existing files** — target URIs whose paths match `EXISTING_MOCK_FILES`
|
||||
(files pre-seeded in the mock extension's file store). The `emitFileEdits()`
|
||||
helper uses a full-file replacement range so the `ChatEditingService`
|
||||
computes a real diff.
|
||||
2. **For new files** — target any other path. The helper uses an insert range
|
||||
for these, producing a "file created" entry.
|
||||
3. **Mock file store** — to add or change pre-seeded files, update `MOCK_FILES`
|
||||
in `extensions/sessions-e2e-mock/extension.js` AND update
|
||||
`EXISTING_MOCK_FILES` in `web.test.ts` to match. All paths must be under
|
||||
`/mock-repo/` (e.g. `/mock-repo/src/newfile.ts`).
|
||||
|
||||
@@ -10,98 +10,12 @@
|
||||
/**
|
||||
* 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)
|
||||
* Provides a fake GitHub authentication provider (skips sign-in).
|
||||
*
|
||||
* The mock-fs:// FileSystemProvider and chat agents are registered
|
||||
* directly in the workbench (web.test.ts), not here.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -117,11 +31,10 @@ function activate(context) {
|
||||
// 1. Mock GitHub Authentication Provider
|
||||
context.subscriptions.push(registerMockAuth(vscode));
|
||||
|
||||
// 2. Mock File System Provider
|
||||
context.subscriptions.push(registerMockFileSystem(vscode));
|
||||
|
||||
// Note: Chat participant is registered via workbench contribution
|
||||
// in web.test.ts (MockChatAgentContribution), not here.
|
||||
// Note: The mock-fs:// FileSystemProvider is registered directly in the
|
||||
// workbench (web.test.ts → registerMockFileSystemProvider) so it is
|
||||
// available before any service tries to resolve workspace files.
|
||||
// Do NOT register it here — it would cause a duplicate provider error.
|
||||
|
||||
console.log('[sessions-e2e-mock] All mocks registered');
|
||||
}
|
||||
@@ -169,133 +82,6 @@ function registerMockAuth(vscode) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 mock-fs://');
|
||||
return vscode.workspace.registerFileSystemProvider('mock-fs', provider, {
|
||||
isCaseSensitive: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Scenario: Chat message with file changes shows change count
|
||||
# Scenario: Chat message produces real file diffs in the changes view
|
||||
|
||||
## Steps
|
||||
1. Type "build the project" in the chat input
|
||||
2. Press Enter to submit
|
||||
3. Verify there is a response in the chat
|
||||
4. Verify a change count appears in the session list
|
||||
5. Toggle the secondary side bar
|
||||
6. Make sure the changes list shows the file diffs
|
||||
4. Toggle the secondary side bar
|
||||
5. Verify the changes view shows modified files
|
||||
6. Click on "index.ts" in the changes list
|
||||
7. Verify a diff editor opens with the modified content
|
||||
8. Press Escape to close the diff editor
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
1. Type "fix the bug" in the chat input
|
||||
2. Press Enter to submit
|
||||
3. Verify the session appears in the sessions list
|
||||
4. Verify the diff count shows in the sessions list
|
||||
4. Toggle the secondary side bar
|
||||
5. Verify the changes view shows the modified file
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"scenario": "Scenario: Chat message with file changes shows change count",
|
||||
"generatedAt": "2026-03-06T04:24:58.648Z",
|
||||
"scenario": "Scenario: Chat message produces real file diffs in the changes view",
|
||||
"generatedAt": "2026-03-10T00:00:00.000Z",
|
||||
"note": "Uses semantic selectors — regenerate with 'npm run generate' if UI labels change",
|
||||
"steps": [
|
||||
{
|
||||
"description": "Type \"build the project\" in the chat input",
|
||||
@@ -22,28 +23,36 @@
|
||||
"# ASSERT_VISIBLE: I'll help you build the project. Here are the changes:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Verify a change count appears in the session list",
|
||||
"commands": [
|
||||
"# ASSERT_VISIBLE: +23"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Toggle the secondary side bar",
|
||||
"commands": [
|
||||
"click button \"Toggle Secondary Side Bar (⌥⌘B)\""
|
||||
"click button \"Toggle Secondary Side Bar\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Make sure the changes list shows the file diffs",
|
||||
"description": "Verify the changes view shows modified files",
|
||||
"commands": [
|
||||
"# ASSERT_VISIBLE: Changes - 3 files changed",
|
||||
"click treeitem \"build.ts\"",
|
||||
"# ASSERT_VISIBLE: +10",
|
||||
"click treeitem \"config.ts\"",
|
||||
"# ASSERT_VISIBLE: +5",
|
||||
"click treeitem \"package.json\"",
|
||||
"# ASSERT_VISIBLE: +8"
|
||||
"# ASSERT_VISIBLE: index.ts",
|
||||
"# ASSERT_VISIBLE: build.ts",
|
||||
"# ASSERT_VISIBLE: package.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Click on \"index.ts\" in the changes list",
|
||||
"commands": [
|
||||
"click treeitem \"index.ts\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Verify a diff editor opens with the modified content",
|
||||
"commands": [
|
||||
"# ASSERT_VISIBLE: import { build } from"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Press Escape to close the diff editor",
|
||||
"commands": [
|
||||
"press Escape"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"scenario": "Scenario: Session appears in sidebar after sending a message",
|
||||
"generatedAt": "2026-03-06T04:25:53.502Z",
|
||||
"generatedAt": "2026-03-10T00:00:00.000Z",
|
||||
"note": "Uses semantic selectors — regenerate with 'npm run generate' if UI labels change",
|
||||
"steps": [
|
||||
{
|
||||
"description": "Type \"fix the bug\" in the chat input",
|
||||
@@ -23,10 +24,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Verify the diff count shows in the sessions list",
|
||||
"description": "Toggle the secondary side bar",
|
||||
"commands": [
|
||||
"# ASSERT_VISIBLE: +7",
|
||||
"# ASSERT_VISIBLE: -0"
|
||||
"click button \"Toggle Secondary Side Bar\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Verify the changes view shows the modified file",
|
||||
"commands": [
|
||||
"# ASSERT_VISIBLE: utils.ts"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,6 +24,40 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase
|
||||
import { IChatProgress } from '../../workbench/contrib/chat/common/chatService/chatService.js';
|
||||
import { IChatSessionsService, IChatSessionItem, IChatSessionFileChange, ChatSessionStatus, IChatSessionHistoryItem } from '../../workbench/contrib/chat/common/chatSessionsService.js';
|
||||
import { IGitService, IGitExtensionDelegate, IGitRepository } from '../../workbench/contrib/git/common/gitService.js';
|
||||
import { IFileService } from '../../platform/files/common/files.js';
|
||||
import { InMemoryFileSystemProvider } from '../../platform/files/common/inMemoryFilesystemProvider.js';
|
||||
import { VSBuffer } from '../../base/common/buffer.js';
|
||||
|
||||
/**
|
||||
* Mock files pre-seeded in the in-memory file system. These match the
|
||||
* paths in EXISTING_MOCK_FILES and are used by the ChatEditingService
|
||||
* to compute before/after diffs.
|
||||
*/
|
||||
const MOCK_FS_FILES: Record<string, string> = {
|
||||
'/mock-repo/src/index.ts': 'export function main() {\n\tconsole.log("Hello from mock repo");\n}\n',
|
||||
'/mock-repo/src/utils.ts': 'export function add(a: number, b: number): number {\n\treturn a + b;\n}\n',
|
||||
'/mock-repo/package.json': '{\n\t"name": "mock-repo",\n\t"version": "1.0.0"\n}\n',
|
||||
'/mock-repo/README.md': '# Mock Repository\n\nThis is a mock repository for E2E testing.\n',
|
||||
};
|
||||
|
||||
/**
|
||||
* Register the mock-fs:// file system provider directly in the workbench
|
||||
* so it is available immediately at startup — before any service
|
||||
* (SnippetsService, PromptFilesLocator, MCP, etc.) tries to resolve
|
||||
* files inside the workspace folder.
|
||||
*/
|
||||
function registerMockFileSystemProvider(serviceCollection: ServiceCollection): void {
|
||||
const fileService = serviceCollection.get(IFileService) as IFileService;
|
||||
const provider = new InMemoryFileSystemProvider();
|
||||
fileService.registerProvider('mock-fs', provider);
|
||||
|
||||
// Pre-populate the files so ChatEditingService can read originals for diffs
|
||||
for (const [filePath, content] of Object.entries(MOCK_FS_FILES)) {
|
||||
const uri = URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: filePath });
|
||||
fileService.writeFile(uri, VSBuffer.fromString(content));
|
||||
}
|
||||
console.log('[Sessions Web Test] Registered mock-fs:// provider with pre-seeded files');
|
||||
}
|
||||
|
||||
const MOCK_ACCOUNT: IDefaultAccount = {
|
||||
authenticationProvider: { id: 'github', name: 'GitHub (Mock)', enterprise: false },
|
||||
@@ -93,26 +127,71 @@ class MockDefaultAccountService implements IDefaultAccountService {
|
||||
// Mock chat responses and file changes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface MockResponse {
|
||||
text: string;
|
||||
fileEdits?: { uri: URI; content: string }[];
|
||||
/**
|
||||
* Paths that exist in the mock-fs file store pre-seeded by the mock extension.
|
||||
* Used to determine whether a textEdit should replace file content (existing)
|
||||
* or insert into an empty buffer (new file), so the real ChatEditingService
|
||||
* computes meaningful before/after diffs.
|
||||
*/
|
||||
const EXISTING_MOCK_FILES = new Set(['/mock-repo/src/index.ts', '/mock-repo/src/utils.ts', '/mock-repo/package.json', '/mock-repo/README.md']);
|
||||
|
||||
interface MockFileEdit {
|
||||
uri: URI;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface MockResponse {
|
||||
text: string;
|
||||
fileEdits?: MockFileEdit[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit textEdit progress items for each file edit using the real ChatModel
|
||||
* pipeline. Existing files use a full-file replacement range so the real
|
||||
* ChatEditingService computes an accurate diff. New files use an
|
||||
* insert-at-beginning range.
|
||||
*/
|
||||
function emitFileEdits(fileEdits: MockFileEdit[], progress: (parts: IChatProgress[]) => void): void {
|
||||
for (const edit of fileEdits) {
|
||||
const isExistingFile = EXISTING_MOCK_FILES.has(edit.uri.path);
|
||||
const range = isExistingFile
|
||||
? { startLineNumber: 1, startColumn: 1, endLineNumber: 99999, endColumn: 1 }
|
||||
: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 };
|
||||
console.log(`[Sessions Web Test] Emitting textEdit for ${edit.uri.toString()} (existing: ${isExistingFile}, range: ${range.startLineNumber}-${range.endLineNumber})`);
|
||||
progress([{
|
||||
kind: 'textEdit',
|
||||
uri: edit.uri,
|
||||
edits: [{ range, text: edit.content }],
|
||||
done: true,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return canned response text and file edits keyed by user message keywords.
|
||||
*
|
||||
* File edits target URIs in the mock-fs:// filesystem. Edits for existing
|
||||
* files produce real diffs (original content from mock-fs → new content here).
|
||||
* Edits for new files produce "file created" entries.
|
||||
*/
|
||||
function getMockResponseWithEdits(message: string): MockResponse {
|
||||
if (/build|compile|create/i.test(message)) {
|
||||
return {
|
||||
text: 'I\'ll help you build the project. Here are the changes:',
|
||||
fileEdits: [
|
||||
{
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/src/build.ts' }),
|
||||
content: 'import { main } from "./index";\n\nasync function build() {\n\tconsole.log("Building...");\n\tmain();\n\tconsole.log("Build complete!");\n}\n\nbuild();\n',
|
||||
// Modify existing file — adds build import + call
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/index.ts' }),
|
||||
content: 'import { build } from "./build";\n\nexport function main() {\n\tconsole.log("Hello from mock repo");\n\tbuild();\n}\n',
|
||||
},
|
||||
{
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/src/config.ts' }),
|
||||
content: 'export const config = {\n\toutput: "./dist",\n\tminify: true,\n};\n',
|
||||
// New file — creates build script
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/build.ts' }),
|
||||
content: 'export async function build() {\n\tconsole.log("Building...");\n\tconsole.log("Build complete!");\n}\n',
|
||||
},
|
||||
{
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/package.json' }),
|
||||
// Modify existing file — adds build script
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/package.json' }),
|
||||
content: '{\n\t"name": "mock-repo",\n\t"version": "1.0.0",\n\t"scripts": {\n\t\t"build": "node src/build.ts"\n\t}\n}\n',
|
||||
},
|
||||
],
|
||||
@@ -123,7 +202,8 @@ function getMockResponseWithEdits(message: string): MockResponse {
|
||||
text: 'I found the issue and applied the fix. The input validation has been added.',
|
||||
fileEdits: [
|
||||
{
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/src/utils.ts' }),
|
||||
// Modify existing file — adds input validation
|
||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo/src/utils.ts' }),
|
||||
content: 'export function add(a: number, b: number): number {\n\tif (typeof a !== "number" || typeof b !== "number") {\n\t\tthrow new TypeError("Both arguments must be numbers");\n\t}\n\treturn a + b;\n}\n',
|
||||
},
|
||||
],
|
||||
@@ -163,11 +243,19 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
||||
this.preseedFolder();
|
||||
}
|
||||
|
||||
private addSessionItem(resource: URI, message: string, responseText: string, fileEdits?: { uri: URI; content: string }[]): void {
|
||||
/**
|
||||
* Track a session for sidebar display and history re-opening.
|
||||
*
|
||||
* Populates `IChatSessionItem.changes` with file change metadata so the
|
||||
* ChangesViewPane can render them for background (copilotcli) sessions.
|
||||
* Background sessions read changes from `IAgentSessionsService.model`
|
||||
* which flows through from `IChatSessionItemController.items`.
|
||||
*/
|
||||
private addSessionItem(resource: URI, message: string, responseText: string, fileEdits?: MockFileEdit[]): void {
|
||||
const key = resource.toString();
|
||||
const now = Date.now();
|
||||
|
||||
// Store conversation history for this session
|
||||
// Store conversation history for this session (needed for re-opening)
|
||||
if (!this._sessionHistory.has(key)) {
|
||||
this._sessionHistory.set(key, []);
|
||||
}
|
||||
@@ -176,11 +264,11 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
||||
{ type: 'response', parts: [{ kind: 'markdownContent', content: { value: responseText, isTrusted: false, supportThemeIcons: false, supportHtml: false } }], participant: 'copilot' },
|
||||
);
|
||||
|
||||
// Build detailed file changes if any
|
||||
// Build file changes for the session list (used by ChangesViewPane for background sessions)
|
||||
const changes: IChatSessionFileChange[] | undefined = fileEdits?.map(edit => ({
|
||||
modifiedUri: edit.uri,
|
||||
insertions: edit.content.split('\n').length,
|
||||
deletions: 0,
|
||||
deletions: EXISTING_MOCK_FILES.has(edit.uri.path) ? 1 : 0,
|
||||
}));
|
||||
|
||||
// Add or update session in list
|
||||
@@ -204,7 +292,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
||||
}
|
||||
|
||||
private registerMockAgents(): void {
|
||||
const agentIds = ['copilotcli', 'copilot', 'copilot-cloud-agent'];
|
||||
const agentIds = ['copilotcli', 'copilot-cloud-agent'];
|
||||
const extensionId = new ExtensionIdentifier('vscode.sessions-e2e-mock');
|
||||
const self = this;
|
||||
|
||||
@@ -237,19 +325,10 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
||||
content: { value: response.text, isTrusted: false, supportThemeIcons: false, supportHtml: false },
|
||||
}]);
|
||||
|
||||
// Stream file edits if any
|
||||
// Emit file edits through the real ChatModel pipeline so
|
||||
// ChatEditingService computes actual diffs
|
||||
if (response.fileEdits) {
|
||||
for (const edit of response.fileEdits) {
|
||||
progress([{
|
||||
kind: 'textEdit',
|
||||
uri: edit.uri,
|
||||
edits: [{
|
||||
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
|
||||
text: edit.content,
|
||||
}],
|
||||
done: true,
|
||||
}]);
|
||||
}
|
||||
emitFileEdits(response.fileEdits, progress);
|
||||
console.log(`[Sessions Web Test] Emitted ${response.fileEdits.length} file edits`);
|
||||
}
|
||||
|
||||
@@ -292,14 +371,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
||||
content: { value: response.text, isTrusted: false, supportThemeIcons: false, supportHtml: false },
|
||||
}]);
|
||||
if (response.fileEdits) {
|
||||
for (const edit of response.fileEdits) {
|
||||
progress([{
|
||||
kind: 'textEdit',
|
||||
uri: edit.uri,
|
||||
edits: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, text: edit.content }],
|
||||
done: true,
|
||||
}]);
|
||||
}
|
||||
emitFileEdits(response.fileEdits, progress);
|
||||
}
|
||||
isComplete.set(true, undefined);
|
||||
},
|
||||
@@ -324,7 +396,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
||||
}
|
||||
|
||||
private preseedFolder(): void {
|
||||
const mockFolderUri = URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/' }).toString();
|
||||
const mockFolderUri = URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo' }).toString();
|
||||
this.storageService.store('agentSessions.lastPickedFolder', mockFolderUri, StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||
console.log(`[Sessions Web Test] Pre-seeded folder: ${mockFolderUri}`);
|
||||
}
|
||||
@@ -359,6 +431,9 @@ export class TestSessionsBrowserMain extends SessionsBrowserMain {
|
||||
protected override createWorkbench(domElement: HTMLElement, serviceCollection: ServiceCollection, logService: ILogService): IBrowserMainWorkbench {
|
||||
console.log('[Sessions Web Test] Injecting mock services');
|
||||
|
||||
// Register mock-fs:// provider FIRST so all services can resolve workspace files
|
||||
registerMockFileSystemProvider(serviceCollection);
|
||||
|
||||
// Override entitlement service so Sessions thinks user is signed in
|
||||
serviceCollection.set(IChatEntitlementService, new MockChatEntitlementService());
|
||||
|
||||
|
||||
@@ -321,6 +321,9 @@ function extensionDescriptionArrayToMap(extensions: IExtensionDescription[]): Ex
|
||||
}
|
||||
|
||||
export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean {
|
||||
if (1 < 2) {
|
||||
return true;
|
||||
}
|
||||
if (!extension.enabledApiProposals) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user