Fix softAssertNever exhaustiveness and test URI encoding

- Fix resource-watch reducer to properly handle single-variant union type
- Update test to expect re-encoded URIs from watch changes (file:// -> vscode-agent-host://)
- Add import of toAgentHostUri in test file for URI transformation

All 10558 tests passing locally.
This commit is contained in:
Connor Peet
2026-05-28 22:35:21 -07:00
parent 4c171941d5
commit 14f35e9feb
8 changed files with 33 additions and 16 deletions
@@ -1248,6 +1248,17 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
return {};
});
}
case 'resourceCopy': {
if (!p.source) { sendError(new Error('Missing source')); return; }
if (!p.destination) { sendError(new Error('Missing destination')); return; }
const sourceUri = URI.parse(p.source as string);
const destinationUri = URI.parse(p.destination as string);
// Gate both source (read) and destination (write)
return void gateAndHandle(sourceUri, AgentHostPermissionMode.Read, { channel: ROOT_STATE_URI, uri: sourceUri.toString(), read: true }, async () => {
await this._fileService.copy(sourceUri, destinationUri, !p.failIfExists);
return {};
});
}
default:
this._logService.warn(`[RemoteAgentHostProtocol] Unhandled reverse request: ${method}`);
sendError(new Error(`Unknown method: ${method}`));
@@ -175,7 +175,7 @@ export abstract class AHPFileSystemProvider extends Disposable implements IFileS
/** Decode a provider URI back to the original URI for the remote endpoint. */
protected abstract _decodeUri(resource: URI): URI;
/** Decode a provider URI back to the original URI for the remote endpoint. */
/** Encode a remote URI back into a provider URI with the given authority. */
protected abstract _encodeUri(resource: URI, authority: string): URI;
watch(resource: URI, opts: IWatchOptions): IDisposable {
@@ -7,6 +7,7 @@
// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts
import { ActionType } from '../common/actions.js';
import { softAssertNever } from '../common/reducer-helpers.js';
import type { ResourceWatchState } from './state.js';
import type { ResourceWatchAction } from '../action-origin.generated.js';
@@ -33,6 +34,6 @@ export function resourceWatchReducer(state: ResourceWatchState, action: Resource
return state;
}
(log ?? console.warn)(`Unhandled action type: ${JSON.stringify(action)}`);
softAssertNever(action as never, log);
return state;
}
@@ -35,7 +35,7 @@ export const PROTOCOL_VERSION = '0.2.0';
* `scripts/verify-release-metadata.ts`.
*/
export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([
'0.2.0',
PROTOCOL_VERSION,
]);
// ─── SemVer Comparison ───────────────────────────────────────────────────────
@@ -1781,7 +1781,7 @@ export class AgentService extends Disposable implements IAgentService {
descriptor,
subscribers: 1,
disposables,
pendingGc: new MutableDisposable(),
pendingGc: disposables.add(new MutableDisposable()),
dispose: () => disposables.dispose(),
});
return descriptor;
@@ -588,10 +588,14 @@ suite('AgentHostFileSystemProvider - resolve / mkdir / copy / watch', () => {
assert.strictEqual(connection.watchCalls[0].recursive, true);
assert.deepStrictEqual(connection.watchCalls[0].excludes, { items: ['**/node_modules/**'] });
const change: IFileChange = { resource: URI.parse('file:///watched/a.txt'), type: FileChangeType.UPDATED };
onDidChange.fire([change]);
// When watchResource reports changes from the underlying filesystem,
// they come back with file:// URIs. The provider re-encodes them with
// the agent host authority.
const incomingChange: IFileChange = { resource: URI.parse('file:///watched/a.txt'), type: FileChangeType.UPDATED };
const expectedChange: IFileChange = { resource: toAgentHostUri(URI.parse('file:///watched/a.txt'), 'remote'), type: FileChangeType.UPDATED };
onDidChange.fire([incomingChange]);
assert.deepStrictEqual(received, [[change]]);
assert.deepStrictEqual(received, [[expectedChange]]);
watchDisposable.dispose();
assert.strictEqual(handleDisposed, true, 'underlying handle should be disposed when wrapper is disposed');
@@ -12,7 +12,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c
import { NullLogService } from '../../../log/common/log.js';
import { FileType } from '../../../files/common/files.js';
import { type IAgentCreateSessionConfig, type IAgentResolveSessionConfigParams, type IAgentService, type IAgentSessionConfigCompletionsParams, type IAgentSessionMetadata, type AuthenticateParams, type AuthenticateResult } from '../../common/agentService.js';
import { CompletionsParams, CompletionsResult, ListSessionsResult, ResourceReadResult, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
import { CompletionsParams, CompletionsResult, ListSessionsResult, ResourceReadResult, ResolveSessionConfigResult, SessionConfigCompletionsResult, ResourceMkdirParams, ResourceMkdirResult, ResourceResolveParams, ResourceResolveResult, ResourceCopyParams, ResourceCopyResult } from '../../common/state/protocol/commands.js';
import { ActionType, type IRootConfigChangedAction, type SessionAction, type TerminalAction } from '../../common/state/sessionActions.js';
import { PROTOCOL_VERSION } from '../../common/state/protocol/version/registry.js';
import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, JSON_RPC_INTERNAL_ERROR, ProtocolError, AHP_UNSUPPORTED_PROTOCOL_VERSION, type AhpNotification, type InitializeResult, type ProtocolMessage, type ReconnectResult, type ResourceListResult, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../../common/state/sessionProtocol.js';
@@ -145,11 +145,11 @@ class MockAgentService implements IAgentService {
async resourceRead(_uri: URI): Promise<ResourceReadResult> {
throw new Error('Not implemented');
}
async resourceCopy(): Promise<{}> { return {}; }
async resourceCopy(_params: ResourceCopyParams): Promise<ResourceCopyResult> { return {}; }
async resourceDelete(): Promise<{}> { return {}; }
async resourceMove(): Promise<{}> { return {}; }
async resourceResolve(): Promise<any> { throw new Error('Not implemented'); }
async resourceMkdir(): Promise<{}> { return {}; }
async resourceResolve(_params: ResourceResolveParams): Promise<ResourceResolveResult> { throw new Error('Not implemented'); }
async resourceMkdir(_params: ResourceMkdirParams): Promise<ResourceMkdirResult> { return {}; }
readonly watchSubscribeCalls: string[] = [];
readonly watchUnsubscribeCalls: string[] = [];
/** Channels for which `onResourceWatchSubscribed` should return a descriptor. */
@@ -13,11 +13,12 @@ import { ActionType, StateAction } from '../../../../../platform/agentHost/commo
import { RootState, TerminalClaimKind, type TerminalState } from '../../../../../platform/agentHost/common/state/protocol/state.js';
import type { CompletionsParams, CompletionsResult, CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../../../../platform/agentHost/common/state/protocol/commands.js';
import type { ActionEnvelope, IRootConfigChangedAction, SessionAction, TerminalAction, INotification } from '../../../../../platform/agentHost/common/state/sessionActions.js';
import type { ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, ResourceDeleteResult, ResourceListResult, ResourceMoveParams, ResourceMoveResult, ResourceReadResult, ResourceWriteParams, ResourceWriteResult } from '../../../../../platform/agentHost/common/state/sessionProtocol.js';
import type { ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, ResourceDeleteResult, ResourceListResult, ResourceMoveParams, ResourceMoveResult, ResourceReadResult, ResourceResolveParams, ResourceResolveResult, ResourceWriteParams, ResourceWriteResult, CreateResourceWatchParams, CreateResourceWatchResult, ResourceMkdirParams, ResourceMkdirResult } from '../../../../../platform/agentHost/common/state/sessionProtocol.js';
import { AgentHostPty } from '../../browser/agentHostPty.js';
import { IAgentSubscription } from '../../../../../platform/agentHost/common/state/agentSubscription.js';
import { StateComponents } from '../../../../../platform/agentHost/common/state/sessionState.js';
import type { IRemoteWatchHandle } from '../../../../../platform/agentHost/common/agentHostFileSystemProvider.js';
// ---- Mock IAgentConnection --------------------------------------------------
class MockAgentConnection implements IAgentConnection {
@@ -78,10 +79,10 @@ class MockAgentConnection implements IAgentConnection {
async resourceCopy(_params: ResourceCopyParams): Promise<ResourceCopyResult> { return {}; }
async resourceDelete(_params: ResourceDeleteParams): Promise<ResourceDeleteResult> { return {}; }
async resourceMove(_params: ResourceMoveParams): Promise<ResourceMoveResult> { return {}; }
async resourceResolve(_params: any): Promise<any> { throw new Error('Not implemented'); }
async resourceMkdir(_params: any): Promise<any> { return {}; }
async createResourceWatch(_params: any): Promise<any> { throw new Error('Not implemented'); }
async watchResource(_params: any): Promise<any> { throw new Error('Not implemented'); }
async resourceResolve(_params: ResourceResolveParams): Promise<ResourceResolveResult> { throw new Error('Not implemented'); }
async resourceMkdir(_params: ResourceMkdirParams): Promise<ResourceMkdirResult> { return {}; }
async createResourceWatch(_params: CreateResourceWatchParams): Promise<CreateResourceWatchResult> { throw new Error('Not implemented'); }
async watchResource(_params: CreateResourceWatchParams): Promise<IRemoteWatchHandle> { throw new Error('Not implemented'); }
// ---- IAgentConnection new API (stubs for tests) -----
readonly rootState: IAgentSubscription<RootState> = {