[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
432
application/state/pluginTerminalProviderRegistry.test.ts
Normal file
432
application/state/pluginTerminalProviderRegistry.test.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
collectPluginTerminalProviderKinds,
|
||||
isPluginTerminalProviderKindAvailable,
|
||||
PluginTerminalProviderAvailability,
|
||||
PluginTerminalProviderRegistry,
|
||||
} from './pluginTerminalProviderRegistry.ts';
|
||||
|
||||
const session: NetcattyTerminalSessionSnapshot = {
|
||||
sessionId: 'session-1',
|
||||
protocol: 'ssh',
|
||||
status: 'connected',
|
||||
};
|
||||
|
||||
test('terminal Provider availability fails closed before or after failed enumeration', async () => {
|
||||
assert.equal(isPluginTerminalProviderKindAvailable(null, 'terminal.matcher'), false);
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { throw new Error('bridge unavailable'); },
|
||||
async providePluginTerminal() { return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
const kinds = await collectPluginTerminalProviderKinds(registry, [
|
||||
'terminal.matcher',
|
||||
'terminal.background',
|
||||
]);
|
||||
assert.equal(isPluginTerminalProviderKindAvailable(kinds, 'terminal.matcher'), false);
|
||||
assert.equal(isPluginTerminalProviderKindAvailable(kinds, 'terminal.background'), false);
|
||||
registry.dispose();
|
||||
});
|
||||
|
||||
test('terminal Provider availability ignores an older enumeration that finishes last', async () => {
|
||||
let resolveOlder: ((providers: NetcattyTerminalProviderContribution[]) => void) | undefined;
|
||||
const olderRegistry = new PluginTerminalProviderRegistry({
|
||||
listPluginTerminalProviders() {
|
||||
return new Promise((resolve) => { resolveOlder = resolve; });
|
||||
},
|
||||
async providePluginTerminal() { return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
const newerRegistry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() {
|
||||
return [{
|
||||
pluginId: 'com.example.matcher',
|
||||
pluginVersion: '1.0.0',
|
||||
pluginDisplayName: 'Matcher',
|
||||
provider: {
|
||||
id: 'com.example.matcher.output',
|
||||
label: 'Output matcher',
|
||||
kind: 'terminal.matcher',
|
||||
},
|
||||
}];
|
||||
},
|
||||
async providePluginTerminal() { return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
const availability = new PluginTerminalProviderAvailability();
|
||||
const older = availability.refresh(olderRegistry, ['terminal.matcher']);
|
||||
const newer = availability.refresh(newerRegistry, ['terminal.matcher']);
|
||||
assert.equal(await newer, true);
|
||||
assert.equal(availability.has('terminal.matcher'), true);
|
||||
resolveOlder?.([]);
|
||||
assert.equal(await older, false);
|
||||
assert.equal(availability.has('terminal.matcher'), true);
|
||||
olderRegistry.dispose();
|
||||
newerRegistry.dispose();
|
||||
});
|
||||
|
||||
test('terminal Provider registry coalesces immutable enumeration until contributions change', async () => {
|
||||
let changed: (() => void) | undefined;
|
||||
let listCalls = 0;
|
||||
const resolvers: Array<(providers: NetcattyTerminalProviderContribution[]) => void> = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
listPluginTerminalProviders() {
|
||||
listCalls += 1;
|
||||
return new Promise((resolve) => { resolvers.push(resolve); });
|
||||
},
|
||||
async providePluginTerminal() { return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
onPluginContributionsChanged(listener) {
|
||||
changed = listener;
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
const first = registry.listProviders({ kind: 'terminal.matcher' });
|
||||
const duplicate = registry.listProviders({ kind: 'terminal.matcher' });
|
||||
assert.equal(listCalls, 1);
|
||||
resolvers[0]?.([]);
|
||||
assert.strictEqual(await first, await duplicate);
|
||||
assert.strictEqual(await registry.listProviders({ kind: 'terminal.matcher' }), await first);
|
||||
assert.equal(listCalls, 1);
|
||||
|
||||
changed?.();
|
||||
const refreshed = registry.listProviders({ kind: 'terminal.matcher' });
|
||||
assert.equal(listCalls, 2);
|
||||
resolvers[1]?.([]);
|
||||
await refreshed;
|
||||
registry.dispose();
|
||||
});
|
||||
|
||||
test('terminal Provider registry suppresses repeated lifecycle RPC after the bridge is unavailable', async () => {
|
||||
let changed: (() => void) | undefined;
|
||||
let listCalls = 0;
|
||||
let lifecycleCalls = 0;
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() {
|
||||
listCalls += 1;
|
||||
throw new Error('PLUGIN_DEVELOPMENT_DISABLED');
|
||||
},
|
||||
async providePluginTerminal() { return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent() {
|
||||
lifecycleCalls += 1;
|
||||
throw new Error('PLUGIN_DEVELOPMENT_DISABLED');
|
||||
},
|
||||
onPluginContributionsChanged(listener) {
|
||||
changed = listener;
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(registry.listProviders({ kind: 'terminal.link' }), /PLUGIN_DEVELOPMENT_DISABLED/);
|
||||
assert.deepEqual(await registry.listProviders({ kind: 'terminal.link' }), []);
|
||||
assert.equal(listCalls, 1);
|
||||
await registry.publishSessionEvent({ type: 'created', session });
|
||||
await registry.publishSessionEvent({ type: 'titleChanged', session: { ...session, title: 'ready' } });
|
||||
assert.equal(lifecycleCalls, 0);
|
||||
|
||||
changed?.();
|
||||
await assert.rejects(
|
||||
registry.publishSessionEvent({ type: 'cwdChanged', session: { ...session, cwd: '/work' } }),
|
||||
/PLUGIN_DEVELOPMENT_DISABLED/,
|
||||
);
|
||||
await registry.publishSessionEvent({ type: 'resized', session: { ...session, cols: 80, rows: 24 } });
|
||||
assert.equal(lifecycleCalls, 1);
|
||||
registry.dispose();
|
||||
});
|
||||
|
||||
test('terminal Provider registry cancels superseded requests and suppresses stale results', async () => {
|
||||
const cancellations: string[] = [];
|
||||
const resolvers: Array<(value: ReadonlyArray<NetcattyTerminalProviderResult>) => void> = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
providePluginTerminal() {
|
||||
return new Promise((resolve) => resolvers.push(resolve));
|
||||
},
|
||||
async cancelPluginTerminalRequest(requestId) { cancellations.push(requestId); return true; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
|
||||
const first = registry.request({
|
||||
kind: 'terminal.completion',
|
||||
operation: 'provideCompletions',
|
||||
session,
|
||||
payload: { input: 'g' },
|
||||
});
|
||||
const second = registry.request({
|
||||
kind: 'terminal.completion',
|
||||
operation: 'provideCompletions',
|
||||
session,
|
||||
payload: { input: 'gi' },
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(cancellations.length, 1);
|
||||
resolvers[1]([]);
|
||||
const secondResult = await second;
|
||||
resolvers[0]([]);
|
||||
const firstResult = await first;
|
||||
assert.equal(secondResult.stale, false);
|
||||
assert.equal(firstResult.stale, true);
|
||||
assert.deepEqual(firstResult.results, []);
|
||||
});
|
||||
|
||||
test('terminal Provider registry aborts an in-flight bridge request and settles stale', async () => {
|
||||
const cancellations: string[] = [];
|
||||
const requests: NetcattyTerminalProviderRequest[] = [];
|
||||
const controller = new AbortController();
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
providePluginTerminal(request) {
|
||||
requests.push(request);
|
||||
return new Promise(() => {});
|
||||
},
|
||||
async cancelPluginTerminalRequest(requestId) { cancellations.push(requestId); return true; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
|
||||
const pending = registry.request({
|
||||
kind: 'terminal.completion',
|
||||
operation: 'provideCompletions',
|
||||
session,
|
||||
}, { signal: controller.signal });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(requests.length, 1);
|
||||
|
||||
controller.abort();
|
||||
const result = await pending;
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(result.stale, true);
|
||||
assert.deepEqual(result.results, []);
|
||||
assert.deepEqual(cancellations, [requests[0].requestId]);
|
||||
registry.dispose();
|
||||
});
|
||||
|
||||
test('terminal Provider registry scopes link supersession per buffer line', async () => {
|
||||
const cancellations: string[] = [];
|
||||
const resolvers: Array<(value: ReadonlyArray<NetcattyTerminalProviderResult>) => void> = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
providePluginTerminal() { return new Promise((resolve) => resolvers.push(resolve)); },
|
||||
async cancelPluginTerminalRequest(requestId) { cancellations.push(requestId); return true; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
const firstLine = registry.request({
|
||||
kind: 'terminal.link',
|
||||
operation: 'provideLinks',
|
||||
session,
|
||||
supersessionKey: 'line:1',
|
||||
});
|
||||
const secondLine = registry.request({
|
||||
kind: 'terminal.link',
|
||||
operation: 'provideLinks',
|
||||
session,
|
||||
supersessionKey: 'line:2',
|
||||
});
|
||||
assert.equal(cancellations.length, 0);
|
||||
const replacementFirstLine = registry.request({
|
||||
kind: 'terminal.link',
|
||||
operation: 'provideLinks',
|
||||
session,
|
||||
supersessionKey: 'line:1',
|
||||
});
|
||||
assert.equal(cancellations.length, 1);
|
||||
resolvers[1]([]);
|
||||
resolvers[2]([]);
|
||||
assert.equal((await secondLine).stale, false);
|
||||
assert.equal((await replacementFirstLine).stale, false);
|
||||
resolvers[0]([]);
|
||||
assert.equal((await firstLine).stale, true);
|
||||
});
|
||||
|
||||
test('terminal Provider registry returns a stale response when a superseded bridge request rejects', async () => {
|
||||
const resolvers: Array<{
|
||||
resolve: (value: ReadonlyArray<NetcattyTerminalProviderResult>) => void;
|
||||
reject: (error: Error) => void;
|
||||
}> = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
providePluginTerminal() {
|
||||
return new Promise((resolve, reject) => resolvers.push({ resolve, reject }));
|
||||
},
|
||||
async cancelPluginTerminalRequest() { return true; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
const first = registry.request({
|
||||
kind: 'terminal.decoration',
|
||||
operation: 'provideDecorations',
|
||||
session,
|
||||
});
|
||||
const second = registry.request({
|
||||
kind: 'terminal.decoration',
|
||||
operation: 'provideDecorations',
|
||||
session,
|
||||
});
|
||||
resolvers[1].resolve([]);
|
||||
assert.equal((await second).stale, false);
|
||||
resolvers[0].reject(new Error('cancelled'));
|
||||
const firstResult = await first;
|
||||
assert.equal(firstResult.stale, true);
|
||||
assert.deepEqual(firstResult.results, []);
|
||||
});
|
||||
|
||||
test('terminal Provider registry freezes enumeration and cancels all session requests', async () => {
|
||||
const cancellations: string[] = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() {
|
||||
return [{
|
||||
pluginId: 'com.example',
|
||||
pluginVersion: '1.0.0',
|
||||
pluginDisplayName: 'Example',
|
||||
provider: { id: 'com.example.completion', label: 'Completion', kind: 'terminal.completion' },
|
||||
}];
|
||||
},
|
||||
providePluginTerminal() { return new Promise(() => {}); },
|
||||
async cancelPluginTerminalRequest(requestId) { cancellations.push(requestId); return true; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
const providers = await registry.listProviders({ kind: 'terminal.completion' });
|
||||
assert.equal(Object.isFrozen(providers), true);
|
||||
assert.equal(Object.isFrozen(providers[0].provider), true);
|
||||
void registry.request({ kind: 'terminal.completion', operation: 'provide', session });
|
||||
void registry.request({ kind: 'terminal.decoration', operation: 'provide', session });
|
||||
registry.cancelSession(session.sessionId);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(cancellations.length, 2);
|
||||
});
|
||||
|
||||
test('terminal Provider registry publishes metadata-only lifecycle snapshots', async () => {
|
||||
const events: NetcattyTerminalSessionEvent[] = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
async providePluginTerminal() { return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent(event) { events.push(event); return []; },
|
||||
});
|
||||
await registry.publishSessionEvent({ type: 'connected', session });
|
||||
await registry.publishSessionEvent({
|
||||
type: 'cwdChanged',
|
||||
session: { sessionId: session.sessionId, protocol: 'ssh', status: 'connected', cwd: '/srv/app' },
|
||||
});
|
||||
assert.deepEqual(events, [
|
||||
{ type: 'connected', session },
|
||||
{ type: 'cwdChanged', session: { ...session, cwd: '/srv/app' } },
|
||||
]);
|
||||
assert.equal(Object.isFrozen(events[0]), true);
|
||||
assert.equal(Object.isFrozen(events[0].session), true);
|
||||
});
|
||||
|
||||
test('terminal Provider lifecycle clears optional fields instead of retaining stale metadata', async () => {
|
||||
const events: NetcattyTerminalSessionEvent[] = [];
|
||||
const requests: NetcattyTerminalProviderRequest[] = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
async providePluginTerminal(request) { requests.push(request); return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent(event) { events.push(event); return []; },
|
||||
});
|
||||
await registry.publishSessionEvent({
|
||||
type: 'created',
|
||||
session: { ...session, cwd: '/srv/app', title: 'Application' },
|
||||
});
|
||||
await registry.publishSessionEvent({ type: 'cwdChanged', session });
|
||||
await registry.publishSessionEvent({ type: 'titleChanged', session });
|
||||
await registry.request({
|
||||
kind: 'terminal.completion',
|
||||
operation: 'provide',
|
||||
session,
|
||||
});
|
||||
assert.equal(Object.hasOwn(events[1].session, 'cwd'), false);
|
||||
assert.equal(Object.hasOwn(events[1].session, 'title'), true);
|
||||
assert.equal(Object.hasOwn(events[2].session, 'cwd'), false);
|
||||
assert.equal(Object.hasOwn(events[2].session, 'title'), false);
|
||||
assert.equal(Object.hasOwn(requests[0].session, 'cwd'), false);
|
||||
assert.equal(Object.hasOwn(requests[0].session, 'title'), false);
|
||||
});
|
||||
|
||||
test('terminal Provider reconnect lifecycle clears omitted connection-scoped metadata', async () => {
|
||||
const events: NetcattyTerminalSessionEvent[] = [];
|
||||
const requests: NetcattyTerminalProviderRequest[] = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
async providePluginTerminal(request) { requests.push(request); return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent(event) { events.push(event); return []; },
|
||||
});
|
||||
await registry.publishSessionEvent({
|
||||
type: 'connected',
|
||||
session: { ...session, cwd: '/srv/app', title: 'Application', alternateScreen: true },
|
||||
});
|
||||
await registry.publishSessionEvent({ type: 'disconnected', session: { ...session, status: 'disconnected' } });
|
||||
await registry.publishSessionEvent({ type: 'reconnected', session });
|
||||
await registry.request({ kind: 'terminal.completion', operation: 'provide', session });
|
||||
|
||||
for (const event of events.slice(1)) {
|
||||
assert.equal(Object.hasOwn(event.session, 'cwd'), false);
|
||||
assert.equal(Object.hasOwn(event.session, 'title'), false);
|
||||
assert.equal(Object.hasOwn(event.session, 'alternateScreen'), false);
|
||||
}
|
||||
assert.equal(Object.hasOwn(requests[0].session, 'cwd'), false);
|
||||
assert.equal(Object.hasOwn(requests[0].session, 'title'), false);
|
||||
assert.equal(Object.hasOwn(requests[0].session, 'alternateScreen'), false);
|
||||
});
|
||||
|
||||
test('terminal Provider requests merge the latest lifecycle snapshot before invocation', async () => {
|
||||
const requests: NetcattyTerminalProviderRequest[] = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
async providePluginTerminal(request) { requests.push(request); return []; },
|
||||
async cancelPluginTerminalRequest() { return false; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
});
|
||||
await registry.publishSessionEvent({
|
||||
type: 'created',
|
||||
session: { ...session, workspaceId: 'workspace-1', title: 'Initial title', cols: 100, rows: 30 },
|
||||
});
|
||||
await registry.request({
|
||||
kind: 'terminal.completion',
|
||||
operation: 'provide',
|
||||
session: { ...session, cwd: '/srv/app' },
|
||||
});
|
||||
assert.deepEqual(requests[0].session, {
|
||||
...session,
|
||||
workspaceId: 'workspace-1',
|
||||
title: 'Initial title',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
cwd: '/srv/app',
|
||||
});
|
||||
assert.equal(Object.isFrozen(requests[0].session), true);
|
||||
});
|
||||
|
||||
test('terminal Provider registry forwards contribution lifecycle invalidation', () => {
|
||||
let bridgeListener: (() => void) | undefined;
|
||||
let disposed = false;
|
||||
const cancellations: string[] = [];
|
||||
const registry = new PluginTerminalProviderRegistry({
|
||||
async listPluginTerminalProviders() { return []; },
|
||||
async providePluginTerminal() { return []; },
|
||||
async cancelPluginTerminalRequest(requestId) { cancellations.push(requestId); return true; },
|
||||
async publishPluginTerminalSessionEvent() { return []; },
|
||||
onPluginContributionsChanged(listener) {
|
||||
bridgeListener = listener;
|
||||
return () => { disposed = true; };
|
||||
},
|
||||
});
|
||||
let changes = 0;
|
||||
const unsubscribe = registry.onDidChangeProviders(() => { changes += 1; });
|
||||
void registry.request({ kind: 'terminal.completion', operation: 'provide', session });
|
||||
bridgeListener?.();
|
||||
assert.equal(changes, 1);
|
||||
assert.equal(cancellations.length, 1);
|
||||
unsubscribe();
|
||||
bridgeListener?.();
|
||||
assert.equal(changes, 1);
|
||||
registry.dispose();
|
||||
assert.equal(disposed, true);
|
||||
});
|
||||
Reference in New Issue
Block a user