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}
|
${additionalBuiltinExtensions}
|
||||||
workspaceProvider: {
|
workspaceProvider: {
|
||||||
workspace: ${useMock
|
workspace: ${useMock
|
||||||
? `{ folderUri: URI.parse('mock-fs://mock-repo/') }`
|
? `{ folderUri: URI.parse('mock-fs://mock-repo/mock-repo') }`
|
||||||
: 'undefined'},
|
: 'undefined'},
|
||||||
open: async () => false,
|
open: async () => false,
|
||||||
payload: [['isSessionsWindow', 'true']],
|
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: {
|
export const EditorOptions: {
|
||||||
acceptSuggestionOnCommitCharacter: IEditorOption<EditorOption.acceptSuggestionOnCommitCharacter, boolean>;
|
acceptSuggestionOnCommitCharacter: IEditorOption<EditorOption.acceptSuggestionOnCommitCharacter, boolean>;
|
||||||
acceptSuggestionOnEnter: IEditorOption<EditorOption.acceptSuggestionOnEnter, 'on' | 'off' | 'smart'>;
|
acceptSuggestionOnEnter: IEditorOption<EditorOption.acceptSuggestionOnEnter, 'on' | 'off' | 'smart'>;
|
||||||
accessibilitySupport: IEditorOption<EditorOption.accessibilitySupport, any>;
|
accessibilitySupport: IEditorOption<EditorOption.accessibilitySupport, AccessibilitySupport>;
|
||||||
accessibilityPageSize: IEditorOption<EditorOption.accessibilityPageSize, number>;
|
accessibilityPageSize: IEditorOption<EditorOption.accessibilityPageSize, number>;
|
||||||
allowOverflow: IEditorOption<EditorOption.allowOverflow, boolean>;
|
allowOverflow: IEditorOption<EditorOption.allowOverflow, boolean>;
|
||||||
allowVariableLineHeights: IEditorOption<EditorOption.allowVariableLineHeights, boolean>;
|
allowVariableLineHeights: IEditorOption<EditorOption.allowVariableLineHeights, boolean>;
|
||||||
@@ -5326,7 +5326,7 @@ declare namespace monaco.editor {
|
|||||||
foldingMaximumRegions: IEditorOption<EditorOption.foldingMaximumRegions, number>;
|
foldingMaximumRegions: IEditorOption<EditorOption.foldingMaximumRegions, number>;
|
||||||
unfoldOnClickAfterEndOfLine: IEditorOption<EditorOption.unfoldOnClickAfterEndOfLine, boolean>;
|
unfoldOnClickAfterEndOfLine: IEditorOption<EditorOption.unfoldOnClickAfterEndOfLine, boolean>;
|
||||||
fontFamily: IEditorOption<EditorOption.fontFamily, string>;
|
fontFamily: IEditorOption<EditorOption.fontFamily, string>;
|
||||||
fontInfo: IEditorOption<EditorOption.fontInfo, any>;
|
fontInfo: IEditorOption<EditorOption.fontInfo, FontInfo>;
|
||||||
fontLigatures2: IEditorOption<EditorOption.fontLigatures, string>;
|
fontLigatures2: IEditorOption<EditorOption.fontLigatures, string>;
|
||||||
fontSize: IEditorOption<EditorOption.fontSize, number>;
|
fontSize: IEditorOption<EditorOption.fontSize, number>;
|
||||||
fontWeight: IEditorOption<EditorOption.fontWeight, string>;
|
fontWeight: IEditorOption<EditorOption.fontWeight, string>;
|
||||||
@@ -5366,7 +5366,7 @@ declare namespace monaco.editor {
|
|||||||
pasteAs: IEditorOption<EditorOption.pasteAs, Readonly<Required<IPasteAsOptions>>>;
|
pasteAs: IEditorOption<EditorOption.pasteAs, Readonly<Required<IPasteAsOptions>>>;
|
||||||
parameterHints: IEditorOption<EditorOption.parameterHints, Readonly<Required<IEditorParameterHintOptions>>>;
|
parameterHints: IEditorOption<EditorOption.parameterHints, Readonly<Required<IEditorParameterHintOptions>>>;
|
||||||
peekWidgetDefaultFocus: IEditorOption<EditorOption.peekWidgetDefaultFocus, 'tree' | 'editor'>;
|
peekWidgetDefaultFocus: IEditorOption<EditorOption.peekWidgetDefaultFocus, 'tree' | 'editor'>;
|
||||||
placeholder: IEditorOption<EditorOption.placeholder, string | undefined>;
|
placeholder: IEditorOption<EditorOption.placeholder, string>;
|
||||||
definitionLinkOpensInPeek: IEditorOption<EditorOption.definitionLinkOpensInPeek, boolean>;
|
definitionLinkOpensInPeek: IEditorOption<EditorOption.definitionLinkOpensInPeek, boolean>;
|
||||||
quickSuggestions: IEditorOption<EditorOption.quickSuggestions, InternalQuickSuggestionsOptions>;
|
quickSuggestions: IEditorOption<EditorOption.quickSuggestions, InternalQuickSuggestionsOptions>;
|
||||||
quickSuggestionsDelay: IEditorOption<EditorOption.quickSuggestionsDelay, number>;
|
quickSuggestionsDelay: IEditorOption<EditorOption.quickSuggestionsDelay, number>;
|
||||||
|
|||||||
@@ -157,7 +157,9 @@ import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contri
|
|||||||
import './contrib/chat/browser/chat.contribution.js';
|
import './contrib/chat/browser/chat.contribution.js';
|
||||||
import './contrib/sessions/browser/sessions.contribution.js';
|
import './contrib/sessions/browser/sessions.contribution.js';
|
||||||
import './contrib/sessions/browser/customizationsToolbar.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/fileTreeView/browser/fileTreeView.contribution.js';
|
||||||
import './contrib/configuration/browser/configuration.contribution.js';
|
import './contrib/configuration/browser/configuration.contribution.js';
|
||||||
import './contrib/welcome/browser/welcome.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
|
**compile-and-replay** architecture powered by
|
||||||
[`playwright-cli`](https://github.com/microsoft/playwright-cli) and Copilot CLI.
|
[`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
|
## How It Works
|
||||||
|
|
||||||
There are two phases:
|
There are two phases:
|
||||||
@@ -59,19 +145,26 @@ e2e/
|
|||||||
├── generate.cjs # Compiles scenarios → .commands.json via Copilot CLI
|
├── generate.cjs # Compiles scenarios → .commands.json via Copilot CLI
|
||||||
├── test.cjs # Replays .commands.json deterministically
|
├── test.cjs # Replays .commands.json deterministically
|
||||||
├── package.json # npm scripts: generate, test
|
├── package.json # npm scripts: generate, test
|
||||||
|
├── extensions/
|
||||||
|
│ └── sessions-e2e-mock/ # Mock extension (auth + mock-fs:// file system)
|
||||||
├── scenarios/
|
├── scenarios/
|
||||||
│ ├── 01-repo-picker-on-submit.scenario.md
|
│ ├── 01-chat-response.scenario.md
|
||||||
│ ├── 02-cloud-disables-add-run-action.scenario.md
|
│ ├── 02-chat-with-changes.scenario.md
|
||||||
│ └── generated/
|
│ └── generated/
|
||||||
│ ├── 01-repo-picker-on-submit.commands.json
|
│ ├── 01-chat-response.commands.json
|
||||||
│ └── 02-cloud-disables-add-run-action.commands.json
|
│ └── 02-chat-with-changes.commands.json
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
└── README.md
|
└── 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/
|
scripts/
|
||||||
├── code-sessions-web.js # HTTP server that serves Sessions as a web app
|
├── code-sessions-web.js # HTTP server that serves Sessions as a web app
|
||||||
└── code-sessions-web.sh # Shell wrapper
|
└── 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.
|
- **Order matters** — scenarios run sequentially; an Escape is pressed between them.
|
||||||
- **Prefix filenames** with numbers (`01-`, `02-`, …) to control execution order.
|
- **Prefix filenames** with numbers (`01-`, `02-`, …) to control execution order.
|
||||||
- **Re-generate selectively**: `npm run generate -- 01-repo` to recompile one scenario.
|
- **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.
|
* Mock extension for Sessions E2E testing.
|
||||||
*
|
*
|
||||||
* Provides fake implementations of:
|
* Provides a fake GitHub authentication provider (skips sign-in).
|
||||||
* - GitHub authentication (skips sign-in)
|
*
|
||||||
* - Chat participant (returns canned responses)
|
* The mock-fs:// FileSystemProvider and chat agents are registered
|
||||||
* - File system provider for github-remote-file:// (in-memory files)
|
* 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
|
// Activation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -117,11 +31,10 @@ function activate(context) {
|
|||||||
// 1. Mock GitHub Authentication Provider
|
// 1. Mock GitHub Authentication Provider
|
||||||
context.subscriptions.push(registerMockAuth(vscode));
|
context.subscriptions.push(registerMockAuth(vscode));
|
||||||
|
|
||||||
// 2. Mock File System Provider
|
// Note: The mock-fs:// FileSystemProvider is registered directly in the
|
||||||
context.subscriptions.push(registerMockFileSystem(vscode));
|
// workbench (web.test.ts → registerMockFileSystemProvider) so it is
|
||||||
|
// available before any service tries to resolve workspace files.
|
||||||
// Note: Chat participant is registered via workbench contribution
|
// Do NOT register it here — it would cause a duplicate provider error.
|
||||||
// in web.test.ts (MockChatAgentContribution), not here.
|
|
||||||
|
|
||||||
console.log('[sessions-e2e-mock] All mocks registered');
|
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
|
// 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
|
## Steps
|
||||||
1. Type "build the project" in the chat input
|
1. Type "build the project" in the chat input
|
||||||
2. Press Enter to submit
|
2. Press Enter to submit
|
||||||
3. Verify there is a response in the chat
|
3. Verify there is a response in the chat
|
||||||
4. Verify a change count appears in the session list
|
4. Toggle the secondary side bar
|
||||||
5. Toggle the secondary side bar
|
5. Verify the changes view shows modified files
|
||||||
6. Make sure the changes list shows the file diffs
|
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
|
1. Type "fix the bug" in the chat input
|
||||||
2. Press Enter to submit
|
2. Press Enter to submit
|
||||||
3. Verify the session appears in the sessions list
|
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",
|
"scenario": "Scenario: Chat message produces real file diffs in the changes view",
|
||||||
"generatedAt": "2026-03-06T04:24:58.648Z",
|
"generatedAt": "2026-03-10T00:00:00.000Z",
|
||||||
|
"note": "Uses semantic selectors — regenerate with 'npm run generate' if UI labels change",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"description": "Type \"build the project\" in the chat input",
|
"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:"
|
"# 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",
|
"description": "Toggle the secondary side bar",
|
||||||
"commands": [
|
"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": [
|
"commands": [
|
||||||
"# ASSERT_VISIBLE: Changes - 3 files changed",
|
"# ASSERT_VISIBLE: index.ts",
|
||||||
"click treeitem \"build.ts\"",
|
"# ASSERT_VISIBLE: build.ts",
|
||||||
"# ASSERT_VISIBLE: +10",
|
"# ASSERT_VISIBLE: package.json"
|
||||||
"click treeitem \"config.ts\"",
|
]
|
||||||
"# ASSERT_VISIBLE: +5",
|
},
|
||||||
"click treeitem \"package.json\"",
|
{
|
||||||
"# ASSERT_VISIBLE: +8"
|
"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",
|
"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": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"description": "Type \"fix the bug\" in the chat input",
|
"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": [
|
"commands": [
|
||||||
"# ASSERT_VISIBLE: +7",
|
"click button \"Toggle Secondary Side Bar\""
|
||||||
"# ASSERT_VISIBLE: -0"
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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 { IChatProgress } from '../../workbench/contrib/chat/common/chatService/chatService.js';
|
||||||
import { IChatSessionsService, IChatSessionItem, IChatSessionFileChange, ChatSessionStatus, IChatSessionHistoryItem } from '../../workbench/contrib/chat/common/chatSessionsService.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 { 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 = {
|
const MOCK_ACCOUNT: IDefaultAccount = {
|
||||||
authenticationProvider: { id: 'github', name: 'GitHub (Mock)', enterprise: false },
|
authenticationProvider: { id: 'github', name: 'GitHub (Mock)', enterprise: false },
|
||||||
@@ -93,26 +127,71 @@ class MockDefaultAccountService implements IDefaultAccountService {
|
|||||||
// Mock chat responses and file changes
|
// Mock chat responses and file changes
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
interface MockResponse {
|
/**
|
||||||
text: string;
|
* Paths that exist in the mock-fs file store pre-seeded by the mock extension.
|
||||||
fileEdits?: { uri: URI; content: string }[];
|
* 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 {
|
function getMockResponseWithEdits(message: string): MockResponse {
|
||||||
if (/build|compile|create/i.test(message)) {
|
if (/build|compile|create/i.test(message)) {
|
||||||
return {
|
return {
|
||||||
text: 'I\'ll help you build the project. Here are the changes:',
|
text: 'I\'ll help you build the project. Here are the changes:',
|
||||||
fileEdits: [
|
fileEdits: [
|
||||||
{
|
{
|
||||||
uri: URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/src/build.ts' }),
|
// Modify existing file — adds build import + call
|
||||||
content: 'import { main } from "./index";\n\nasync function build() {\n\tconsole.log("Building...");\n\tmain();\n\tconsole.log("Build complete!");\n}\n\nbuild();\n',
|
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' }),
|
// New file — creates build script
|
||||||
content: 'export const config = {\n\toutput: "./dist",\n\tminify: true,\n};\n',
|
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',
|
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.',
|
text: 'I found the issue and applied the fix. The input validation has been added.',
|
||||||
fileEdits: [
|
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',
|
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();
|
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 key = resource.toString();
|
||||||
const now = Date.now();
|
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)) {
|
if (!this._sessionHistory.has(key)) {
|
||||||
this._sessionHistory.set(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' },
|
{ 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 => ({
|
const changes: IChatSessionFileChange[] | undefined = fileEdits?.map(edit => ({
|
||||||
modifiedUri: edit.uri,
|
modifiedUri: edit.uri,
|
||||||
insertions: edit.content.split('\n').length,
|
insertions: edit.content.split('\n').length,
|
||||||
deletions: 0,
|
deletions: EXISTING_MOCK_FILES.has(edit.uri.path) ? 1 : 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add or update session in list
|
// Add or update session in list
|
||||||
@@ -204,7 +292,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerMockAgents(): void {
|
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 extensionId = new ExtensionIdentifier('vscode.sessions-e2e-mock');
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
@@ -237,19 +325,10 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
|||||||
content: { value: response.text, isTrusted: false, supportThemeIcons: false, supportHtml: false },
|
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) {
|
if (response.fileEdits) {
|
||||||
for (const edit of response.fileEdits) {
|
emitFileEdits(response.fileEdits, progress);
|
||||||
progress([{
|
|
||||||
kind: 'textEdit',
|
|
||||||
uri: edit.uri,
|
|
||||||
edits: [{
|
|
||||||
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
|
|
||||||
text: edit.content,
|
|
||||||
}],
|
|
||||||
done: true,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
console.log(`[Sessions Web Test] Emitted ${response.fileEdits.length} file edits`);
|
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 },
|
content: { value: response.text, isTrusted: false, supportThemeIcons: false, supportHtml: false },
|
||||||
}]);
|
}]);
|
||||||
if (response.fileEdits) {
|
if (response.fileEdits) {
|
||||||
for (const edit of response.fileEdits) {
|
emitFileEdits(response.fileEdits, progress);
|
||||||
progress([{
|
|
||||||
kind: 'textEdit',
|
|
||||||
uri: edit.uri,
|
|
||||||
edits: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, text: edit.content }],
|
|
||||||
done: true,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isComplete.set(true, undefined);
|
isComplete.set(true, undefined);
|
||||||
},
|
},
|
||||||
@@ -324,7 +396,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu
|
|||||||
}
|
}
|
||||||
|
|
||||||
private preseedFolder(): void {
|
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);
|
this.storageService.store('agentSessions.lastPickedFolder', mockFolderUri, StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||||
console.log(`[Sessions Web Test] Pre-seeded folder: ${mockFolderUri}`);
|
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 {
|
protected override createWorkbench(domElement: HTMLElement, serviceCollection: ServiceCollection, logService: ILogService): IBrowserMainWorkbench {
|
||||||
console.log('[Sessions Web Test] Injecting mock services');
|
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
|
// Override entitlement service so Sessions thinks user is signed in
|
||||||
serviceCollection.set(IChatEntitlementService, new MockChatEntitlementService());
|
serviceCollection.set(IChatEntitlementService, new MockChatEntitlementService());
|
||||||
|
|
||||||
|
|||||||
@@ -321,6 +321,9 @@ function extensionDescriptionArrayToMap(extensions: IExtensionDescription[]): Ex
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean {
|
export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean {
|
||||||
|
if (1 < 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!extension.enabledApiProposals) {
|
if (!extension.enabledApiProposals) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user