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
197 lines
7.5 KiB
TypeScript
197 lines
7.5 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
import type { Host, TerminalSession } from '../types';
|
|
import type { TerminalPopupPayload } from '../domain/systemManager/types';
|
|
import { resolveTerminalPopupHost, resolveTerminalPopupReuseId } from './TerminalPopupPage';
|
|
|
|
const source = readFileSync(new URL('./TerminalPopupPage.tsx', import.meta.url), 'utf8');
|
|
const terminalSource = readFileSync(new URL('./Terminal.tsx', import.meta.url), 'utf8');
|
|
const terminalLayerSource = readFileSync(new URL('./TerminalLayer.tsx', import.meta.url), 'utf8');
|
|
|
|
const vaultHost = (overrides: Partial<Host> = {}): Host => ({
|
|
id: 'host-1',
|
|
label: 'Prod',
|
|
hostname: 'prod.example.com',
|
|
username: 'deploy',
|
|
port: 22,
|
|
protocol: 'ssh',
|
|
tags: [],
|
|
os: 'linux',
|
|
moshEnabled: true,
|
|
...overrides,
|
|
});
|
|
|
|
const sourceSession = (overrides: Partial<TerminalSession> = {}): TerminalSession => ({
|
|
id: 'session-1',
|
|
hostId: 'host-1',
|
|
hostLabel: 'Prod',
|
|
hostname: 'prod.example.com',
|
|
username: 'deploy',
|
|
status: 'connected',
|
|
protocol: 'ssh',
|
|
port: 22,
|
|
moshEnabled: false,
|
|
...overrides,
|
|
});
|
|
|
|
const popupPayload = (sourceSessionValue: TerminalSession): TerminalPopupPayload => ({
|
|
title: 'Prod · docker: api',
|
|
parentSessionId: sourceSessionValue.id,
|
|
sourceSession: sourceSessionValue,
|
|
startupCommand: 'docker exec -it abc123 sh',
|
|
});
|
|
|
|
test('resolveTerminalPopupHost honors source session transport over saved Mosh host settings', () => {
|
|
const host = resolveTerminalPopupHost(popupPayload(sourceSession()), [vaultHost()]);
|
|
|
|
assert.equal(host.id, 'host-1');
|
|
assert.equal(host.hostname, 'prod.example.com');
|
|
assert.equal(host.protocol, 'ssh');
|
|
assert.equal(host.moshEnabled, false);
|
|
assert.equal(host.etEnabled, false);
|
|
});
|
|
|
|
test('resolveTerminalPopupHost still falls back to source session details when the host is missing', () => {
|
|
const host = resolveTerminalPopupHost(popupPayload(sourceSession({ hostId: 'missing' })), []);
|
|
|
|
assert.equal(host.id, 'missing');
|
|
assert.equal(host.label, 'Prod');
|
|
assert.equal(host.protocol, 'ssh');
|
|
assert.equal(host.moshEnabled, false);
|
|
});
|
|
|
|
test('resolveTerminalPopupHost preserves plugin transport and configuration for copied windows', () => {
|
|
const providerId = 'com.example.transport.connection';
|
|
const protocol = `plugin:${providerId}` as const;
|
|
const pluginConnection = {
|
|
providerId,
|
|
authenticationProviderId: 'com.example.transport.auth',
|
|
configuration: { endpoint: 'gateway.example', features: ['importers'] },
|
|
credentialId: 'credential-reference-1234',
|
|
};
|
|
|
|
const host = resolveTerminalPopupHost(
|
|
popupPayload(sourceSession({
|
|
protocol,
|
|
pluginConnection,
|
|
hostId: 'missing-plugin-host',
|
|
hostname: 'provider-placeholder.example',
|
|
})),
|
|
[],
|
|
);
|
|
|
|
assert.equal(host.protocol, protocol);
|
|
assert.deepEqual(host.pluginConnection, pluginConnection);
|
|
assert.notEqual(host.pluginConnection, pluginConnection);
|
|
});
|
|
|
|
test('resolveTerminalPopupHost does not turn command popups into serial sessions without serial config', () => {
|
|
const host = resolveTerminalPopupHost(
|
|
popupPayload(sourceSession({ protocol: 'serial' })),
|
|
[vaultHost({ protocol: 'serial' })],
|
|
);
|
|
|
|
assert.equal(host.protocol, 'ssh');
|
|
assert.equal(host.moshEnabled, false);
|
|
});
|
|
|
|
test('resolveTerminalPopupHost preserves serial settings when attaching a live session', () => {
|
|
const serialConfig = {
|
|
path: '/dev/ttyUSB0',
|
|
baudRate: 115200,
|
|
lineMode: true,
|
|
localEcho: true,
|
|
} as const;
|
|
const payload = popupPayload(sourceSession({
|
|
protocol: 'serial',
|
|
hostname: serialConfig.path,
|
|
port: serialConfig.baudRate,
|
|
serialConfig,
|
|
}));
|
|
payload.attachSessionId = payload.sourceSession.id;
|
|
|
|
const host = resolveTerminalPopupHost(payload, [vaultHost({ protocol: 'serial', serialConfig })]);
|
|
|
|
assert.equal(host.protocol, 'serial');
|
|
assert.deepEqual(host.serialConfig, serialConfig);
|
|
assert.match(source, /serialConfig=\{isAttachMode \? config\.sourceSession\.serialConfig : undefined\}/);
|
|
});
|
|
|
|
test('resolveTerminalPopupReuseId uses the explicit reuse id from the prepared source session', () => {
|
|
assert.equal(
|
|
resolveTerminalPopupReuseId(popupPayload(sourceSession({ reuseConnectionFromSessionId: 'session-1' }))),
|
|
'session-1',
|
|
);
|
|
|
|
assert.equal(
|
|
resolveTerminalPopupReuseId(popupPayload(sourceSession({ reuseConnectionFromSessionId: undefined }))),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test('popup terminals resolve complete host config and pass jump hosts into Terminal', () => {
|
|
assert.match(source, /proxyProfiles,\s+knownHosts,\s+snippets,\s+snippetPackages,\s+groupConfigs,/);
|
|
assert.match(source, /deleteSelectedSnippets,/);
|
|
assert.match(source, /resolveTerminalPopupHost\(config,\s*hosts,\s*\{\s+groupConfigs,\s+proxyProfiles,/);
|
|
assert.match(source, /resolveTerminalChainHosts\(\{\s+host,\s+hosts,\s+groupConfigs,\s+proxyProfiles,/);
|
|
assert.match(source, /chainHosts=\{chainHosts\}/);
|
|
// Popup has no AppSideEffects listener; bulk delete must hit this vault instance.
|
|
assert.match(source, /onDeleteSnippets=\{deleteSelectedSnippets\}/);
|
|
});
|
|
|
|
test('popup terminals use their window-local vault readiness', () => {
|
|
assert.match(source, /vaultInitializedOverride=\{vaultInitialized\}/);
|
|
});
|
|
|
|
test('popup provider tree mounts the plugin authentication host', () => {
|
|
assert.match(source, /import \{ PluginAuthenticationHost \} from '\.\/plugins\/PluginAuthenticationHost';/);
|
|
assert.match(
|
|
source,
|
|
/<I18nProvider locale=\{settings\.uiLanguage\}>\s+<TerminalPopupPageInner\s+settings=\{settings\}\s+allowTerminalStart=\{allowTerminalStart\}\s+\/>\s+<PluginAuthenticationHost \/>\s+<\/I18nProvider>/,
|
|
);
|
|
});
|
|
|
|
test('attach popup close preparation has a bounded timeout', () => {
|
|
assert.match(source, /Promise\.race\(\[/);
|
|
assert.match(source, /Attach close preparation timed out/);
|
|
assert.match(source, /1500/);
|
|
});
|
|
|
|
test('terminal exit auto-close setting reaches tabs, workspaces, and popups', () => {
|
|
assert.match(
|
|
terminalLayerSource,
|
|
/resolveTerminalSessionExitIntent\(\s+evt,\s+terminalSettings\?\.autoCloseOnExit \?\? true,\s+\)/,
|
|
);
|
|
assert.match(
|
|
terminalSource,
|
|
/const onSessionExitRef = useRef\(onSessionExit\);[\s\S]*?useLayoutEffect\(\(\) => \{\s+onSessionExitRef\.current = onSessionExit;\s+\}, \[onSessionExit\]\);/,
|
|
);
|
|
assert.match(
|
|
terminalSource,
|
|
/onSessionExitRef\.current\?\.\(closedSessionId, evt\)/,
|
|
);
|
|
assert.match(
|
|
source,
|
|
/shouldCloseTerminalPopupOnExit\(evt, \{\s+autoCloseOnExit: settings\.terminalSettings\.autoCloseOnExit,\s+isAttachMode,\s+\}\)/,
|
|
);
|
|
assert.match(
|
|
source,
|
|
/shouldRevealTerminalPopupOnExit\(evt, \{\s+autoCloseOnExit: settings\.terminalSettings\.autoCloseOnExit,\s+isAttachMode,\s+\}\)[\s\S]*?revealTerminal\(\);\s+return;[\s\S]*?setStartupError/,
|
|
);
|
|
});
|
|
|
|
test('an attached observe popup never owns automatic reconnect', () => {
|
|
const reconnectStart = terminalSource.indexOf('const scheduleAutoReconnect = useCallback');
|
|
const reconnectEnd = terminalSource.indexOf('const prepareRestoredReconnect', reconnectStart);
|
|
const reconnectSource = terminalSource.slice(reconnectStart, reconnectEnd);
|
|
assert.match(reconnectSource, /if \(attachExistingSession\) return false;/);
|
|
assert.match(reconnectSource, /\[attachExistingSession, host, t, terminalSettings, updateStatus\]/);
|
|
assert.match(terminalSource, /const startReconnect = async[\s\S]*?if \(attachExistingSession\) return;/);
|
|
assert.match(
|
|
terminalSource,
|
|
/useEffect\(\(\) => \{\s+if \(attachExistingSession\) return undefined;\s+return terminalReconnectRegistry\.register/,
|
|
);
|
|
});
|