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
276 lines
9.7 KiB
TypeScript
276 lines
9.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { registerPluginTerminalLinkProvider } from './pluginTerminalLinkProvider.ts';
|
|
|
|
test('plugin terminal link host requests bounded line links and honors activation policy', async () => {
|
|
let provider: { provideLinks(line: number, callback: (links?: unknown[]) => void): void } | undefined;
|
|
let registrationDisposed = false;
|
|
const requests: Array<[string, string, unknown, string | undefined]> = [];
|
|
const opened: string[] = [];
|
|
const term = {
|
|
element: undefined,
|
|
buffer: {
|
|
active: {
|
|
getLine(line: number) {
|
|
if (line !== 0) return undefined;
|
|
return { translateToString() { return 'visit example'; } };
|
|
},
|
|
},
|
|
},
|
|
registerLinkProvider(next: typeof provider) {
|
|
provider = next;
|
|
return { dispose() { registrationDisposed = true; } };
|
|
},
|
|
};
|
|
const disposable = registerPluginTerminalLinkProvider({
|
|
term: term as never,
|
|
async request(kind, operation, payload, _deadlineMs, supersessionKey) {
|
|
requests.push([kind, operation, payload, supersessionKey]);
|
|
if (kind === 'terminal.link') {
|
|
return {
|
|
stale: false,
|
|
results: [{
|
|
providerId: 'com.example.links',
|
|
status: 'ok',
|
|
result: { links: [{ start: 6, length: 7, uri: 'https://example.com', label: 'Example' }] },
|
|
}],
|
|
};
|
|
}
|
|
return { stale: false, results: [] };
|
|
},
|
|
canActivate: (event) => event.metaKey,
|
|
async openExternal(uri) { opened.push(uri); },
|
|
});
|
|
|
|
const links = await new Promise<unknown[]>((resolve) => {
|
|
provider?.provideLinks(1, (value) => resolve(value ?? []));
|
|
}) as Array<{ range: unknown; text: string; activate(event: { metaKey: boolean }): void }>;
|
|
|
|
assert.deepEqual(requests.map(([kind, operation]) => [kind, operation]), [
|
|
['terminal.link', 'provideLinks'],
|
|
['terminal.hover', 'provideHovers'],
|
|
]);
|
|
assert.deepEqual(requests.map((request) => request[3]), ['line:1', 'line:1']);
|
|
assert.equal(links[0]?.text, 'example');
|
|
assert.deepEqual(links[0]?.range, {
|
|
start: { x: 7, y: 1 },
|
|
end: { x: 13, y: 1 },
|
|
});
|
|
links[0]?.activate({ metaKey: false });
|
|
links[0]?.activate({ metaKey: true });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(opened, ['https://example.com']);
|
|
|
|
disposable.dispose();
|
|
assert.equal(registrationDisposed, true);
|
|
});
|
|
|
|
test('plugin terminal link host suppresses stale and oversized physical lines', async () => {
|
|
let provider: { provideLinks(line: number, callback: (links?: unknown[]) => void): void } | undefined;
|
|
let requestCount = 0;
|
|
const term = {
|
|
element: undefined,
|
|
buffer: { active: { getLine() { return { translateToString() { return 'x'.repeat(8_193); } }; } } },
|
|
registerLinkProvider(next: typeof provider) {
|
|
provider = next;
|
|
return { dispose() {} };
|
|
},
|
|
};
|
|
registerPluginTerminalLinkProvider({
|
|
term: term as never,
|
|
async request() {
|
|
requestCount += 1;
|
|
return { stale: true, results: [] };
|
|
},
|
|
canActivate: () => true,
|
|
async openExternal() {},
|
|
});
|
|
|
|
const links = await new Promise<unknown[]>((resolve) => {
|
|
provider?.provideLinks(1, (value) => resolve(value ?? []));
|
|
});
|
|
assert.deepEqual(links, []);
|
|
assert.equal(requestCount, 0);
|
|
});
|
|
|
|
test('plugin terminal link host releases xterm when activation or permission work stalls', async () => {
|
|
let provider: { provideLinks(line: number, callback: (links?: unknown[]) => void): void } | undefined;
|
|
const signals: AbortSignal[] = [];
|
|
const term = {
|
|
element: undefined,
|
|
buffer: { active: { getLine() { return { translateToString() { return 'example'; } }; } } },
|
|
registerLinkProvider(next: typeof provider) {
|
|
provider = next;
|
|
return { dispose() {} };
|
|
},
|
|
};
|
|
registerPluginTerminalLinkProvider({
|
|
term: term as never,
|
|
request: (_kind, _operation, _payload, _deadlineMs, _supersessionKey, signal) => {
|
|
if (signal) signals.push(signal);
|
|
return new Promise(() => {});
|
|
},
|
|
canActivate: () => true,
|
|
async openExternal() {},
|
|
responseTimeoutMs: 5,
|
|
});
|
|
|
|
const links = await new Promise<unknown[]>((resolve) => {
|
|
provider?.provideLinks(1, (value) => resolve(value ?? []));
|
|
});
|
|
assert.deepEqual(links, []);
|
|
assert.equal(signals.length, 2);
|
|
assert.equal(signals.every((signal) => signal.aborted), true);
|
|
});
|
|
|
|
test('plugin terminal link host aborts and suppresses in-flight scrollback on hide or disconnect', async () => {
|
|
let provider: { provideLinks(line: number, callback: (links?: unknown[]) => void): void } | undefined;
|
|
const signals: AbortSignal[] = [];
|
|
let requests = 0;
|
|
const term = {
|
|
element: undefined,
|
|
buffer: { active: { getLine() { return { translateToString() { return 'secret scrollback'; } }; } } },
|
|
registerLinkProvider(next: typeof provider) {
|
|
provider = next;
|
|
return { dispose() {} };
|
|
},
|
|
};
|
|
const host = registerPluginTerminalLinkProvider({
|
|
term: term as never,
|
|
request: (_kind, _operation, _payload, _deadlineMs, _supersessionKey, signal) => {
|
|
requests += 1;
|
|
if (signal) signals.push(signal);
|
|
return new Promise((resolve) => {
|
|
signal?.addEventListener('abort', () => resolve({ stale: true, results: [] }), { once: true });
|
|
});
|
|
},
|
|
canActivate: () => true,
|
|
async openExternal() {},
|
|
});
|
|
|
|
const hiddenResult = new Promise<unknown[]>((resolve) => {
|
|
provider?.provideLinks(1, (value) => resolve(value ?? []));
|
|
});
|
|
host.setVisible(false);
|
|
assert.deepEqual(await hiddenResult, []);
|
|
assert.equal(signals.length, 2);
|
|
assert.equal(signals.every((signal) => signal.aborted), true);
|
|
provider?.provideLinks(1, (value) => assert.equal(value, undefined));
|
|
assert.equal(requests, 2);
|
|
|
|
host.setVisible(true);
|
|
const disconnectedResult = new Promise<unknown[]>((resolve) => {
|
|
provider?.provideLinks(1, (value) => resolve(value ?? []));
|
|
});
|
|
host.setActive(false);
|
|
assert.deepEqual(await disconnectedResult, []);
|
|
assert.equal(signals.slice(2).every((signal) => signal.aborted), true);
|
|
provider?.provideLinks(1, (value) => assert.equal(value, undefined));
|
|
assert.equal(requests, 4);
|
|
});
|
|
|
|
test('plugin terminal link host performs no request when neither Provider kind is declared', async () => {
|
|
let provider: { provideLinks(line: number, callback: (links?: unknown[]) => void): void } | undefined;
|
|
let requests = 0;
|
|
const term = {
|
|
element: undefined,
|
|
buffer: { active: { getLine() { return { translateToString() { return 'example'; } }; } } },
|
|
registerLinkProvider(next: typeof provider) {
|
|
provider = next;
|
|
return { dispose() {} };
|
|
},
|
|
};
|
|
registerPluginTerminalLinkProvider({
|
|
term: term as never,
|
|
async request() { requests += 1; return { stale: false, results: [] }; },
|
|
isProviderAvailable: () => false,
|
|
canActivate: () => true,
|
|
async openExternal() {},
|
|
});
|
|
const links = await new Promise<unknown[]>((resolve) => {
|
|
provider?.provideLinks(1, (value) => resolve(value ?? []));
|
|
});
|
|
assert.deepEqual(links, []);
|
|
assert.equal(requests, 0);
|
|
});
|
|
|
|
test('plugin terminal link host rejects async results after the buffer line changes', async () => {
|
|
let provider: { provideLinks(line: number, callback: (links?: unknown[]) => void): void } | undefined;
|
|
let lineText = 'visit example';
|
|
let resolveLinks: ((value: {
|
|
stale: false;
|
|
results: Array<{ providerId: string; status: 'ok'; result: unknown }>;
|
|
}) => void) | undefined;
|
|
const term = {
|
|
element: undefined,
|
|
buffer: { active: { getLine() { return { translateToString() { return lineText; } }; } } },
|
|
registerLinkProvider(next: typeof provider) {
|
|
provider = next;
|
|
return { dispose() {} };
|
|
},
|
|
};
|
|
registerPluginTerminalLinkProvider({
|
|
term: term as never,
|
|
request(kind) {
|
|
if (kind === 'terminal.hover') return Promise.resolve({ stale: false, results: [] });
|
|
return new Promise((resolve) => { resolveLinks = resolve; });
|
|
},
|
|
canActivate: () => true,
|
|
async openExternal() {},
|
|
});
|
|
|
|
const result = new Promise<unknown[]>((resolve) => {
|
|
provider?.provideLinks(1, (links) => resolve(links ?? []));
|
|
});
|
|
lineText = 'unrelated output';
|
|
resolveLinks?.({
|
|
stale: false,
|
|
results: [{
|
|
providerId: 'com.example.links',
|
|
status: 'ok',
|
|
result: { links: [{ start: 6, length: 7, uri: 'https://example.com' }] },
|
|
}],
|
|
});
|
|
assert.deepEqual(await result, []);
|
|
});
|
|
|
|
test('plugin terminal link activation revalidates the accepted buffer line', async () => {
|
|
let provider: { provideLinks(line: number, callback: (links?: unknown[]) => void): void } | undefined;
|
|
let lineText = 'visit example';
|
|
const opened: string[] = [];
|
|
const term = {
|
|
element: undefined,
|
|
buffer: { active: { getLine() { return { translateToString() { return lineText; } }; } } },
|
|
registerLinkProvider(next: typeof provider) {
|
|
provider = next;
|
|
return { dispose() {} };
|
|
},
|
|
};
|
|
registerPluginTerminalLinkProvider({
|
|
term: term as never,
|
|
async request(kind) {
|
|
return kind === 'terminal.link'
|
|
? {
|
|
stale: false,
|
|
results: [{
|
|
providerId: 'com.example.links',
|
|
status: 'ok',
|
|
result: { links: [{ start: 6, length: 7, uri: 'https://example.com' }] },
|
|
}],
|
|
}
|
|
: { stale: false, results: [] };
|
|
},
|
|
canActivate: () => true,
|
|
async openExternal(uri) { opened.push(uri); },
|
|
});
|
|
|
|
const links = await new Promise<Array<{ activate(event: MouseEvent): void }>>((resolve) => {
|
|
provider?.provideLinks(1, (value) => resolve((value ?? []) as Array<{ activate(event: MouseEvent): void }>));
|
|
});
|
|
lineText = 'unrelated output';
|
|
links[0]?.activate({} as MouseEvent);
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.deepEqual(opened, []);
|
|
});
|