| ... | ... | @@ -17,6 +17,27 @@ import { |
| 17 | 17 | resolveRemoteT3CliPackageSpec, |
| 18 | 18 | runSshCommand, |
| 19 | 19 | } from "./command.ts"; |
| 20 | import { SshCommandError } from "./errors.ts"; |
| 21 | |
| 22 | const encoder = new TextEncoder(); |
| 23 | |
| 24 | const makeFailedProcess = (input: { readonly stdout: string; readonly stderr?: string }) => { |
| 25 | const stdoutStream = Stream.make(encoder.encode(input.stdout)); |
| 26 | const stderrStream = input.stderr ? Stream.make(encoder.encode(input.stderr)) : Stream.empty; |
| 27 | return ChildProcessSpawner.makeHandle({ |
| 28 | pid: ChildProcessSpawner.ProcessId(123), |
| 29 | stdout: stdoutStream, |
| 30 | stderr: stderrStream, |
| 31 | all: Stream.empty, |
| 32 | exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(1)), |
| 33 | isRunning: Effect.succeed(false), |
| 34 | kill: () => Effect.void, |
| 35 | stdin: Sink.drain, |
| 36 | getInputFd: () => Sink.drain, |
| 37 | getOutputFd: () => Stream.empty, |
| 38 | unref: Effect.succeed(Effect.void), |
| 39 | }); |
| 40 | }; |
| 20 | 41 | |
| 21 | 42 | const makeNeverFinishingProcess = () => { |
| 22 | 43 | let finish: ((exitCode: ChildProcessSpawner.ExitCode) => void) | null = null; |
| ... | ... | @@ -124,6 +145,65 @@ describe("ssh command", () => { |
| 124 | 145 | }), |
| 125 | 146 | ); |
| 126 | 147 | |
| 148 | it.effect("includes stdout in non-zero command failures when stderr is empty", () => { |
| 149 | const spawner = ChildProcessSpawner.make(() => |
| 150 | Effect.succeed(makeFailedProcess({ stdout: "Pairing token creation failed\n" })), |
| 151 | ); |
| 152 | const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner); |
| 153 | const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer); |
| 154 | |
| 155 | return Effect.gen(function* () { |
| 156 | const result = yield* Effect.result( |
| 157 | runSshCommand( |
| 158 | { |
| 159 | alias: "devbox", |
| 160 | hostname: "devbox.example.com", |
| 161 | username: "julius", |
| 162 | port: 2222, |
| 163 | }, |
| 164 | { remoteCommandArgs: ["sh", "-s"] }, |
| 165 | ), |
| 166 | ); |
| 167 | |
| 168 | assert.isTrue(Result.isFailure(result)); |
| 169 | if (Result.isFailure(result)) { |
| 170 | assert.instanceOf(result.failure, SshCommandError); |
| 171 | assert.equal(result.failure.message, "Pairing token creation failed"); |
| 172 | assert.equal(result.failure.stdout, "Pairing token creation failed\n"); |
| 173 | assert.equal(result.failure.stderr, ""); |
| 174 | } |
| 175 | }).pipe(Effect.provide(processLayer)); |
| 176 | }); |
| 177 | |
| 178 | it.effect("redacts credentials from stdout in non-zero command failures", () => { |
| 179 | const spawner = ChildProcessSpawner.make(() => |
| 180 | Effect.succeed(makeFailedProcess({ stdout: '{"credential":"pairing-secret"}\n' })), |
| 181 | ); |
| 182 | const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner); |
| 183 | const processLayer = Layer.mergeAll(NodeServices.layer, spawnerLayer); |
| 184 | |
| 185 | return Effect.gen(function* () { |
| 186 | const result = yield* Effect.result( |
| 187 | runSshCommand( |
| 188 | { |
| 189 | alias: "devbox", |
| 190 | hostname: "devbox.example.com", |
| 191 | username: "julius", |
| 192 | port: 2222, |
| 193 | }, |
| 194 | { remoteCommandArgs: ["sh", "-s"] }, |
| 195 | ), |
| 196 | ); |
| 197 | |
| 198 | assert.isTrue(Result.isFailure(result)); |
| 199 | if (Result.isFailure(result)) { |
| 200 | assert.instanceOf(result.failure, SshCommandError); |
| 201 | assert.equal(result.failure.message, '{"credential":"[redacted]"}'); |
| 202 | assert.equal(result.failure.stdout, '{"credential":"[redacted]"}\n'); |
| 203 | } |
| 204 | }).pipe(Effect.provide(processLayer)); |
| 205 | }); |
| 206 | |
| 127 | 207 | it.effect("fails commands that never finish", () => { |
| 128 | 208 | const spawner = ChildProcessSpawner.make(() => Effect.succeed(makeNeverFinishingProcess())); |
| 129 | 209 | const spawnerLayer = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner); |