Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions apps/server/src/vcs/VcsProcess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import { TestClock } from "effect/testing";

import { VcsProcessExitError, VcsProcessTimeoutError } from "@t3tools/contracts";
import {
VcsProcessExitError,
VcsProcessSpawnError,
VcsProcessTimeoutError,
} from "@t3tools/contracts";
import * as VcsProcess from "./VcsProcess.ts";

const run = (input: VcsProcess.VcsProcessInput) =>
Expand Down Expand Up @@ -61,14 +65,53 @@ describe("VcsProcess.run", () => {

it.effect("fails with VcsProcessExitError for non-zero exits by default", () =>
Effect.gen(function* () {
const secretArgument = "--token=super-secret-token";
const secretStderr = "remote rejected super-secret-token";
const error = yield* run({
operation: "test.exit",
command: "node",
args: ["-e", "process.stderr.write('boom'); process.exit(2)"],
args: [
"-e",
"process.stderr.write(process.argv[1]); process.exit(2)",
secretStderr,
secretArgument,
],
cwd: process.cwd(),
}).pipe(Effect.flip);

expect(error).toBeInstanceOf(VcsProcessExitError);
expect(error).toMatchObject({
operation: "test.exit",
command: "node",
argumentCount: 4,
exitCode: 2,
detail: "Process exited with a non-zero status.",
stderrLength: secretStderr.length,
stderrTruncated: false,
});
expect(error.message).not.toContain(secretArgument);
expect(error.message).not.toContain(secretStderr);
}).pipe(provideLive),
);

it.effect("retains spawn causes without exposing process arguments in the error message", () =>
Effect.gen(function* () {
const secretArgument = "--token=super-secret-token";
const error = yield* run({
operation: "test.spawn",
command: "definitely-not-a-t3code-executable",
args: [secretArgument],
cwd: process.cwd(),
}).pipe(Effect.flip);

expect(error).toBeInstanceOf(VcsProcessSpawnError);
expect(error).toMatchObject({
operation: "test.spawn",
command: "definitely-not-a-t3code-executable",
argumentCount: 1,
});
expect(error).toHaveProperty("cause");
expect(error.message).not.toContain(secretArgument);
}).pipe(provideLive),
);

Expand Down
16 changes: 6 additions & 10 deletions apps/server/src/vcs/VcsProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,15 @@ const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;
const OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]";

function commandLabel(command: string, args: ReadonlyArray<string>): string {
return [command, ...args].join(" ");
}

export const make = Effect.gen(function* () {
const processRunner = yield* ProcessRunner.ProcessRunner;

const run = Effect.fn("VcsProcess.run")(function* (input: VcsProcessInput) {
const label = commandLabel(input.command, input.args);
const baseError = {
operation: input.operation,
command: label,
command: input.command,
cwd: input.cwd,
argumentCount: input.args.length,
};

const result = yield* processRunner
Expand Down Expand Up @@ -98,11 +94,11 @@ export const make = Effect.gen(function* () {

if (!input.allowNonZeroExit && result.code !== 0) {
return yield* new VcsProcessExitError({
operation: input.operation,
command: label,
Comment thread
cursor[bot] marked this conversation as resolved.
cwd: input.cwd,
...baseError,
exitCode: result.code,
detail: result.stderr.trim() || `${label} exited with code ${result.code}.`,
detail: "Process exited with a non-zero status.",
stderrLength: result.stderr.length,
stderrTruncated: result.stderrTruncated,
});
}

Expand Down
9 changes: 8 additions & 1 deletion packages/contracts/src/vcs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Schema from "effect/Schema";
import { TrimmedNonEmptyString } from "./baseSchemas.ts";
import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";

export const VcsDriverKind = Schema.Literals(["git", "jj", "unknown"]);
export type VcsDriverKind = typeof VcsDriverKind.Type;
Expand Down Expand Up @@ -62,6 +62,7 @@ export interface VcsProcessErrorContext {
readonly operation: string;
readonly command: string;
readonly cwd: string;
readonly argumentCount?: number;
}

export interface VcsProcessSpawnFailure {
Expand Down Expand Up @@ -92,6 +93,7 @@ export class VcsProcessSpawnError extends Schema.TaggedErrorClass<VcsProcessSpaw
operation: Schema.String,
command: Schema.String,
cwd: Schema.String,
argumentCount: Schema.optional(NonNegativeInt),
cause: Schema.Defect(),
},
) {
Expand All @@ -113,8 +115,11 @@ export class VcsProcessExitError extends Schema.TaggedErrorClass<VcsProcessExitE
operation: Schema.String,
command: Schema.String,
cwd: Schema.String,
argumentCount: Schema.optional(NonNegativeInt),
exitCode: Schema.Number,
detail: Schema.String,
stderrLength: Schema.optional(NonNegativeInt),
stderrTruncated: Schema.optional(Schema.Boolean),
},
) {
override get message(): string {
Expand All @@ -128,6 +133,7 @@ export class VcsProcessTimeoutError extends Schema.TaggedErrorClass<VcsProcessTi
operation: Schema.String,
command: Schema.String,
cwd: Schema.String,
argumentCount: Schema.optional(NonNegativeInt),
timeoutMs: Schema.Number,
},
) {
Expand All @@ -149,6 +155,7 @@ export class VcsOutputDecodeError extends Schema.TaggedErrorClass<VcsOutputDecod
operation: Schema.String,
command: Schema.String,
cwd: Schema.String,
argumentCount: Schema.optional(NonNegativeInt),
detail: Schema.String,
cause: Schema.optional(Schema.Defect()),
},
Expand Down
Loading