mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
Refactor managed execution state for virtual backgrounding
This commit is contained in:
@@ -313,9 +313,9 @@ export const useGeminiStream = (
|
|||||||
|
|
||||||
const activeToolPtyId = useMemo(() => {
|
const activeToolPtyId = useMemo(() => {
|
||||||
const executingBackgroundableTool = toolCalls.find(
|
const executingBackgroundableTool = toolCalls.find(
|
||||||
(tc): tc is TrackedExecutingToolCall =>
|
(toolCall): toolCall is TrackedExecutingToolCall =>
|
||||||
tc.status === CoreToolCallStatus.Executing &&
|
toolCall.status === CoreToolCallStatus.Executing &&
|
||||||
typeof tc.pid === 'number',
|
typeof toolCall.pid === 'number',
|
||||||
);
|
);
|
||||||
return executingBackgroundableTool?.pid;
|
return executingBackgroundableTool?.pid;
|
||||||
}, [toolCalls]);
|
}, [toolCalls]);
|
||||||
|
|||||||
@@ -432,13 +432,21 @@ describe('ShellExecutionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('pty interaction', () => {
|
describe('pty interaction', () => {
|
||||||
|
let activePtysGetSpy: { mockRestore: () => void };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(ShellExecutionService['activePtys'], 'get').mockReturnValue({
|
activePtysGetSpy = vi
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
.spyOn(ShellExecutionService['activePtys'], 'get')
|
||||||
ptyProcess: mockPtyProcess as any,
|
.mockReturnValue({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
headlessTerminal: mockHeadlessTerminal as any,
|
ptyProcess: mockPtyProcess as any,
|
||||||
});
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
headlessTerminal: mockHeadlessTerminal as any,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
activePtysGetSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write to the pty and trigger a render', async () => {
|
it('should write to the pty and trigger a render', async () => {
|
||||||
@@ -1633,3 +1641,85 @@ describe('ShellExecutionService environment variables', () => {
|
|||||||
await new Promise(process.nextTick);
|
await new Promise(process.nextTick);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ShellExecutionService virtual executions', () => {
|
||||||
|
it('completes a virtual execution in the foreground', async () => {
|
||||||
|
const { pid, result } = ShellExecutionService.createVirtualExecution();
|
||||||
|
if (pid === undefined) {
|
||||||
|
throw new Error('Expected virtual pid to be defined.');
|
||||||
|
}
|
||||||
|
const onExit = vi.fn();
|
||||||
|
const unsubscribe = ShellExecutionService.onExit(pid, onExit);
|
||||||
|
|
||||||
|
ShellExecutionService.appendVirtualOutput(pid, 'Hello');
|
||||||
|
ShellExecutionService.appendVirtualOutput(pid, ' World');
|
||||||
|
ShellExecutionService.completeVirtualExecution(pid, { exitCode: 0 });
|
||||||
|
|
||||||
|
const executionResult = await result;
|
||||||
|
|
||||||
|
expect(executionResult.output).toBe('Hello World');
|
||||||
|
expect(executionResult.backgrounded).toBeUndefined();
|
||||||
|
expect(executionResult.exitCode).toBe(0);
|
||||||
|
expect(executionResult.error).toBeNull();
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(onExit).toHaveBeenCalledWith(0, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports backgrounding virtual executions and streaming additional output', async () => {
|
||||||
|
const { pid, result } = ShellExecutionService.createVirtualExecution();
|
||||||
|
if (pid === undefined) {
|
||||||
|
throw new Error('Expected virtual pid to be defined.');
|
||||||
|
}
|
||||||
|
const chunks: string[] = [];
|
||||||
|
const onExit = vi.fn();
|
||||||
|
|
||||||
|
const unsubscribeStream = ShellExecutionService.subscribe(pid, (event) => {
|
||||||
|
if (event.type === 'data' && typeof event.chunk === 'string') {
|
||||||
|
chunks.push(event.chunk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const unsubscribeExit = ShellExecutionService.onExit(pid, onExit);
|
||||||
|
|
||||||
|
ShellExecutionService.appendVirtualOutput(pid, 'Chunk 1');
|
||||||
|
ShellExecutionService.background(pid);
|
||||||
|
|
||||||
|
const backgroundResult = await result;
|
||||||
|
expect(backgroundResult.backgrounded).toBe(true);
|
||||||
|
expect(backgroundResult.output).toBe('Chunk 1');
|
||||||
|
|
||||||
|
ShellExecutionService.appendVirtualOutput(pid, '\nChunk 2');
|
||||||
|
ShellExecutionService.completeVirtualExecution(pid, { exitCode: 0 });
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(chunks.join('')).toContain('Chunk 2');
|
||||||
|
expect(onExit).toHaveBeenCalledWith(0, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
unsubscribeStream();
|
||||||
|
unsubscribeExit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('kills virtual executions via the existing kill API', async () => {
|
||||||
|
const onKill = vi.fn();
|
||||||
|
const { pid, result } = ShellExecutionService.createVirtualExecution(
|
||||||
|
'',
|
||||||
|
onKill,
|
||||||
|
);
|
||||||
|
if (pid === undefined) {
|
||||||
|
throw new Error('Expected virtual pid to be defined.');
|
||||||
|
}
|
||||||
|
|
||||||
|
ShellExecutionService.appendVirtualOutput(pid, 'work');
|
||||||
|
ShellExecutionService.kill(pid);
|
||||||
|
|
||||||
|
const killResult = await result;
|
||||||
|
expect(onKill).toHaveBeenCalledTimes(1);
|
||||||
|
expect(killResult.aborted).toBe(true);
|
||||||
|
expect(killResult.exitCode).toBe(130);
|
||||||
|
expect(killResult.error?.message).toContain('Operation cancelled by user');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -152,11 +152,22 @@ interface ActiveChildProcess {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActiveVirtualProcess {
|
interface ActiveVirtualProcessState {
|
||||||
output: string;
|
output: string;
|
||||||
onKill?: () => void;
|
onKill?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActiveManagedProcess =
|
||||||
|
| {
|
||||||
|
kind: 'child';
|
||||||
|
process: ChildProcess;
|
||||||
|
state: ActiveChildProcess['state'];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: 'virtual';
|
||||||
|
state: ActiveVirtualProcessState;
|
||||||
|
};
|
||||||
|
|
||||||
const getFullBufferText = (terminal: pkg.Terminal): string => {
|
const getFullBufferText = (terminal: pkg.Terminal): string => {
|
||||||
const buffer = terminal.buffer.active;
|
const buffer = terminal.buffer.active;
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
@@ -202,11 +213,7 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
|
|||||||
|
|
||||||
export class ShellExecutionService {
|
export class ShellExecutionService {
|
||||||
private static activePtys = new Map<number, ActivePty>();
|
private static activePtys = new Map<number, ActivePty>();
|
||||||
private static activeChildProcesses = new Map<number, ActiveChildProcess>();
|
private static activeProcesses = new Map<number, ActiveManagedProcess>();
|
||||||
private static activeVirtualProcesses = new Map<
|
|
||||||
number,
|
|
||||||
ActiveVirtualProcess
|
|
||||||
>();
|
|
||||||
private static exitedPtyInfo = new Map<
|
private static exitedPtyInfo = new Map<
|
||||||
number,
|
number,
|
||||||
{ exitCode: number; signal?: number }
|
{ exitCode: number; signal?: number }
|
||||||
@@ -302,13 +309,29 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static getActiveChildProcess(
|
||||||
|
pid: number,
|
||||||
|
): ActiveChildProcess | undefined {
|
||||||
|
const activeProcess = this.activeProcesses.get(pid);
|
||||||
|
if (activeProcess?.kind !== 'child') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return { process: activeProcess.process, state: activeProcess.state };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getActiveVirtualProcess(
|
||||||
|
pid: number,
|
||||||
|
): ActiveVirtualProcessState | undefined {
|
||||||
|
const activeProcess = this.activeProcesses.get(pid);
|
||||||
|
if (activeProcess?.kind !== 'virtual') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return activeProcess.state;
|
||||||
|
}
|
||||||
|
|
||||||
private static allocateVirtualPid(): number {
|
private static allocateVirtualPid(): number {
|
||||||
let pid = ++this.nextVirtualPid;
|
let pid = ++this.nextVirtualPid;
|
||||||
while (
|
while (this.activePtys.has(pid) || this.activeProcesses.has(pid)) {
|
||||||
this.activePtys.has(pid) ||
|
|
||||||
this.activeChildProcesses.has(pid) ||
|
|
||||||
this.activeVirtualProcesses.has(pid)
|
|
||||||
) {
|
|
||||||
pid = ++this.nextVirtualPid;
|
pid = ++this.nextVirtualPid;
|
||||||
}
|
}
|
||||||
return pid;
|
return pid;
|
||||||
@@ -319,9 +342,12 @@ export class ShellExecutionService {
|
|||||||
onKill?: () => void,
|
onKill?: () => void,
|
||||||
): ShellExecutionHandle {
|
): ShellExecutionHandle {
|
||||||
const pid = this.allocateVirtualPid();
|
const pid = this.allocateVirtualPid();
|
||||||
this.activeVirtualProcesses.set(pid, {
|
this.activeProcesses.set(pid, {
|
||||||
output: initialOutput,
|
kind: 'virtual',
|
||||||
onKill,
|
state: {
|
||||||
|
output: initialOutput,
|
||||||
|
onKill,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = new Promise<ShellExecutionResult>((resolve) => {
|
const result = new Promise<ShellExecutionResult>((resolve) => {
|
||||||
@@ -332,7 +358,7 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static appendVirtualOutput(pid: number, chunk: string): void {
|
static appendVirtualOutput(pid: number, chunk: string): void {
|
||||||
const virtual = this.activeVirtualProcesses.get(pid);
|
const virtual = this.getActiveVirtualProcess(pid);
|
||||||
if (!virtual || chunk.length === 0) {
|
if (!virtual || chunk.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -349,7 +375,7 @@ export class ShellExecutionService {
|
|||||||
aborted?: boolean;
|
aborted?: boolean;
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
const virtual = this.activeVirtualProcesses.get(pid);
|
const virtual = this.getActiveVirtualProcess(pid);
|
||||||
if (!virtual) {
|
if (!virtual) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -382,7 +408,7 @@ export class ShellExecutionService {
|
|||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
this.activeListeners.delete(pid);
|
this.activeListeners.delete(pid);
|
||||||
this.activeVirtualProcesses.delete(pid);
|
this.activeProcesses.delete(pid);
|
||||||
|
|
||||||
this.exitedPtyInfo.set(pid, {
|
this.exitedPtyInfo.set(pid, {
|
||||||
exitCode: exitCode ?? 0,
|
exitCode: exitCode ?? 0,
|
||||||
@@ -432,7 +458,8 @@ export class ShellExecutionService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (child.pid) {
|
if (child.pid) {
|
||||||
this.activeChildProcesses.set(child.pid, {
|
this.activeProcesses.set(child.pid, {
|
||||||
|
kind: 'child',
|
||||||
process: child,
|
process: child,
|
||||||
state,
|
state,
|
||||||
});
|
});
|
||||||
@@ -542,7 +569,7 @@ export class ShellExecutionService {
|
|||||||
onOutputEvent(event);
|
onOutputEvent(event);
|
||||||
ShellExecutionService.emitEvent(child.pid, event);
|
ShellExecutionService.emitEvent(child.pid, event);
|
||||||
|
|
||||||
this.activeChildProcesses.delete(child.pid);
|
this.activeProcesses.delete(child.pid);
|
||||||
this.activeResolvers.delete(child.pid);
|
this.activeResolvers.delete(child.pid);
|
||||||
this.activeListeners.delete(child.pid);
|
this.activeListeners.delete(child.pid);
|
||||||
}
|
}
|
||||||
@@ -1018,11 +1045,9 @@ export class ShellExecutionService {
|
|||||||
* @param input The string to write to the terminal.
|
* @param input The string to write to the terminal.
|
||||||
*/
|
*/
|
||||||
static writeToPty(pid: number, input: string): void {
|
static writeToPty(pid: number, input: string): void {
|
||||||
if (this.activeChildProcesses.has(pid)) {
|
const activeChild = this.getActiveChildProcess(pid);
|
||||||
const activeChild = this.activeChildProcesses.get(pid);
|
if (activeChild) {
|
||||||
if (activeChild) {
|
activeChild.process.stdin?.write(input);
|
||||||
activeChild.process.stdin?.write(input);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1037,11 +1062,11 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static isPtyActive(pid: number): boolean {
|
static isPtyActive(pid: number): boolean {
|
||||||
if (this.activeVirtualProcesses.has(pid)) {
|
if (this.getActiveVirtualProcess(pid)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activeChildProcesses.has(pid)) {
|
if (this.getActiveChildProcess(pid)) {
|
||||||
try {
|
try {
|
||||||
return process.kill(pid, 0);
|
return process.kill(pid, 0);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1079,8 +1104,10 @@ export class ShellExecutionService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
return () => disposable.dispose();
|
return () => disposable.dispose();
|
||||||
} else if (this.activeChildProcesses.has(pid)) {
|
}
|
||||||
const activeChild = this.activeChildProcesses.get(pid);
|
|
||||||
|
const activeChild = this.getActiveChildProcess(pid);
|
||||||
|
if (activeChild) {
|
||||||
const listener = (code: number | null, signal: NodeJS.Signals | null) => {
|
const listener = (code: number | null, signal: NodeJS.Signals | null) => {
|
||||||
let signalNumber: number | undefined;
|
let signalNumber: number | undefined;
|
||||||
if (signal) {
|
if (signal) {
|
||||||
@@ -1092,7 +1119,9 @@ export class ShellExecutionService {
|
|||||||
return () => {
|
return () => {
|
||||||
activeChild?.process.removeListener('exit', listener);
|
activeChild?.process.removeListener('exit', listener);
|
||||||
};
|
};
|
||||||
} else if (this.activeVirtualProcesses.has(pid)) {
|
}
|
||||||
|
|
||||||
|
if (this.getActiveVirtualProcess(pid)) {
|
||||||
const listener = (event: ShellOutputEvent) => {
|
const listener = (event: ShellOutputEvent) => {
|
||||||
if (event.type === 'exit') {
|
if (event.type === 'exit') {
|
||||||
callback(event.exitCode ?? 0, event.signal ?? undefined);
|
callback(event.exitCode ?? 0, event.signal ?? undefined);
|
||||||
@@ -1101,14 +1130,14 @@ export class ShellExecutionService {
|
|||||||
};
|
};
|
||||||
const unsubscribe = this.subscribe(pid, listener);
|
const unsubscribe = this.subscribe(pid, listener);
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
} else {
|
|
||||||
// Check if it already exited recently
|
|
||||||
const exitedInfo = this.exitedPtyInfo.get(pid);
|
|
||||||
if (exitedInfo) {
|
|
||||||
callback(exitedInfo.exitCode, exitedInfo.signal);
|
|
||||||
}
|
|
||||||
return () => {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it already exited recently
|
||||||
|
const exitedInfo = this.exitedPtyInfo.get(pid);
|
||||||
|
if (exitedInfo) {
|
||||||
|
callback(exitedInfo.exitCode, exitedInfo.signal);
|
||||||
|
}
|
||||||
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1118,8 +1147,8 @@ export class ShellExecutionService {
|
|||||||
*/
|
*/
|
||||||
static kill(pid: number): void {
|
static kill(pid: number): void {
|
||||||
const activePty = this.activePtys.get(pid);
|
const activePty = this.activePtys.get(pid);
|
||||||
const activeChild = this.activeChildProcesses.get(pid);
|
const activeChild = this.getActiveChildProcess(pid);
|
||||||
const activeVirtual = this.activeVirtualProcesses.get(pid);
|
const activeVirtual = this.getActiveVirtualProcess(pid);
|
||||||
|
|
||||||
if (activeVirtual) {
|
if (activeVirtual) {
|
||||||
activeVirtual.onKill?.();
|
activeVirtual.onKill?.();
|
||||||
@@ -1131,7 +1160,7 @@ export class ShellExecutionService {
|
|||||||
return;
|
return;
|
||||||
} else if (activeChild) {
|
} else if (activeChild) {
|
||||||
killProcessGroup({ pid }).catch(() => {});
|
killProcessGroup({ pid }).catch(() => {});
|
||||||
this.activeChildProcesses.delete(pid);
|
this.activeProcesses.delete(pid);
|
||||||
} else if (activePty) {
|
} else if (activePty) {
|
||||||
killProcessGroup({ pid, pty: activePty.ptyProcess }).catch(() => {});
|
killProcessGroup({ pid, pty: activePty.ptyProcess }).catch(() => {});
|
||||||
this.activePtys.delete(pid);
|
this.activePtys.delete(pid);
|
||||||
@@ -1154,8 +1183,8 @@ export class ShellExecutionService {
|
|||||||
const rawOutput = Buffer.from('');
|
const rawOutput = Buffer.from('');
|
||||||
|
|
||||||
const activePty = this.activePtys.get(pid);
|
const activePty = this.activePtys.get(pid);
|
||||||
const activeChild = this.activeChildProcesses.get(pid);
|
const activeChild = this.getActiveChildProcess(pid);
|
||||||
const activeVirtual = this.activeVirtualProcesses.get(pid);
|
const activeVirtual = this.getActiveVirtualProcess(pid);
|
||||||
|
|
||||||
if (activePty) {
|
if (activePty) {
|
||||||
output = getFullBufferText(activePty.headlessTerminal);
|
output = getFullBufferText(activePty.headlessTerminal);
|
||||||
@@ -1214,8 +1243,8 @@ export class ShellExecutionService {
|
|||||||
|
|
||||||
// Send current buffer content immediately
|
// Send current buffer content immediately
|
||||||
const activePty = this.activePtys.get(pid);
|
const activePty = this.activePtys.get(pid);
|
||||||
const activeChild = this.activeChildProcesses.get(pid);
|
const activeChild = this.getActiveChildProcess(pid);
|
||||||
const activeVirtual = this.activeVirtualProcesses.get(pid);
|
const activeVirtual = this.getActiveVirtualProcess(pid);
|
||||||
|
|
||||||
if (activePty) {
|
if (activePty) {
|
||||||
// Use serializeTerminalToObject to preserve colors and structure
|
// Use serializeTerminalToObject to preserve colors and structure
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2026 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
|
||||||
import { ShellExecutionService } from './shellExecutionService.js';
|
|
||||||
|
|
||||||
describe('ShellExecutionService virtual executions', () => {
|
|
||||||
it('completes a virtual execution in the foreground', async () => {
|
|
||||||
const { pid, result } = ShellExecutionService.createVirtualExecution();
|
|
||||||
const onExit = vi.fn();
|
|
||||||
const unsubscribe = ShellExecutionService.onExit(pid!, onExit);
|
|
||||||
|
|
||||||
ShellExecutionService.appendVirtualOutput(pid!, 'Hello');
|
|
||||||
ShellExecutionService.appendVirtualOutput(pid!, ' World');
|
|
||||||
ShellExecutionService.completeVirtualExecution(pid!, { exitCode: 0 });
|
|
||||||
|
|
||||||
const executionResult = await result;
|
|
||||||
|
|
||||||
expect(executionResult.output).toBe('Hello World');
|
|
||||||
expect(executionResult.backgrounded).toBeUndefined();
|
|
||||||
expect(executionResult.exitCode).toBe(0);
|
|
||||||
expect(executionResult.error).toBeNull();
|
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
|
||||||
expect(onExit).toHaveBeenCalledWith(0, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
unsubscribe();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports backgrounding virtual executions and streaming additional output', async () => {
|
|
||||||
const { pid, result } = ShellExecutionService.createVirtualExecution();
|
|
||||||
const chunks: string[] = [];
|
|
||||||
const onExit = vi.fn();
|
|
||||||
|
|
||||||
const unsubscribeStream = ShellExecutionService.subscribe(pid!, (event) => {
|
|
||||||
if (event.type === 'data' && typeof event.chunk === 'string') {
|
|
||||||
chunks.push(event.chunk);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const unsubscribeExit = ShellExecutionService.onExit(pid!, onExit);
|
|
||||||
|
|
||||||
ShellExecutionService.appendVirtualOutput(pid!, 'Chunk 1');
|
|
||||||
ShellExecutionService.background(pid!);
|
|
||||||
|
|
||||||
const backgroundResult = await result;
|
|
||||||
expect(backgroundResult.backgrounded).toBe(true);
|
|
||||||
expect(backgroundResult.output).toBe('Chunk 1');
|
|
||||||
|
|
||||||
ShellExecutionService.appendVirtualOutput(pid!, '\nChunk 2');
|
|
||||||
ShellExecutionService.completeVirtualExecution(pid!, { exitCode: 0 });
|
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
|
||||||
expect(chunks.join('')).toContain('Chunk 2');
|
|
||||||
expect(onExit).toHaveBeenCalledWith(0, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
unsubscribeStream();
|
|
||||||
unsubscribeExit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('kills virtual executions via the existing kill API', async () => {
|
|
||||||
const onKill = vi.fn();
|
|
||||||
const { pid, result } = ShellExecutionService.createVirtualExecution(
|
|
||||||
'',
|
|
||||||
onKill,
|
|
||||||
);
|
|
||||||
|
|
||||||
ShellExecutionService.appendVirtualOutput(pid!, 'work');
|
|
||||||
ShellExecutionService.kill(pid!);
|
|
||||||
|
|
||||||
const killResult = await result;
|
|
||||||
expect(onKill).toHaveBeenCalledTimes(1);
|
|
||||||
expect(killResult.aborted).toBe(true);
|
|
||||||
expect(killResult.exitCode).toBe(130);
|
|
||||||
expect(killResult.error?.message).toContain('Operation cancelled by user');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user