mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 05:28:19 -05:00
AgentHost - adopt the ChangesetContentChanged action (#323170)
* AgentHost - switch over to ChangesetContentChanged action * Revert the change to the tests * Fix integration test
This commit is contained in:
@@ -115,6 +115,13 @@ export interface IAgentHostChangesetOperationService extends IDisposable {
|
||||
* be used.
|
||||
*/
|
||||
updateOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): void;
|
||||
|
||||
/**
|
||||
* Returns the operations that should be advertised for the given changeset, or
|
||||
* `undefined` when no operations are available.
|
||||
*/
|
||||
getOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): readonly ChangesetOperation[] | undefined;
|
||||
|
||||
/**
|
||||
* Invokes an advertised operation after validating the changeset, operation id,
|
||||
* and requested target scope.
|
||||
|
||||
@@ -63,6 +63,7 @@ export {
|
||||
type ChangesetStatusChangedAction,
|
||||
type ChangesetFileSetAction,
|
||||
type ChangesetFileRemovedAction,
|
||||
type ChangesetContentChangedAction,
|
||||
type ChangesetOperationsChangedAction,
|
||||
type ChangesetClearedAction,
|
||||
type AnnotationsSetAction,
|
||||
|
||||
@@ -119,9 +119,6 @@ export class AgentHostChangesetCoordinator extends Disposable {
|
||||
// subscribed for the session (the service reads the exposed
|
||||
// subscription list).
|
||||
this._changesets.recomputeSubscribedChangesets(sessionStr);
|
||||
|
||||
// Update the operations for all subscribed changesets
|
||||
this._changesetOperationService.updateOperations(sessionStr, undefined, gitState);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,21 +180,18 @@ export class AgentHostChangesetCoordinator extends Disposable {
|
||||
if (parsed?.kind === ChangesetKind.Branch) {
|
||||
this._addSubscription(parsed.sessionUri, resourceStr);
|
||||
this._changesets.refreshBranchChangeset(parsed.sessionUri);
|
||||
this._changesetOperationService.updateOperations(parsed.sessionUri, resourceStr);
|
||||
this._changesetFileMonitor.trackSessionChanges(resourceStr, parsed.sessionUri);
|
||||
return;
|
||||
}
|
||||
if (parsed?.kind === ChangesetKind.Uncommitted) {
|
||||
this._addSubscription(parsed.sessionUri, resourceStr);
|
||||
void this._changesets.computeUncommittedChangeset(parsed.sessionUri);
|
||||
this._changesetOperationService.updateOperations(parsed.sessionUri, resourceStr);
|
||||
this._changesetFileMonitor.trackSessionChanges(resourceStr, parsed.sessionUri);
|
||||
return;
|
||||
}
|
||||
if (parsed?.kind === ChangesetKind.Session) {
|
||||
this._addSubscription(parsed.sessionUri, resourceStr);
|
||||
this._changesets.refreshSessionChangeset(parsed.sessionUri);
|
||||
this._changesetOperationService.updateOperations(parsed.sessionUri, resourceStr);
|
||||
this._changesetFileMonitor.trackSessionChanges(resourceStr, parsed.sessionUri);
|
||||
return;
|
||||
}
|
||||
@@ -208,7 +202,6 @@ export class AgentHostChangesetCoordinator extends Disposable {
|
||||
// subsequent deltas flow from `onToolCallEditsApplied` /
|
||||
// `onTurnComplete` once we've added this turn id here.
|
||||
this._addSubscription(parsed.sessionUri, resourceStr);
|
||||
this._changesetOperationService.updateOperations(parsed.sessionUri, resourceStr);
|
||||
return;
|
||||
}
|
||||
if (!parsed && this._stateManager.getSessionState(resourceStr)) {
|
||||
@@ -312,7 +305,6 @@ export class AgentHostChangesetCoordinator extends Disposable {
|
||||
await this.restoreSessionIfChangesetSubscription(resource, restoreSession);
|
||||
if (parsed.kind === ChangesetKind.Turn && parsed.turnId) {
|
||||
await this._changesets.computeTurnChangeset(parsed.sessionUri, parsed.turnId);
|
||||
this._changesetOperationService.updateOperations(parsed.sessionUri, resourceStr);
|
||||
} else if (parsed.kind === ChangesetKind.Compare && parsed.originalTurnId && parsed.modifiedTurnId) {
|
||||
// Compare-turns is computed once on subscribe. Both turns are
|
||||
// typically historical so the snapshot doesn't need to track
|
||||
|
||||
@@ -15,11 +15,6 @@ import type { IChangesetOperationContribution, IAgentHostChangesetOperationServi
|
||||
import { AgentHostStateManager } from './agentHostStateManager.js';
|
||||
import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js';
|
||||
import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js';
|
||||
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
|
||||
import { AgentHostPullRequestOperationContribution } from './agentHostPullRequestOperationProvider.js';
|
||||
import { AgentHostCommitOperationContribution } from './agentHostCommitOperationProvider.js';
|
||||
import { AgentHostDiscardChangesOperationContribution } from './agentHostDiscardChangesOperationProvider.js';
|
||||
import { AgentHostSyncOperationContribution } from './agentHostSyncOperationProvider.js';
|
||||
|
||||
export class AgentHostChangesetOperationService extends Disposable implements IAgentHostChangesetOperationService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
@@ -33,19 +28,14 @@ export class AgentHostChangesetOperationService extends Disposable implements IA
|
||||
private readonly _stateManager: AgentHostStateManager,
|
||||
@IAgentHostGitStateService private readonly _gitStateService: IAgentHostGitStateService,
|
||||
@IAgentHostChangesetSubscriptionService private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService,
|
||||
@IInstantiationService instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registry = {
|
||||
registerChangesetOperationHandler: (operationId, handler) => this._registerChangesetOperationHandler(operationId, handler),
|
||||
onDidChangeOperations: sessionKey => this.updateOperations(sessionKey),
|
||||
refreshSessionGitState: sessionKey => this._refreshSessionGitStateAndOperations(sessionKey),
|
||||
};
|
||||
|
||||
this._register(this.registerContribution(instantiationService.createInstance(AgentHostCommitOperationContribution, this._stateManager)));
|
||||
this._register(this.registerContribution(instantiationService.createInstance(AgentHostPullRequestOperationContribution, this._stateManager)));
|
||||
this._register(this.registerContribution(instantiationService.createInstance(AgentHostSyncOperationContribution, this._stateManager)));
|
||||
this._register(this.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution, this._stateManager)));
|
||||
}
|
||||
|
||||
registerContribution(contribution: IChangesetOperationContribution): IDisposable {
|
||||
@@ -59,12 +49,12 @@ export class AgentHostChangesetOperationService extends Disposable implements IA
|
||||
});
|
||||
}
|
||||
|
||||
updateOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): void {
|
||||
getOperations(sessionKey: string, changeset: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): readonly ChangesetOperation[] | undefined {
|
||||
if (!gitState) {
|
||||
const sessionState = this._stateManager.getSessionState(sessionKey);
|
||||
gitState = readSessionGitState(sessionState?._meta);
|
||||
if (!gitState) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,29 +63,18 @@ export class AgentHostChangesetOperationService extends Disposable implements IA
|
||||
gitHubState = readSessionGitHubState(sessionState?.summary._meta);
|
||||
}
|
||||
|
||||
const changesets = changeset
|
||||
? [changeset]
|
||||
: this._changesetSubscriptions.getSessionSubscriptions(sessionKey);
|
||||
|
||||
for (const changeset of changesets) {
|
||||
const parsed = parseChangesetUri(changeset);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const operations = this._getOperations({
|
||||
sessionKey,
|
||||
changesetUri: changeset,
|
||||
changesetKind: parsed.kind,
|
||||
gitState,
|
||||
gitHubState
|
||||
});
|
||||
|
||||
this._stateManager.dispatchServerAction(changeset, {
|
||||
type: ActionType.ChangesetOperationsChanged,
|
||||
operations: operations ? [...operations] : undefined,
|
||||
});
|
||||
const parsed = parseChangesetUri(changeset);
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._getOperations({
|
||||
sessionKey,
|
||||
changesetUri: changeset,
|
||||
changesetKind: parsed.kind,
|
||||
gitState,
|
||||
gitHubState
|
||||
});
|
||||
}
|
||||
|
||||
private _getOperations(context: IChangesetOperationContext): readonly ChangesetOperation[] | undefined {
|
||||
@@ -122,6 +101,34 @@ export class AgentHostChangesetOperationService extends Disposable implements IA
|
||||
return operations;
|
||||
}
|
||||
|
||||
updateOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): void {
|
||||
if (!gitState) {
|
||||
const sessionState = this._stateManager.getSessionState(sessionKey);
|
||||
gitState = readSessionGitState(sessionState?._meta);
|
||||
if (!gitState) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gitHubState) {
|
||||
const sessionState = this._stateManager.getSessionState(sessionKey);
|
||||
gitHubState = readSessionGitHubState(sessionState?.summary._meta);
|
||||
}
|
||||
|
||||
const changesets = changeset
|
||||
? [changeset]
|
||||
: this._changesetSubscriptions.getSessionSubscriptions(sessionKey);
|
||||
|
||||
for (const changeset of changesets) {
|
||||
const operations = this.getOperations(sessionKey, changeset, gitState, gitHubState);
|
||||
|
||||
this._stateManager.dispatchServerAction(changeset, {
|
||||
type: ActionType.ChangesetOperationsChanged,
|
||||
operations: operations ? [...operations] : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _refreshSessionGitStateAndOperations(sessionKey: string): Promise<void> {
|
||||
const gitState = await this._gitStateService.refreshSessionGitState(sessionKey);
|
||||
if (!gitState) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import { computeSessionDiffs, computeTurnDiffs, computeUnionedDiffs, type IIncre
|
||||
import { META_CHECKPOINT_WORKING_DIR } from './agentHostCheckpointService.js';
|
||||
import { IAgentHostChangesetService, IPersistedChangesetMetadata, IRestoredChangesetDiffs, CHANGESET_DB_METADATA_KEYS, META_CHANGES_SUMMARY, META_CHANGESET_BRANCH, META_CHANGESET_SESSION, META_LEGACY_DIFFS, StaticChangesetKind } from '../common/agentHostChangesetService.js';
|
||||
import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js';
|
||||
import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js';
|
||||
|
||||
function staticChangesetUri(session: ProtocolURI, kind: StaticChangesetKind): ProtocolURI {
|
||||
return kind === 'branch'
|
||||
@@ -158,6 +159,7 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC
|
||||
@IAgentHostGitService private readonly _gitService: IAgentHostGitService,
|
||||
@IAgentHostCheckpointService private readonly _checkpointService: IAgentHostCheckpointService,
|
||||
@IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService,
|
||||
@IAgentHostChangesetOperationService private readonly _changesetOperationService: IAgentHostChangesetOperationService,
|
||||
@IAgentHostChangesetSubscriptionService private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService,
|
||||
) {
|
||||
super();
|
||||
@@ -794,6 +796,7 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC
|
||||
}
|
||||
|
||||
this._publishChangesetDiffs(session, changesetUri, diffs);
|
||||
|
||||
// Persist the file list so a subsequent `listSessions` /
|
||||
// `restoreSession` can reseed the changeset before the first
|
||||
// post-restart compute completes.
|
||||
@@ -859,36 +862,27 @@ export class AgentHostChangesetService extends Disposable implements IAgentHostC
|
||||
* fresh file list has been applied.
|
||||
*/
|
||||
private _publishChangesetDiffs(session: ProtocolURI, changesetUri: ProtocolURI, diffs: readonly ISessionFileDiff[]): void {
|
||||
const previous = this._stateManager.getChangesetState(changesetUri);
|
||||
const previousIds = new Set<string>(previous?.files.map(f => f.id) ?? []);
|
||||
// Get the available operations for this changeset. This call assumes that at this point
|
||||
// the git state of the session is up-to-date as it is being used to determine the available
|
||||
// operations. Long term this should be replaced with a more robust mechanism.
|
||||
const operations = this._changesetOperationService.getOperations(session, changesetUri);
|
||||
|
||||
// Emit file upserts. Use `after.uri` as the stable id when available
|
||||
// (covers creates and edits) and fall back to `before.uri` for
|
||||
// deletions; this matches the spec's recommendation and avoids id
|
||||
// collisions for renames (which carry distinct before/after URIs).
|
||||
const nextFilesById = new Map<string, ISessionFileDiff>();
|
||||
const files: ChangesetFile[] = [];
|
||||
for (const edit of diffs) {
|
||||
const id = edit.after?.uri ?? edit.before?.uri;
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
nextFilesById.set(id, edit);
|
||||
const file: ChangesetFile = { id, edit };
|
||||
this._stateManager.dispatchServerAction(changesetUri, {
|
||||
type: ActionType.ChangesetFileSet,
|
||||
file,
|
||||
});
|
||||
files.push({ id, edit });
|
||||
}
|
||||
|
||||
// Emit removals for any file that disappeared in this pass.
|
||||
for (const id of previousIds) {
|
||||
if (!nextFilesById.has(id)) {
|
||||
this._stateManager.dispatchServerAction(changesetUri, {
|
||||
type: ActionType.ChangesetFileRemoved,
|
||||
fileId: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
this._stateManager.dispatchServerAction(changesetUri, {
|
||||
type: ActionType.ChangesetContentChanged,
|
||||
files,
|
||||
operations: operations
|
||||
? [...operations]
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Move the changeset out of `computing` (or out of an earlier error)
|
||||
// now that we have a fresh, complete file list.
|
||||
|
||||
@@ -1033,7 +1033,7 @@ export class AgentHostStateManager extends Disposable {
|
||||
origin,
|
||||
};
|
||||
|
||||
this._logService.trace(`[AgentHostStateManager] Emitting envelope: seq=${envelope.serverSeq}, type=${action.type}${origin ? `, origin=${origin.clientId}:${origin.clientSeq}` : ''}`);
|
||||
this._logService.trace(`[AgentHostStateManager] Emitting envelope: seq=${envelope.serverSeq}, channel=${envelope.channel}, type=${action.type}${origin ? `, origin=${origin.clientId}:${origin.clientSeq}` : ''}`);
|
||||
this._onDidEmitEnvelope.fire(envelope);
|
||||
|
||||
return resultingState;
|
||||
|
||||
@@ -64,6 +64,10 @@ import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChang
|
||||
import { AgentHostChangesetSubscriptionService } from './agentHostChangesetSubscriptionService.js';
|
||||
import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js';
|
||||
import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js';
|
||||
import { AgentHostCommitOperationContribution } from './agentHostCommitOperationProvider.js';
|
||||
import { AgentHostDiscardChangesOperationContribution } from './agentHostDiscardChangesOperationProvider.js';
|
||||
import { AgentHostPullRequestOperationContribution } from './agentHostPullRequestOperationProvider.js';
|
||||
import { AgentHostSyncOperationContribution } from './agentHostSyncOperationProvider.js';
|
||||
|
||||
/**
|
||||
* Grace period before an empty, unsubscribed session is garbage-collected
|
||||
@@ -274,19 +278,25 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
this._changesetSubscriptions = instantiationService.createInstance(AgentHostChangesetSubscriptionService);
|
||||
services.set(IAgentHostChangesetSubscriptionService, this._changesetSubscriptions);
|
||||
|
||||
// The changeset service is responsible for computing, publishing, and persisting changesets.
|
||||
this._changesets = this._register(instantiationService.createInstance(AgentHostChangesetService, this._stateManager));
|
||||
services.set(IAgentHostChangesetService, this._changesets);
|
||||
|
||||
// The operation contribution service manages the lifecycle of changeset operations.
|
||||
this._changesetOperationService = this._register(instantiationService.createInstance(AgentHostChangesetOperationService, this._stateManager));
|
||||
services.set(IAgentHostChangesetOperationService, this._changesetOperationService);
|
||||
|
||||
// The changeset service is responsible for computing, publishing, and persisting changesets.
|
||||
this._changesets = this._register(instantiationService.createInstance(AgentHostChangesetService, this._stateManager));
|
||||
services.set(IAgentHostChangesetService, this._changesets);
|
||||
|
||||
// The coordinator owns all AgentService-side orchestration of the changeset feature: lifecycle
|
||||
// hooks, listSessions overlay, subscription URI routing, and the deferred-refresh state machine.
|
||||
this._changesetCoordinator = this._register(instantiationService.createInstance(AgentHostChangesetCoordinator, this._stateManager));
|
||||
this._register(this._stateManager.onDidChangeSessionActiveTurn(e => this._changesetCoordinator.onSessionTurnActiveChanged(e.session, e.active)));
|
||||
|
||||
// Register the changeset operation contributions.
|
||||
this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostCommitOperationContribution, this._stateManager)));
|
||||
this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostPullRequestOperationContribution, this._stateManager)));
|
||||
this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostSyncOperationContribution, this._stateManager)));
|
||||
this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution, this._stateManager)));
|
||||
|
||||
this._completions = this._register(instantiationService.createInstance(AgentHostCompletions));
|
||||
// Built-in generic provider: completes files in the session's workspace folder.
|
||||
const workspaceFiles = this._register(instantiationService.createInstance(AgentHostWorkspaceFiles));
|
||||
|
||||
@@ -67,6 +67,7 @@ suite('ChangesetSessionCoordinator', () => {
|
||||
const operationContributionService: IAgentHostChangesetOperationService = {
|
||||
_serviceBrand: undefined,
|
||||
registerContribution: () => Disposable.None,
|
||||
getOperations: () => undefined,
|
||||
updateOperations: () => { },
|
||||
invokeChangesetOperation: async () => ({}),
|
||||
dispose: () => { },
|
||||
|
||||
@@ -7,7 +7,6 @@ import assert from 'assert';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { DisposableStore, type IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
import { InstantiationService } from '../../../instantiation/common/instantiationService.js';
|
||||
import { NullLogService } from '../../../log/common/log.js';
|
||||
import type { IChangesetOperationContribution, IChangesetOperationContext, IChangesetOperationHandler, IChangesetOperationRegistry } from '../../common/agentHostChangesetOperationService.js';
|
||||
import { buildUncommittedChangesetUri } from '../../common/changesetUri.js';
|
||||
@@ -85,7 +84,6 @@ suite('AgentHostChangesetOperationService', () => {
|
||||
stateManager,
|
||||
new TestGitStateService(),
|
||||
new AgentHostChangesetSubscriptionService(),
|
||||
disposables.add(new InstantiationService()),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ActionEnvelope, ActionType } from '../../common/state/sessionActions.js
|
||||
import { ChangesetStatus, SessionStatus, withSessionGitState, type Changeset } from '../../common/state/sessionState.js';
|
||||
import { AgentHostChangesetService } from '../../node/agentHostChangesetService.js';
|
||||
import { IAgentHostChangesetSubscriptionService } from '../../common/agentHostChangesetSubscriptionService.js';
|
||||
import { IAgentHostChangesetOperationService } from '../../common/agentHostChangesetOperationService.js';
|
||||
import { NULL_CHECKPOINT_SERVICE } from '../../common/agentHostCheckpointService.js';
|
||||
import { IAgentHostGitService } from '../../common/agentHostGitService.js';
|
||||
import { AgentHostStateManager } from '../../node/agentHostStateManager.js';
|
||||
@@ -41,6 +42,22 @@ function createSubscriptionService(...changesets: string[]): IAgentHostChangeset
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a no-op changeset operation service for tests. It advertises no
|
||||
* operations, which mirrors the default behaviour of a session without any
|
||||
* operation contributions.
|
||||
*/
|
||||
function createOperationService(): IAgentHostChangesetOperationService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
registerContribution: () => toDisposable(() => { }),
|
||||
updateOperations: () => { },
|
||||
getOperations: () => undefined,
|
||||
invokeChangesetOperation: async () => { throw new Error('not implemented'); },
|
||||
dispose: () => { },
|
||||
};
|
||||
}
|
||||
|
||||
suite.skip('AgentHostChangesetService', () => {
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
@@ -73,6 +90,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
createNoopGitService(),
|
||||
NULL_CHECKPOINT_SERVICE,
|
||||
disposables.add(new AgentConfigurationService(stateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
createSubscriptionService(buildUncommittedChangesetUri(sessionUri.toString())),
|
||||
));
|
||||
});
|
||||
@@ -258,7 +276,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
} as unknown as IAgentHostGitService;
|
||||
|
||||
const localChangesets = disposables.add(new AgentHostChangesetService(
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createSubscriptionService(buildUncommittedChangesetUri(sessionUri.toString()))));
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createOperationService(), createSubscriptionService(buildUncommittedChangesetUri(sessionUri.toString()))));
|
||||
|
||||
localStateManager.createSession({
|
||||
resource: sessionUri.toString(),
|
||||
@@ -299,15 +317,16 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
{ workingDirectory: 'file:///wd', sessionUri: sessionUri.toString(), baseBranch: undefined },
|
||||
{ workingDirectory: 'file:///wd', sessionUri: sessionUri.toString(), baseBranch: 'main' },
|
||||
]);
|
||||
// Each git diff lands as its own `changeset/fileSet` envelope.
|
||||
// Walk the captured stream and reconstruct the per-changeset
|
||||
// file lists to assert each matches the git service output.
|
||||
const fileSets = envelopes
|
||||
.filter(e => e.action.type === ActionType.ChangesetFileSet) as Array<{ channel: string; action: { file: { edit: unknown } } }>;
|
||||
const sessionFileSets = fileSets.filter(e => e.channel === `${sessionUri.toString()}/changeset/session`);
|
||||
const uncommittedFileSets = fileSets.filter(e => e.channel === `${sessionUri.toString()}/changeset/uncommitted`);
|
||||
assert.deepStrictEqual(sessionFileSets.map(e => e.action.file.edit), gitDiffs);
|
||||
assert.deepStrictEqual(uncommittedFileSets.map(e => e.action.file.edit), gitDiffs);
|
||||
// Each compute pass lands as a single `changeset/contentChanged`
|
||||
// envelope carrying the full file list. Walk the captured stream
|
||||
// and reconstruct the per-changeset file lists to assert each
|
||||
// matches the git service output.
|
||||
const contentChanges = envelopes
|
||||
.filter(e => e.action.type === ActionType.ChangesetContentChanged) as Array<{ channel: string; action: { files: Array<{ edit: unknown }> } }>;
|
||||
const sessionContent = contentChanges.filter(e => e.channel === `${sessionUri.toString()}/changeset/session`);
|
||||
const uncommittedContent = contentChanges.filter(e => e.channel === `${sessionUri.toString()}/changeset/uncommitted`);
|
||||
assert.deepStrictEqual(sessionContent.at(-1)?.action.files.map(f => f.edit), gitDiffs);
|
||||
assert.deepStrictEqual(uncommittedContent.at(-1)?.action.files.map(f => f.edit), gitDiffs);
|
||||
|
||||
// The compute pass also persists the file list under the
|
||||
// legacy `'diffs'` slot so it survives restarts. The write
|
||||
@@ -335,7 +354,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
},
|
||||
} as unknown as IAgentHostGitService;
|
||||
const localChangesets = disposables.add(new AgentHostChangesetService(
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createSubscriptionService(buildUncommittedChangesetUri(sessionUri.toString()))));
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createOperationService(), createSubscriptionService(buildUncommittedChangesetUri(sessionUri.toString()))));
|
||||
const sessionStr = sessionUri.toString();
|
||||
|
||||
localStateManager.createSession({
|
||||
@@ -371,7 +390,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
},
|
||||
} as unknown as IAgentHostGitService;
|
||||
const localChangesets = disposables.add(new AgentHostChangesetService(
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createSubscriptionService()));
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createOperationService(), createSubscriptionService()));
|
||||
const sessionStr = sessionUri.toString();
|
||||
|
||||
localStateManager.createSession({
|
||||
@@ -404,7 +423,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
} as unknown as IAgentHostGitService;
|
||||
|
||||
const localChangesets = disposables.add(new AgentHostChangesetService(
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createSubscriptionService()));
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createOperationService(), createSubscriptionService()));
|
||||
|
||||
localStateManager.createSession({
|
||||
resource: sessionUri.toString(),
|
||||
@@ -431,14 +450,15 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
await diffsEmitted;
|
||||
|
||||
// With no recorded edits, the edit-tracker aggregator returns an
|
||||
// empty array — no `changeset/fileSet` envelopes are emitted. The
|
||||
// important assertion is that we still ran the producer through
|
||||
// to a `changeset/statusChanged → ready` envelope, which proves
|
||||
// the fallback path executed without throwing.
|
||||
const fileSets = envelopes
|
||||
// empty array — the single `changeset/contentChanged` envelope
|
||||
// carries an empty file list. The important assertion is that we
|
||||
// still ran the producer through to a `changeset/statusChanged →
|
||||
// ready` envelope, which proves the fallback path executed without
|
||||
// throwing.
|
||||
const contentChanges = envelopes
|
||||
.map(e => e.action)
|
||||
.filter(a => a.type === ActionType.ChangesetFileSet);
|
||||
assert.deepStrictEqual(fileSets, []);
|
||||
.filter(a => a.type === ActionType.ChangesetContentChanged) as Array<{ files: unknown[] }>;
|
||||
assert.deepStrictEqual(contentChanges.map(a => a.files), [[]]);
|
||||
const statusAction = envelopes
|
||||
.map(e => e.action)
|
||||
.find(a => a.type === ActionType.ChangesetStatusChanged);
|
||||
@@ -463,7 +483,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
} as unknown as IAgentHostGitService;
|
||||
|
||||
const localChangesets = disposables.add(new AgentHostChangesetService(
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createSubscriptionService()));
|
||||
localStateManager, new NullLogService(), sessionDataService, stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createOperationService(), createSubscriptionService()));
|
||||
|
||||
const sessionStr = sessionUri.toString();
|
||||
localStateManager.createSession({
|
||||
@@ -537,7 +557,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
} as unknown as IAgentHostGitService;
|
||||
const localStateManager = disposables.add(new AgentHostStateManager(new NullLogService()));
|
||||
const localChangesets = disposables.add(new AgentHostChangesetService(
|
||||
localStateManager, new NullLogService(), createNullSessionDataService(), stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createSubscriptionService(buildUncommittedChangesetUri(sessionUri.toString()))));
|
||||
localStateManager, new NullLogService(), createNullSessionDataService(), stubGit, NULL_CHECKPOINT_SERVICE, disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())), createOperationService(), createSubscriptionService(buildUncommittedChangesetUri(sessionUri.toString()))));
|
||||
|
||||
const sessionStr = sessionUri.toString();
|
||||
localStateManager.createSession({
|
||||
@@ -584,6 +604,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
stubGit,
|
||||
NULL_CHECKPOINT_SERVICE,
|
||||
disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
subscriptionService,
|
||||
));
|
||||
return { service, localStateManager, computes, subscriptions: subscriptionService.subscriptions };
|
||||
@@ -904,6 +925,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
createNoopGitService(),
|
||||
NULL_CHECKPOINT_SERVICE,
|
||||
disposables.add(new AgentConfigurationService(stateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
subscriptionService,
|
||||
));
|
||||
}
|
||||
@@ -995,10 +1017,10 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('per-turn URI streams incremental ChangesetFileSet / ChangesetFileRemoved as the same turn is recomputed', async () => {
|
||||
test('per-turn URI streams a ChangesetContentChanged snapshot as the same turn is recomputed', async () => {
|
||||
// End-to-end variant exercising the real `computeTurnDiffs` path
|
||||
// — produces actual diff payloads from session-DB messages so
|
||||
// `_publishChangesetDiffs` emits real per-file actions on each
|
||||
// `_publishChangesetDiffs` emits a full content snapshot on each
|
||||
// recompute pass.
|
||||
const sessionDb = new SessionDatabase(':memory:');
|
||||
disposables.add(toDisposable(() => sessionDb.close()));
|
||||
@@ -1010,6 +1032,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
createNoopGitService(),
|
||||
NULL_CHECKPOINT_SERVICE,
|
||||
disposables.add(new AgentConfigurationService(localStateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
createSubscriptionService(buildTurnChangesetUri(sessionUri.toString(), 'turn-1')),
|
||||
));
|
||||
|
||||
@@ -1089,6 +1112,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
'mod': { parent: 'ref-orig', current: 'ref-mod' },
|
||||
}),
|
||||
disposables.add(new AgentConfigurationService(stateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
createSubscriptionService(),
|
||||
));
|
||||
|
||||
@@ -1121,6 +1145,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
// 'mod' is intentionally absent
|
||||
}),
|
||||
disposables.add(new AgentConfigurationService(stateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
createSubscriptionService(),
|
||||
));
|
||||
|
||||
@@ -1150,6 +1175,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
'mod': { parent: 'same-ref', current: 'same-ref' },
|
||||
}),
|
||||
disposables.add(new AgentConfigurationService(stateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
createSubscriptionService(),
|
||||
));
|
||||
|
||||
@@ -1180,6 +1206,7 @@ suite.skip('AgentHostChangesetService', () => {
|
||||
'mod': { parent: 'ref-orig', current: 'ref-mod' },
|
||||
}),
|
||||
disposables.add(new AgentConfigurationService(stateManager, new NullLogService())),
|
||||
createOperationService(),
|
||||
createSubscriptionService(),
|
||||
));
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { tmpdir } from 'os';
|
||||
import { join } from '../../../../../base/common/path.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { SubscribeResult } from '../../../common/state/protocol/commands.js';
|
||||
import type { ChangesetFileSetAction, SessionAddedParams } from '../../../common/state/sessionActions.js';
|
||||
import type { ChangesetContentChangedAction, SessionAddedParams } from '../../../common/state/sessionActions.js';
|
||||
import { PROTOCOL_VERSION } from '../../../common/state/protocol/version/registry.js';
|
||||
import {
|
||||
dispatchTurnStarted,
|
||||
@@ -101,19 +101,30 @@ const hasGit = (() => {
|
||||
const editedFile = join(tmpRoot, 'from-terminal.txt');
|
||||
dispatchTurnStarted(client, sessionUri, 'turn-1', `terminal-edit:${editedFile}`, 1);
|
||||
|
||||
// Wait for a `changeset/fileSet` action targeting the edited file.
|
||||
// On macOS, git's `--show-toplevel` resolves symlinks (/var →
|
||||
// /private/var) so the URI may differ in prefix; match by basename.
|
||||
const fileSetNotif = await client.waitForNotification(n => {
|
||||
if (!isActionNotification(n, 'changeset/fileSet')) {
|
||||
// Wait for a `changeset/contentChanged` action whose file list
|
||||
// includes the edited file. On macOS, git's `--show-toplevel`
|
||||
// resolves symlinks (/var → /private/var) so the URI may differ in
|
||||
// prefix; match by basename.
|
||||
const fileUri = (edit: ChangesetContentChangedAction['files'][number]['edit']) =>
|
||||
edit.after?.uri ?? edit.before?.uri;
|
||||
const matchesEditedFile = (action: ChangesetContentChangedAction) =>
|
||||
action.files.some(f => {
|
||||
const u = fileUri(f.edit);
|
||||
return typeof u === 'string' && u.endsWith('/from-terminal.txt');
|
||||
});
|
||||
const contentChangedNotif = await client.waitForNotification(n => {
|
||||
if (!isActionNotification(n, 'changeset/contentChanged')) {
|
||||
return false;
|
||||
}
|
||||
const action = getActionEnvelope(n).action as ChangesetFileSetAction;
|
||||
const u = action.file.edit.after?.uri ?? action.file.edit.before?.uri;
|
||||
return typeof u === 'string' && u.endsWith('/from-terminal.txt');
|
||||
return matchesEditedFile(getActionEnvelope(n).action as ChangesetContentChangedAction);
|
||||
}, 10_000);
|
||||
const action = getActionEnvelope(fileSetNotif).action as ChangesetFileSetAction;
|
||||
assert.ok(action.file.edit.after, 'expected after-side for newly added file');
|
||||
assert.ok(!action.file.edit.before, 'newly added file should have no before-side');
|
||||
const action = getActionEnvelope(contentChangedNotif).action as ChangesetContentChangedAction;
|
||||
const file = action.files.find(f => {
|
||||
const u = fileUri(f.edit);
|
||||
return typeof u === 'string' && u.endsWith('/from-terminal.txt');
|
||||
});
|
||||
assert.ok(file, 'expected the edited file in the changeset content');
|
||||
assert.ok(file.edit.after, 'expected after-side for newly added file');
|
||||
assert.ok(!file.edit.before, 'newly added file should have no before-side');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user