Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/protocol/sessionDiffsRealSdk.integrationTest.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
/**
7
* Real-SDK integration test for the git-driven session diff path.
8
*
9
* Disabled by default. Run with:
10
*
11
* AGENT_HOST_REAL_SDK=1 ./scripts/test-integration.sh \
12
* --run src/vs/platform/agentHost/test/node/protocol/sessionDiffsRealSdk.integrationTest.ts
13
*
14
* Authentication: token from `gh auth token` (or `GITHUB_TOKEN`).
15
*
16
* SAFETY: Working directory is always a freshly-`git init`-ed temp folder
17
* scoped to a single test, removed in teardown.
18
*/
19
20
import assert from 'assert';
21
import * as cp from 'child_process';
22
import { execSync } from 'child_process';
23
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'fs';
24
import { tmpdir } from 'os';
25
import { join } from '../../../../../base/common/path.js';
26
import { URI } from '../../../../../base/common/uri.js';
27
import { SubscribeResult } from '../../../common/state/protocol/commands.js';
28
import { PROTOCOL_VERSION } from '../../../common/state/sessionCapabilities.js';
29
import type { INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js';
30
import type { SessionState } from '../../../common/state/sessionState.js';
31
import type { SessionAddedNotification, SessionDiffsChangedAction, SessionToolCallReadyAction } from '../../../common/state/sessionActions.js';
32
import {
33
getActionEnvelope,
34
IServerHandle,
35
isActionNotification,
36
startRealServer,
37
TestProtocolClient,
38
} from './testHelpers.js';
39
40
const REAL_SDK_ENABLED = process.env['AGENT_HOST_REAL_SDK'] === '1';
41
42
const hasGit = (() => {
43
try { cp.execFileSync('git', ['--version'], { stdio: 'ignore' }); return true; } catch { return false; }
44
})();
45
46
function resolveGitHubToken(): string {
47
const envToken = process.env['GITHUB_TOKEN'];
48
if (envToken) {
49
return envToken;
50
}
51
return execSync('gh auth token', { encoding: 'utf-8' }).trim();
52
}
53
54
(REAL_SDK_ENABLED && hasGit ? suite : suite.skip)('Protocol WebSocket — Real Copilot SDK git-driven diffs', function () {
55
56
let server: IServerHandle;
57
let client: TestProtocolClient;
58
const createdSessions: string[] = [];
59
const tempDirs: string[] = [];
60
61
suiteSetup(async function () {
62
this.timeout(60_000);
63
server = await startRealServer();
64
});
65
66
suiteTeardown(function () {
67
server?.process.kill();
68
});
69
70
setup(async function () {
71
this.timeout(30_000);
72
client = new TestProtocolClient(server.port);
73
await client.connect();
74
});
75
76
teardown(async function () {
77
for (const session of createdSessions) {
78
try { await client.call('disposeSession', { session }, 5000); } catch { /* best-effort */ }
79
}
80
createdSessions.length = 0;
81
client.close();
82
for (const dir of tempDirs) {
83
rmSync(dir, { recursive: true, force: true });
84
}
85
tempDirs.length = 0;
86
});
87
88
test('terminal-driven file edit shows up in summary.diffs (no ToolResultFileEditContent emitted)', async function () {
89
this.timeout(180_000);
90
91
// Initialize a tmp git repo as the working directory.
92
const tempDir = mkdtempSync(`${tmpdir()}/ahp-real-diff-`);
93
tempDirs.push(tempDir);
94
const env = { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' };
95
const runGit = (...args: string[]) => execSync(`git ${args.join(' ')}`, { cwd: tempDir, env, stdio: 'pipe' });
96
runGit('init', '-q', '-b', 'main');
97
writeFileSync(join(tempDir, 'seed.txt'), 'seed\n');
98
runGit('add', '.');
99
runGit('commit', '-q', '-m', 'init');
100
101
const workingDirUri = URI.file(tempDir).toString();
102
103
await client.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'real-sdk-git-diffs' }, 30_000);
104
await client.call('authenticate', { resource: 'https://api.github.com', token: resolveGitHubToken() }, 30_000);
105
106
const sessionUri = URI.from({ scheme: 'copilotcli', path: `/real-diff-${Date.now()}` }).toString();
107
await client.call('createSession', { session: sessionUri, provider: 'copilotcli', workingDirectory: workingDirUri }, 30_000);
108
109
const addedNotif = await client.waitForNotification(n =>
110
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded',
111
15_000,
112
);
113
const realSessionUri = ((addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource;
114
createdSessions.push(realSessionUri);
115
116
await client.call<SubscribeResult>('subscribe', { resource: realSessionUri });
117
client.clearReceived();
118
119
// Approve any tool call the agent issues. Restricted to `bash`-style
120
// shell tools so the model can't trick the test into running arbitrary
121
// other tools.
122
let approvalSeq = 1;
123
const approve = (action: SessionToolCallReadyAction & { session: string; turnId: string }) => {
124
client.notify('dispatchAction', {
125
clientSeq: ++approvalSeq,
126
action: {
127
type: 'session/toolCallConfirmed',
128
session: action.session,
129
turnId: action.turnId,
130
toolCallId: action.toolCallId,
131
approved: true,
132
},
133
});
134
};
135
const seenSeqs = new Set<number>();
136
let approverActive = true;
137
const approverLoop = (async () => {
138
while (approverActive) {
139
try {
140
const ready = await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady') && !seenSeqs.has(getActionEnvelope(n).serverSeq), 2_000);
141
const env = getActionEnvelope(ready);
142
seenSeqs.add(env.serverSeq);
143
approve(env.action as SessionToolCallReadyAction & { session: string; turnId: string });
144
} catch { /* timeout — keep polling */ }
145
}
146
})();
147
148
// Ask the agent to use bash to write a specific file. The exact filename
149
// is fixed so we can assert on it. The model is instructed to use bash
150
// (not a write_file tool) so the edit isn't reported via the SDK's
151
// file-edit content events — the diff has to come from git.
152
const targetFile = join(tempDir, 'from-bash.txt');
153
// Quote/escape targetFile for the shell so paths containing spaces or
154
// shell metacharacters don't break the test.
155
const shellQuotedTargetFile = `'${targetFile.replace(/'/g, `'\\''`)}'`;
156
const prompt = `Use the bash shell tool to run exactly: echo hello > ${shellQuotedTargetFile}\nDo not use any file-write tool. Use only bash.`;
157
client.notify('dispatchAction', {
158
clientSeq: 1,
159
action: { type: 'session/turnStarted', session: realSessionUri, turnId: 'turn-diff', userMessage: { text: prompt } },
160
});
161
162
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'), 150_000);
163
approverActive = false;
164
await approverLoop;
165
166
// Sanity: file was actually written by the agent.
167
const files = readdirSync(tempDir);
168
assert.ok(files.includes('from-bash.txt'), `agent did not write the requested file. dir contents: ${files.join(', ')}`);
169
170
// The diff broadcast may have already arrived during the turn — accept
171
// any matching one received during the run, or look at the final state.
172
const targetUri = URI.file(targetFile).toString();
173
const diffNotifs = client.receivedNotifications(n => isActionNotification(n, 'session/diffsChanged'));
174
const sawInLive = diffNotifs.some(n => {
175
const a = getActionEnvelope(n).action as SessionDiffsChangedAction;
176
return a.diffs.some(d => d.after?.uri === targetUri || d.before?.uri === targetUri);
177
});
178
179
if (!sawInLive) {
180
// Fall back to the final snapshot.
181
const result = await client.call<SubscribeResult>('subscribe', { resource: realSessionUri });
182
const state = result.snapshot.state as SessionState;
183
const diffs = state.summary.diffs ?? [];
184
const matching = diffs.find(d => d.after?.uri === targetUri || d.before?.uri === targetUri);
185
assert.ok(matching, `expected git-driven diff for ${targetUri}; live notifications=${diffNotifs.length}; snapshot diffs=${JSON.stringify(diffs.map(d => d.after?.uri ?? d.before?.uri))}`);
186
}
187
});
188
});
189
190