162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
|
|
import assert from 'node:assert/strict';
|
||
|
|
import test from 'node:test';
|
||
|
|
|
||
|
|
import type { PluginTerminalProviderRegistry } from '../../../application/state/pluginTerminalProviderRegistry.ts';
|
||
|
|
import { provideTerminalCompletions } from './terminalCompletionProviders.ts';
|
||
|
|
|
||
|
|
test('terminal completion adapter merges validated plugin results through the host Provider path', async () => {
|
||
|
|
const calls: unknown[] = [];
|
||
|
|
const registry = {
|
||
|
|
async request(request: unknown) {
|
||
|
|
calls.push(request);
|
||
|
|
return {
|
||
|
|
requestId: 'request-1',
|
||
|
|
stale: false,
|
||
|
|
results: [{
|
||
|
|
pluginId: 'com.example',
|
||
|
|
pluginVersion: '1.0.0',
|
||
|
|
providerId: 'com.example.completion',
|
||
|
|
kind: 'terminal.completion',
|
||
|
|
requestId: 'provider-1',
|
||
|
|
status: 'ok',
|
||
|
|
result: {
|
||
|
|
items: [
|
||
|
|
{ text: 'zzzzunlikely-command', displayText: 'Plugin command', score: 50_000 },
|
||
|
|
{ text: '', score: 100_000 },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
}],
|
||
|
|
} as const;
|
||
|
|
},
|
||
|
|
} as unknown as PluginTerminalProviderRegistry;
|
||
|
|
const results = await provideTerminalCompletions(registry, {
|
||
|
|
input: 'zzzzunlikely',
|
||
|
|
session: { sessionId: 'session-1', protocol: 'ssh', status: 'connected' },
|
||
|
|
hostOs: 'linux',
|
||
|
|
maximum: 8,
|
||
|
|
});
|
||
|
|
assert.equal(calls.length, 1);
|
||
|
|
assert.equal(results[0].text, 'zzzzunlikely-command');
|
||
|
|
assert.equal(results[0].source, 'plugin');
|
||
|
|
assert.equal(results[0].providerId, 'com.example.completion');
|
||
|
|
assert.equal(results.some((item) => item.text === ''), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('terminal completion adapter ignores stale plugin responses', async () => {
|
||
|
|
const registry = {
|
||
|
|
async request() { return { requestId: 'request-1', stale: true, results: [] }; },
|
||
|
|
} as unknown as PluginTerminalProviderRegistry;
|
||
|
|
const results = await provideTerminalCompletions(registry, {
|
||
|
|
input: 'zzzzunlikely',
|
||
|
|
session: { sessionId: 'session-1', protocol: 'ssh', status: 'connected' },
|
||
|
|
hostOs: 'linux',
|
||
|
|
maximum: 8,
|
||
|
|
});
|
||
|
|
assert.equal(results.some((item) => item.source === 'plugin'), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('terminal completion adapter preserves built-in results when the plugin bridge fails', async () => {
|
||
|
|
const registry = {
|
||
|
|
async request() { throw new Error('bridge unavailable'); },
|
||
|
|
} as unknown as PluginTerminalProviderRegistry;
|
||
|
|
const results = await provideTerminalCompletions(registry, {
|
||
|
|
input: 'zzzzunlikely',
|
||
|
|
session: { sessionId: 'session-1', protocol: 'ssh', status: 'connected' },
|
||
|
|
hostOs: 'linux',
|
||
|
|
maximum: 8,
|
||
|
|
});
|
||
|
|
assert.ok(Array.isArray(results));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('terminal completion adapter bounds plugin activation and authorization before returning built-ins', async () => {
|
||
|
|
let signal: AbortSignal | undefined;
|
||
|
|
const registry = {
|
||
|
|
async request(_request: unknown, options?: { signal?: AbortSignal }) {
|
||
|
|
signal = options?.signal;
|
||
|
|
return new Promise(() => {});
|
||
|
|
},
|
||
|
|
} as unknown as PluginTerminalProviderRegistry;
|
||
|
|
const result = await Promise.race([
|
||
|
|
provideTerminalCompletions(registry, {
|
||
|
|
input: 'git ',
|
||
|
|
session: { sessionId: 'session-1', protocol: 'ssh', status: 'connected' },
|
||
|
|
hostOs: 'linux',
|
||
|
|
maximum: 8,
|
||
|
|
pluginResponseTimeoutMs: 10,
|
||
|
|
}),
|
||
|
|
new Promise<'timed-out'>((resolve) => setTimeout(() => resolve('timed-out'), 250)),
|
||
|
|
]);
|
||
|
|
assert.notEqual(result, 'timed-out');
|
||
|
|
assert.ok(Array.isArray(result));
|
||
|
|
assert.equal(signal?.aborted, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('terminal completion adapter aborts and discards plugin results when the host security gate closes', async () => {
|
||
|
|
let providerSignal: AbortSignal | undefined;
|
||
|
|
let resolveProvider: ((value: {
|
||
|
|
requestId: string;
|
||
|
|
stale: false;
|
||
|
|
results: readonly unknown[];
|
||
|
|
}) => void) | undefined;
|
||
|
|
const registry = {
|
||
|
|
request(_request: unknown, options?: { signal?: AbortSignal }) {
|
||
|
|
providerSignal = options?.signal;
|
||
|
|
return new Promise((resolve) => { resolveProvider = resolve as typeof resolveProvider; });
|
||
|
|
},
|
||
|
|
} as unknown as PluginTerminalProviderRegistry;
|
||
|
|
const securityController = new AbortController();
|
||
|
|
const pending = provideTerminalCompletions(registry, {
|
||
|
|
input: 'safe-command',
|
||
|
|
session: { sessionId: 'session-1', protocol: 'ssh', status: 'connected' },
|
||
|
|
hostOs: 'linux',
|
||
|
|
maximum: 8,
|
||
|
|
signal: securityController.signal,
|
||
|
|
});
|
||
|
|
await new Promise((resolve) => setImmediate(resolve));
|
||
|
|
securityController.abort();
|
||
|
|
resolveProvider?.({
|
||
|
|
requestId: 'request-1',
|
||
|
|
stale: false,
|
||
|
|
results: [{
|
||
|
|
providerId: 'com.example.completion',
|
||
|
|
status: 'ok',
|
||
|
|
result: { items: [{ text: 'plugin-result', score: 50_000 }] },
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
const results = await pending;
|
||
|
|
assert.equal(providerSignal?.aborted, true);
|
||
|
|
assert.equal(results.some((item) => item.source === 'plugin'), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('terminal completion adapter preserves built-in snippet metadata on duplicate plugin text', async () => {
|
||
|
|
const registry = {
|
||
|
|
async request() {
|
||
|
|
return {
|
||
|
|
requestId: 'request-1',
|
||
|
|
stale: false,
|
||
|
|
results: [{
|
||
|
|
pluginId: 'com.example',
|
||
|
|
pluginVersion: '1.0.0',
|
||
|
|
providerId: 'com.example.completion',
|
||
|
|
kind: 'terminal.completion',
|
||
|
|
requestId: 'provider-1',
|
||
|
|
status: 'ok',
|
||
|
|
result: { items: [{ text: 'deploy', score: 50_000 }] },
|
||
|
|
}],
|
||
|
|
} as const;
|
||
|
|
},
|
||
|
|
} as unknown as PluginTerminalProviderRegistry;
|
||
|
|
const snippet = { id: 'deploy', label: 'deploy', command: 'kubectl apply -f .' };
|
||
|
|
const results = await provideTerminalCompletions(registry, {
|
||
|
|
input: 'dep',
|
||
|
|
session: { sessionId: 'session-1', protocol: 'ssh', status: 'connected' },
|
||
|
|
hostOs: 'linux',
|
||
|
|
snippets: [snippet],
|
||
|
|
maximum: 8,
|
||
|
|
});
|
||
|
|
const duplicate = results.find((item) => item.text === 'deploy');
|
||
|
|
assert.equal(duplicate?.source, 'snippet');
|
||
|
|
assert.equal(duplicate?.snippet, snippet);
|
||
|
|
assert.equal(results.filter((item) => item.text === 'deploy').length, 1);
|
||
|
|
});
|