[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:
731
components/terminal/pluginTerminalVisualProviderHost.test.ts
Normal file
731
components/terminal/pluginTerminalVisualProviderHost.test.ts
Normal file
@@ -0,0 +1,731 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { PluginTerminalVisualProviderHost } from './pluginTerminalVisualProviderHost.ts';
|
||||
|
||||
test('visual Provider host renders bounded matcher, semantic, prompt, and background output', async () => {
|
||||
const renderedElements: Array<Record<string, unknown>> = [];
|
||||
const decorationOptions: unknown[] = [];
|
||||
const rootChildren: Array<{ remove(): void }> = [];
|
||||
let onWriteParsed: (() => void) | undefined;
|
||||
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement() {
|
||||
const attributes = new Map<string, string>();
|
||||
const element = {
|
||||
className: '',
|
||||
textContent: '',
|
||||
title: '',
|
||||
style: {} as Record<string, string>,
|
||||
setAttribute(name: string, value: string) { attributes.set(name, value); },
|
||||
remove() {
|
||||
const index = rootChildren.indexOf(element);
|
||||
if (index >= 0) rootChildren.splice(index, 1);
|
||||
},
|
||||
};
|
||||
return element;
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const term = {
|
||||
element: {
|
||||
appendChild(element: { remove(): void }) { rootChildren.push(element); },
|
||||
},
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 2,
|
||||
cursorX: 6,
|
||||
getLine(line: number) {
|
||||
if (line === 0) return { isWrapped: false, translateToString(trim: boolean) { return trim ? 'fail' : 'fail'; } };
|
||||
if (line === 1) return { isWrapped: true, translateToString() { return 'ed'; } };
|
||||
if (line === 2) return { isWrapped: false, translateToString() { return 'host$ '; } };
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
onWriteParsed(listener: () => void) {
|
||||
onWriteParsed = listener;
|
||||
return { dispose() {} };
|
||||
},
|
||||
registerMarker() {
|
||||
return { dispose() {} };
|
||||
},
|
||||
registerDecoration(options: unknown) {
|
||||
decorationOptions.push(options);
|
||||
return {
|
||||
dispose() {},
|
||||
onRender(listener: (element: Record<string, unknown>) => void) {
|
||||
const element = {
|
||||
className: '',
|
||||
textContent: '',
|
||||
title: '',
|
||||
style: {},
|
||||
setAttribute() {},
|
||||
};
|
||||
renderedElements.push(element);
|
||||
listener(element);
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const kinds: string[] = [];
|
||||
const matcherPayloads: unknown[] = [];
|
||||
const promptPayloads: unknown[] = [];
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
matcherQuietMs: 1,
|
||||
async request(kind, _operation, payload) {
|
||||
kinds.push(kind);
|
||||
if (kind === 'terminal.semantic') {
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'semantic', status: 'ok', result: {
|
||||
classification: 'deployment',
|
||||
destructive: true,
|
||||
annotations: [{ text: 'production', color: '#ff0000' }],
|
||||
} }],
|
||||
};
|
||||
}
|
||||
if (kind === 'terminal.prompt') {
|
||||
promptPayloads.push(payload);
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'prompt', status: 'ok', result: {
|
||||
annotations: [{ text: 'venv', color: '#00ff00' }],
|
||||
} }],
|
||||
};
|
||||
}
|
||||
if (kind === 'terminal.matcher') {
|
||||
matcherPayloads.push(payload);
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'matcher', status: 'ok', result: {
|
||||
matches: [{ lineId: '0:1', start: 0, length: 6, label: 'Failure', severity: 'error' }],
|
||||
} }],
|
||||
};
|
||||
}
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'background', status: 'ok', result: {
|
||||
layers: [{ id: 'tint', color: '#102030', opacity: 0.2 }],
|
||||
} }],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await host.commandSubmitted('deploy');
|
||||
await host.commandCompleted();
|
||||
onWriteParsed?.();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
assert.deepEqual(kinds.sort(), [
|
||||
'terminal.background',
|
||||
'terminal.matcher',
|
||||
'terminal.prompt',
|
||||
'terminal.semantic',
|
||||
]);
|
||||
assert.equal(rootChildren.length, 1);
|
||||
assert.deepEqual(matcherPayloads, [{
|
||||
lines: [
|
||||
{ lineId: '0:1', line: 'failed', bufferLineNumber: 1 },
|
||||
{ lineId: '2:2', line: 'host$ ', bufferLineNumber: 3 },
|
||||
],
|
||||
}]);
|
||||
assert.deepEqual(promptPayloads, [{
|
||||
reason: 'commandCompleted',
|
||||
promptLine: 'host$',
|
||||
bufferLineNumber: 3,
|
||||
}]);
|
||||
assert.equal(decorationOptions.length, 3);
|
||||
assert.equal(renderedElements.filter((element) => element.title === 'Failure').length, 2);
|
||||
assert.equal(renderedElements.some((element) => (
|
||||
element.textContent === '[destructive] deployment | production | venv'
|
||||
)), true);
|
||||
assert.equal(renderedElements.some((element) => element.title === 'Failure'), true);
|
||||
|
||||
host.setActive(false);
|
||||
assert.equal(rootChildren.length, 0);
|
||||
host.dispose();
|
||||
} finally {
|
||||
if (originalDocument) Object.defineProperty(globalThis, 'document', originalDocument);
|
||||
else Reflect.deleteProperty(globalThis, 'document');
|
||||
}
|
||||
});
|
||||
|
||||
test('visual Provider host preserves semantics when command completion wins the Provider race', async () => {
|
||||
let resolveSemantic: ((value: {
|
||||
stale: boolean;
|
||||
results: Array<{ providerId: string; status: string; result: unknown }>;
|
||||
}) => void) | undefined;
|
||||
const renderedText: string[] = [];
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: { active: { type: 'normal', baseY: 0, cursorY: 0, getLine() { return undefined; } } },
|
||||
onWriteParsed() { return { dispose() {} }; },
|
||||
registerMarker() { return { dispose() {} }; },
|
||||
registerDecoration() {
|
||||
return {
|
||||
dispose() {},
|
||||
onRender(listener: (element: Record<string, unknown>) => void) {
|
||||
const element = {
|
||||
className: '',
|
||||
textContent: '',
|
||||
title: '',
|
||||
style: {},
|
||||
setAttribute() {},
|
||||
};
|
||||
listener(element);
|
||||
renderedText.push(String(element.textContent));
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
isProviderAvailable: (kind) => kind === 'terminal.semantic',
|
||||
request() {
|
||||
return new Promise((resolve) => { resolveSemantic = resolve; });
|
||||
},
|
||||
});
|
||||
const submitted = host.commandSubmitted('true');
|
||||
const completed = host.commandCompleted();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.deepEqual(renderedText, []);
|
||||
resolveSemantic?.({
|
||||
stale: false,
|
||||
results: [{
|
||||
providerId: 'semantic',
|
||||
status: 'ok',
|
||||
result: { classification: 'success', annotations: [{ text: 'fast command' }] },
|
||||
}],
|
||||
});
|
||||
await Promise.all([submitted, completed]);
|
||||
assert.deepEqual(renderedText, ['success | fast command']);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host restores annotations after terminal history is rebuilt', async () => {
|
||||
const markers: Array<{ line: number; isDisposed: boolean; dispose(): void }> = [];
|
||||
const renderedText: string[] = [];
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: { active: { type: 'normal', baseY: 4, cursorY: 2, cursorX: 0, getLine() { return undefined; } } },
|
||||
onWriteParsed() { return { dispose() {} }; },
|
||||
registerMarker(offset: number) {
|
||||
const marker = {
|
||||
line: 6 + offset,
|
||||
isDisposed: false,
|
||||
dispose() { this.isDisposed = true; this.line = -1; },
|
||||
};
|
||||
markers.push(marker);
|
||||
return marker;
|
||||
},
|
||||
registerDecoration() {
|
||||
return {
|
||||
dispose() {},
|
||||
onRender(listener: (element: Record<string, unknown>) => void) {
|
||||
const element = {
|
||||
className: '',
|
||||
textContent: '',
|
||||
title: '',
|
||||
style: {},
|
||||
setAttribute() {},
|
||||
};
|
||||
listener(element);
|
||||
renderedText.push(String(element.textContent));
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
isProviderAvailable: (kind) => kind === 'terminal.semantic',
|
||||
async request() {
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'semantic', status: 'ok', result: {
|
||||
classification: 'success',
|
||||
annotations: [{ text: 'retained annotation' }],
|
||||
} }],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await host.commandSubmitted('true');
|
||||
await host.commandCompleted();
|
||||
assert.deepEqual(renderedText, ['success | retained annotation']);
|
||||
markers[0].dispose();
|
||||
|
||||
host.terminalRebuilt();
|
||||
assert.deepEqual(renderedText, [
|
||||
'success | retained annotation',
|
||||
'success | retained annotation',
|
||||
]);
|
||||
assert.equal(markers[1].line, 6);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host preserves semantic queue order at its bounded capacity', async () => {
|
||||
let requests = 0;
|
||||
const renderedText: string[] = [];
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: { active: { type: 'normal', baseY: 0, cursorY: 0, cursorX: 0, getLine() { return undefined; } } },
|
||||
onWriteParsed() { return { dispose() {} }; },
|
||||
registerMarker() { return { dispose() {} }; },
|
||||
registerDecoration() {
|
||||
return {
|
||||
dispose() {},
|
||||
onRender(listener: (element: Record<string, unknown>) => void) {
|
||||
const element = { className: '', textContent: '', title: '', style: {}, setAttribute() {} };
|
||||
listener(element);
|
||||
renderedText.push(String(element.textContent));
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
isProviderAvailable: (kind) => kind === 'terminal.semantic',
|
||||
async request() {
|
||||
const index = requests++;
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'semantic', status: 'ok', result: { classification: `command-${index}` } }],
|
||||
};
|
||||
},
|
||||
});
|
||||
await Promise.all(Array.from({ length: 65 }, (_, index) => host.commandSubmitted(`echo ${index}`)));
|
||||
assert.equal(requests, 64);
|
||||
await host.commandCompleted();
|
||||
assert.deepEqual(renderedText, ['command-0']);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host never labels the last output line as a shell prompt', async () => {
|
||||
const promptPayloads: unknown[] = [];
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 0,
|
||||
cursorX: 14,
|
||||
getLine() {
|
||||
return { isWrapped: false, translateToString() { return 'command failed'; } };
|
||||
},
|
||||
},
|
||||
},
|
||||
onWriteParsed() { return { dispose() {} }; },
|
||||
registerMarker() { return null; },
|
||||
registerDecoration() { return undefined; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
isProviderAvailable: (kind) => kind === 'terminal.prompt',
|
||||
async request(_kind, _operation, payload) {
|
||||
promptPayloads.push(payload);
|
||||
return { stale: false, results: [] };
|
||||
},
|
||||
});
|
||||
await host.commandCompleted();
|
||||
assert.deepEqual(promptPayloads, [{ reason: 'commandCompleted' }]);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host bounds activation and permission waits end to end', async () => {
|
||||
const signals: AbortSignal[] = [];
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 0,
|
||||
getLine() { return { translateToString() { return 'failed'; } }; },
|
||||
},
|
||||
},
|
||||
onWriteParsed() { return { dispose() {} }; },
|
||||
registerMarker() { return null; },
|
||||
registerDecoration() { return undefined; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
providerResponseTimeoutMs: 5,
|
||||
async request(_kind, _operation, _payload, _deadlineMs, _supersessionKey, signal) {
|
||||
if (signal) signals.push(signal);
|
||||
return new Promise(() => {});
|
||||
},
|
||||
});
|
||||
const result = await Promise.race([
|
||||
host.commandSubmitted('deploy').then(() => 'completed'),
|
||||
new Promise<'timed-out'>((resolve) => setTimeout(() => resolve('timed-out'), 100)),
|
||||
]);
|
||||
assert.equal(result, 'completed');
|
||||
assert.equal(signals.length, 2);
|
||||
assert.equal(signals.every((signal) => signal.aborted), true);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider background refresh pauses while hidden and under reduced motion', async () => {
|
||||
let requests = 0;
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 0,
|
||||
getLine() { return { translateToString() { return 'prompt'; } }; },
|
||||
},
|
||||
},
|
||||
onWriteParsed() { return { dispose() {} }; },
|
||||
registerMarker() { return null; },
|
||||
registerDecoration() { return undefined; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
reducedMotion: true,
|
||||
async request(kind) {
|
||||
if (kind === 'terminal.background') requests += 1;
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'background', status: 'ok', result: {
|
||||
layers: [],
|
||||
refreshAfterMs: 250,
|
||||
} }],
|
||||
};
|
||||
},
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(requests, 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
assert.equal(requests, 1);
|
||||
host.setVisible(false);
|
||||
await host.refreshBackground('hidden');
|
||||
assert.equal(requests, 1);
|
||||
host.setVisible(true);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(requests, 2);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host aborts and invalidates in-flight visual requests when hidden', async () => {
|
||||
let onWriteParsed: (() => void) | undefined;
|
||||
let resolveBackground: ((value: {
|
||||
stale: false;
|
||||
results: Array<{ providerId: string; status: 'ok'; result: unknown }>;
|
||||
}) => void) | undefined;
|
||||
let resolveMatcher: typeof resolveBackground;
|
||||
const signals = new Map<string, AbortSignal>();
|
||||
const rootChildren: Array<{ remove(): void }> = [];
|
||||
let decorations = 0;
|
||||
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement() {
|
||||
const element = {
|
||||
className: '',
|
||||
style: {} as Record<string, string>,
|
||||
setAttribute() {},
|
||||
remove() {
|
||||
const index = rootChildren.indexOf(element);
|
||||
if (index >= 0) rootChildren.splice(index, 1);
|
||||
},
|
||||
};
|
||||
return element;
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const term = {
|
||||
element: { appendChild(element: { remove(): void }) { rootChildren.push(element); } },
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 0,
|
||||
cursorX: 6,
|
||||
getLine(line: number) {
|
||||
if (line !== 0) return undefined;
|
||||
return { isWrapped: false, translateToString() { return 'failed'; } };
|
||||
},
|
||||
},
|
||||
},
|
||||
onWriteParsed(listener: () => void) { onWriteParsed = listener; return { dispose() {} }; },
|
||||
registerMarker() { return { dispose() {} }; },
|
||||
registerDecoration() { decorations += 1; return { dispose() {}, onRender() {} }; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
matcherQuietMs: 1,
|
||||
request(kind, _operation, _payload, _deadlineMs, _supersessionKey, signal) {
|
||||
if (signal) signals.set(kind, signal);
|
||||
return new Promise((resolve) => {
|
||||
if (kind === 'terminal.background') resolveBackground = resolve;
|
||||
if (kind === 'terminal.matcher') resolveMatcher = resolve;
|
||||
});
|
||||
},
|
||||
});
|
||||
onWriteParsed?.();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
assert.deepEqual([...signals.keys()].sort(), ['terminal.background', 'terminal.matcher']);
|
||||
|
||||
host.setVisible(false);
|
||||
assert.equal(signals.get('terminal.background')?.aborted, true);
|
||||
assert.equal(signals.get('terminal.matcher')?.aborted, true);
|
||||
|
||||
resolveBackground?.({
|
||||
stale: false,
|
||||
results: [{ providerId: 'background', status: 'ok', result: {
|
||||
layers: [{ id: 'late', color: '#102030', opacity: 0.2 }],
|
||||
refreshAfterMs: 250,
|
||||
} }],
|
||||
});
|
||||
resolveMatcher?.({
|
||||
stale: false,
|
||||
results: [{ providerId: 'matcher', status: 'ok', result: {
|
||||
matches: [{ lineId: '0:0', start: 0, length: 6, label: 'Late' }],
|
||||
} }],
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(rootChildren.length, 0);
|
||||
assert.equal(decorations, 0);
|
||||
const requestCount = signals.size;
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
assert.equal(signals.size, requestCount);
|
||||
host.dispose();
|
||||
} finally {
|
||||
if (originalDocument) Object.defineProperty(globalThis, 'document', originalDocument);
|
||||
else Reflect.deleteProperty(globalThis, 'document');
|
||||
}
|
||||
});
|
||||
|
||||
test('visual Provider host keeps the terminal output hot path idle without declared Providers', async () => {
|
||||
let onWriteParsed: (() => void) | undefined;
|
||||
let requests = 0;
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 0,
|
||||
getLine() { return { translateToString() { return 'output'; } }; },
|
||||
},
|
||||
},
|
||||
onWriteParsed(listener: () => void) {
|
||||
onWriteParsed = listener;
|
||||
return { dispose() {} };
|
||||
},
|
||||
registerMarker() { return null; },
|
||||
registerDecoration() { return undefined; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
matcherQuietMs: 1,
|
||||
isProviderAvailable: () => false,
|
||||
async request() { requests += 1; return { stale: false, results: [] }; },
|
||||
});
|
||||
onWriteParsed?.();
|
||||
await host.commandSubmitted('deploy');
|
||||
await host.commandCompleted();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
assert.equal(requests, 0);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host discards normal-buffer matcher results after alternate-screen entry', async () => {
|
||||
let onWriteParsed: (() => void) | undefined;
|
||||
let resolveMatcher: ((value: {
|
||||
stale: boolean;
|
||||
results: Array<{ providerId: string; status: string; result: unknown }>;
|
||||
}) => void) | undefined;
|
||||
let decorations = 0;
|
||||
const active = {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 0,
|
||||
getLine() {
|
||||
return { isWrapped: false, translateToString() { return 'failed'; } };
|
||||
},
|
||||
};
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: { active },
|
||||
onWriteParsed(listener: () => void) {
|
||||
onWriteParsed = listener;
|
||||
return { dispose() {} };
|
||||
},
|
||||
registerMarker() { return { dispose() {} }; },
|
||||
registerDecoration() { decorations += 1; return { dispose() {}, onRender() {} }; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
matcherQuietMs: 1,
|
||||
isProviderAvailable: (kind) => kind === 'terminal.matcher',
|
||||
request() { return new Promise((resolve) => { resolveMatcher = resolve; }); },
|
||||
});
|
||||
onWriteParsed?.();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
active.type = 'alternate';
|
||||
resolveMatcher?.({
|
||||
stale: false,
|
||||
results: [{
|
||||
providerId: 'matcher',
|
||||
status: 'ok',
|
||||
result: { matches: [{ lineId: '0:0', start: 0, length: 6, label: 'Failure' }] },
|
||||
}],
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(decorations, 0);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host invalidates an in-flight matcher immediately when output changes', async () => {
|
||||
let onWriteParsed: (() => void) | undefined;
|
||||
let matcherSignal: AbortSignal | undefined;
|
||||
let decorations = 0;
|
||||
let lineText = 'failed';
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 0,
|
||||
getLine() { return { isWrapped: false, translateToString() { return lineText; } }; },
|
||||
},
|
||||
},
|
||||
onWriteParsed(listener: () => void) { onWriteParsed = listener; return { dispose() {} }; },
|
||||
registerMarker() { return { dispose() {} }; },
|
||||
registerDecoration() { decorations += 1; return { dispose() {}, onRender() {} }; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
matcherQuietMs: 1,
|
||||
isProviderAvailable: (kind) => kind === 'terminal.matcher',
|
||||
request(_kind, _operation, _payload, _deadlineMs, _supersessionKey, signal) {
|
||||
matcherSignal = signal;
|
||||
return new Promise((resolve) => {
|
||||
signal?.addEventListener('abort', () => resolve({ stale: true, results: [] }), { once: true });
|
||||
});
|
||||
},
|
||||
});
|
||||
onWriteParsed?.();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
assert.equal(matcherSignal?.aborted, false);
|
||||
|
||||
lineText = 'new output';
|
||||
onWriteParsed?.();
|
||||
assert.equal(matcherSignal?.aborted, true);
|
||||
assert.equal(decorations, 0);
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
test('visual Provider host reacts to reduced-motion preference changes', async () => {
|
||||
let listener: ((event: { matches: boolean }) => void) | undefined;
|
||||
let removed = false;
|
||||
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
matchMedia() {
|
||||
return {
|
||||
matches: false,
|
||||
addEventListener(_type: string, next: typeof listener) { listener = next; },
|
||||
removeEventListener() { removed = true; },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
let requests = 0;
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: { active: { type: 'normal', baseY: 0, cursorY: 0, getLine() { return undefined; } } },
|
||||
onWriteParsed() { return { dispose() {} }; },
|
||||
registerMarker() { return null; },
|
||||
registerDecoration() { return undefined; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
async request(kind) {
|
||||
if (kind === 'terminal.background') requests += 1;
|
||||
return {
|
||||
stale: false,
|
||||
results: [{ providerId: 'background', status: 'ok', result: {
|
||||
layers: [],
|
||||
refreshAfterMs: 250,
|
||||
} }],
|
||||
};
|
||||
},
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(requests, 1);
|
||||
listener?.({ matches: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
assert.equal(requests, 1);
|
||||
listener?.({ matches: false });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(requests, 2);
|
||||
host.dispose();
|
||||
assert.equal(removed, true);
|
||||
} finally {
|
||||
if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow);
|
||||
else Reflect.deleteProperty(globalThis, 'window');
|
||||
}
|
||||
});
|
||||
|
||||
test('visual Provider host does not expose a truncated wrapped logical-line suffix', async () => {
|
||||
let onWriteParsed: (() => void) | undefined;
|
||||
const matcherPayloads: Array<{ lines?: unknown[] }> = [];
|
||||
const term = {
|
||||
element: null,
|
||||
buffer: {
|
||||
active: {
|
||||
type: 'normal',
|
||||
baseY: 0,
|
||||
cursorY: 256,
|
||||
getLine(line: number) {
|
||||
if (line < 256) {
|
||||
return { isWrapped: true, translateToString() { return 'x'; } };
|
||||
}
|
||||
if (line === 256) {
|
||||
return { isWrapped: false, translateToString() { return 'prompt'; } };
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
onWriteParsed(next: () => void) { onWriteParsed = next; return { dispose() {} }; },
|
||||
registerMarker() { return null; },
|
||||
registerDecoration() { return undefined; },
|
||||
};
|
||||
const host = new PluginTerminalVisualProviderHost({
|
||||
term: term as never,
|
||||
matcherQuietMs: 1,
|
||||
isProviderAvailable: (kind) => kind === 'terminal.matcher',
|
||||
async request(kind, _operation, payload) {
|
||||
if (kind === 'terminal.matcher') matcherPayloads.push(payload);
|
||||
return { stale: false, results: [] };
|
||||
},
|
||||
});
|
||||
onWriteParsed?.();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
assert.deepEqual(matcherPayloads, [{
|
||||
lines: [{ lineId: '256:256', line: 'prompt', bufferLineNumber: 257 }],
|
||||
}]);
|
||||
host.dispose();
|
||||
});
|
||||
Reference in New Issue
Block a user