This commit improves the management of script execution process by enhancing the way terminal commands are handled, paving the way for easier future modifications and providing clearer feedback to users when scripts are cancelled. Previously, the UI displayed a generic error message which could lead to confusion if the user intentionally cancelled the script execution. Now, a specific error dialog will appear, improving the user experience by accurately reflecting the action taken by the user. This change affects code execution on Linux where closing GNOME terminal returns exit code `137` which is then treated by script cancellation by privacy.sexy to show the accurate error dialog. It does not affect macOS and Windows as curret commands result in success (`0`) exit code on cancellation. Additionally, this update encapsulates OS-specific logic into dedicated classes, promoting better separation of concerns and increasing the modularity of the codebase. This makes it simpler to maintain and extend the application. Key changes: - Display a specific error message for script cancellations. - Refactor command execution into dedicated classes. - Improve file permission setting flexibility and avoid setting file permissions on Windows as it's not required to execute files. - Introduce more granular error types for script execution. - Increase logging for shell commands to aid in debugging. - Expand test coverage to ensure reliability. - Fix error dialogs not showing the error messages due to incorrect propagation of errors. Other supported changes: - Update `SECURITY.md` with details on script readback and verification. - Fix a typo in `IpcRegistration.spec.ts`. - Document antivirus scans in `desktop-vs-web-features.md`.
161 lines
6.4 KiB
TypeScript
161 lines
6.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { Logger } from '@/application/Common/Log/Logger';
|
|
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
|
import { LoggingNodeShellCommandRunner } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/ShellRunner/LoggingNodeShellCommandRunner';
|
|
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
|
|
import type { SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
|
import { CommandOpsStub } from '@tests/unit/shared/Stubs/CommandOpsStub';
|
|
import { ChildProcessStub } from '@tests/unit/shared/Stubs/ChildProcesssStub';
|
|
import type { ShellCommandOutcome } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Runner/ShellRunner/ShellCommandRunner';
|
|
|
|
describe('LoggingNodeShellCommandRunner', () => {
|
|
describe('logging', () => {
|
|
it('logs on command execution', () => {
|
|
// arrange
|
|
const logger = new LoggerStub();
|
|
const context = new TestContext()
|
|
.withLogger(logger);
|
|
const expectedLogMessage = `Executing command: ${context.command}`;
|
|
// act
|
|
context.runShellCommand();
|
|
// assert
|
|
expect(logger.assertLogsContainMessagePart('info', expectedLogMessage));
|
|
});
|
|
|
|
it('logs on command completion with exit code', () => {
|
|
// arrange
|
|
const exitCode = 31;
|
|
const expectedLogMessage = `Command completed with exit code ${exitCode}.`;
|
|
const logger = new LoggerStub();
|
|
const childProcessStub = new ChildProcessStub();
|
|
const context = new TestContext()
|
|
.withLogger(logger)
|
|
.withSystemOperations(createSystemOperationsWithChildProcessStub(childProcessStub));
|
|
// act
|
|
context.runShellCommand();
|
|
childProcessStub.emitExit(exitCode, null);
|
|
// assert
|
|
expect(logger.assertLogsContainMessagePart('info', expectedLogMessage));
|
|
});
|
|
|
|
it('logs on command termination by a signal', async () => {
|
|
// arrange
|
|
const signal: NodeJS.Signals = 'SIGKILL';
|
|
const expectedLogMessage = `Command terminated by signal: ${signal}`;
|
|
const logger = new LoggerStub();
|
|
const childProcessStub = new ChildProcessStub();
|
|
const context = new TestContext()
|
|
.withLogger(logger)
|
|
.withSystemOperations(createSystemOperationsWithChildProcessStub(childProcessStub));
|
|
// act
|
|
context.runShellCommand();
|
|
childProcessStub.emitExit(null, signal);
|
|
// assert
|
|
expect(logger.assertLogsContainMessagePart('warn', expectedLogMessage));
|
|
});
|
|
|
|
it('logs on command execution fail', async () => {
|
|
// arrange
|
|
const expectedErrorMessage = 'Error when executing command';
|
|
const expectedLogMessage = 'Command execution failed:';
|
|
const logger = new LoggerStub();
|
|
const childProcessStub = new ChildProcessStub();
|
|
const context = new TestContext()
|
|
.withLogger(logger)
|
|
.withSystemOperations(createSystemOperationsWithChildProcessStub(childProcessStub));
|
|
// act
|
|
context.runShellCommand();
|
|
childProcessStub.emitError(new Error(expectedLogMessage));
|
|
// assert
|
|
expect(logger.assertLogsContainMessagePart('error', expectedLogMessage));
|
|
expect(logger.assertLogsContainMessagePart('error', expectedErrorMessage));
|
|
});
|
|
});
|
|
|
|
describe('return object', () => {
|
|
it('when child process exits on its own', async () => {
|
|
// arrange
|
|
const expectedExitCode = 31;
|
|
const expectedOutcomeType: ShellCommandOutcome['type'] = 'RegularProcessExit';
|
|
const childProcessStub = new ChildProcessStub()
|
|
.withAutoEmitExit(false);
|
|
const context = new TestContext()
|
|
.withSystemOperations(createSystemOperationsWithChildProcessStub(childProcessStub));
|
|
// act
|
|
const task = context.runShellCommand();
|
|
childProcessStub.emitExit(expectedExitCode, null);
|
|
const actualResult = await task;
|
|
// assert
|
|
expect(actualResult.type).to.equal(expectedOutcomeType);
|
|
expect(actualResult.exitCode).to.equal(expectedExitCode);
|
|
});
|
|
it('when child process is terminated by a signal', async () => {
|
|
// arrange
|
|
const expectedTerminationSignal: NodeJS.Signals = 'SIGABRT';
|
|
const expectedOutcomeType: ShellCommandOutcome['type'] = 'ExternallyTerminated';
|
|
const childProcessStub = new ChildProcessStub()
|
|
.withAutoEmitExit(false);
|
|
const context = new TestContext()
|
|
.withSystemOperations(createSystemOperationsWithChildProcessStub(childProcessStub));
|
|
// act
|
|
const task = context.runShellCommand();
|
|
childProcessStub.emitExit(null, expectedTerminationSignal);
|
|
const actualResult = await task;
|
|
// assert
|
|
expect(actualResult.type).to.equal(expectedOutcomeType);
|
|
expect(actualResult.terminationSignal).to.equal(expectedTerminationSignal);
|
|
});
|
|
it('when child process has errors', async () => {
|
|
// arrange
|
|
const expectedError = new Error('inner error');
|
|
const expectedOutcomeType: ShellCommandOutcome['type'] = 'ExecutionError';
|
|
const childProcessStub = new ChildProcessStub()
|
|
.withAutoEmitExit(false);
|
|
const context = new TestContext()
|
|
.withSystemOperations(createSystemOperationsWithChildProcessStub(childProcessStub));
|
|
// act
|
|
const task = context.runShellCommand();
|
|
childProcessStub.emitError(expectedError);
|
|
const actualResult = await task;
|
|
// assert
|
|
expect(actualResult.type).to.equal(expectedOutcomeType);
|
|
expect(actualResult.error).to.deep.equal(expectedError);
|
|
});
|
|
});
|
|
});
|
|
|
|
function createSystemOperationsWithChildProcessStub(
|
|
childProcessStub: ChildProcessStub,
|
|
): SystemOperations {
|
|
const commandOps = new CommandOpsStub()
|
|
.withChildProcess(childProcessStub.asChildProcess());
|
|
return new SystemOperationsStub()
|
|
.withCommand(commandOps);
|
|
}
|
|
|
|
class TestContext {
|
|
public readonly command: string = 'echo "Hello from unit tests!"';
|
|
|
|
private logger: Logger = new LoggerStub();
|
|
|
|
private systemOps: SystemOperations = new SystemOperationsStub();
|
|
|
|
public withLogger(logger: Logger): this {
|
|
this.logger = logger;
|
|
return this;
|
|
}
|
|
|
|
public withSystemOperations(systemOps: SystemOperations): this {
|
|
this.systemOps = systemOps;
|
|
return this;
|
|
}
|
|
|
|
public runShellCommand(): ReturnType<LoggingNodeShellCommandRunner['runShellCommand']> {
|
|
const sut = new LoggingNodeShellCommandRunner(
|
|
this.logger,
|
|
this.systemOps,
|
|
);
|
|
return sut.runShellCommand(this.command);
|
|
}
|
|
}
|