Initial Commit
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
SERVER_KEYS_CODES,
|
||||
ServerKeysError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import { runFunction } from "./run-function.js";
|
||||
|
||||
interface CapturedLogger {
|
||||
info: (m: string) => void;
|
||||
error: (m: string) => void;
|
||||
out: string[];
|
||||
}
|
||||
|
||||
function makeLogger(): CapturedLogger {
|
||||
const out: string[] = [];
|
||||
return {
|
||||
out,
|
||||
info: (m: string) => out.push(`I:${m}`),
|
||||
error: (m: string) => out.push(`E:${m}`),
|
||||
};
|
||||
}
|
||||
|
||||
function neverExit(exitCodes: number[]) {
|
||||
return (code: number) => {
|
||||
exitCodes.push(code);
|
||||
};
|
||||
}
|
||||
|
||||
describe("runFunction", () => {
|
||||
it("returns the body's value on success and increments OTel without printing an error", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const logger = makeLogger();
|
||||
const exitCodes: number[] = [];
|
||||
const result = await runFunction("ok-cmd", async () => "done", {
|
||||
logger,
|
||||
exit: neverExit(exitCodes),
|
||||
});
|
||||
expect(result).toBe("done");
|
||||
expect(exitCodes).toHaveLength(0);
|
||||
expect(logger.out.filter((line) => line.startsWith("E:"))).toHaveLength(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it("wraps a ServerKeysError, exits 1, prints structured stderr, then re-throws", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const logger = makeLogger();
|
||||
const exitCodes: number[] = [];
|
||||
await expect(
|
||||
runFunction(
|
||||
"fail-cmd",
|
||||
async () => {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.VAULT_NOT_FOUND,
|
||||
"missing"
|
||||
);
|
||||
},
|
||||
{ logger, exit: neverExit(exitCodes) }
|
||||
)
|
||||
).rejects.toBeInstanceOf(ServerKeysError);
|
||||
expect(exitCodes).toEqual([1]);
|
||||
expect(logger.out[0]).toBe(
|
||||
"E:[fail-cmd] SERVER_KEYS.VAULT_NOT_FOUND: missing"
|
||||
);
|
||||
});
|
||||
|
||||
it("normalises a non-ServerKeysError Error throw into SERVER_KEYS.UNKNOWN_COMMAND", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const logger = makeLogger();
|
||||
const exitCodes: number[] = [];
|
||||
await expect(
|
||||
runFunction(
|
||||
"unk-cmd",
|
||||
async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
{ logger, exit: neverExit(exitCodes) }
|
||||
)
|
||||
).rejects.toMatchObject({
|
||||
code: SERVER_KEYS_CODES.UNKNOWN_COMMAND,
|
||||
});
|
||||
expect(exitCodes).toEqual([1]);
|
||||
expect(logger.out[0]).toContain(
|
||||
"SERVER_KEYS.UNKNOWN_COMMAND: unk-cmd failed: boom"
|
||||
);
|
||||
});
|
||||
|
||||
it("wraps non-Error throws with String() in the message", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const logger = makeLogger();
|
||||
const exitCodes: number[] = [];
|
||||
await expect(
|
||||
runFunction(
|
||||
"str-cmd",
|
||||
async () => {
|
||||
const notErr: unknown = "string-throw";
|
||||
throw notErr;
|
||||
},
|
||||
{ logger, exit: neverExit(exitCodes) }
|
||||
)
|
||||
).rejects.toMatchObject({
|
||||
code: SERVER_KEYS_CODES.UNKNOWN_COMMAND,
|
||||
});
|
||||
expect(logger.out[0]).toContain("string-throw");
|
||||
});
|
||||
|
||||
it("short-circuits with SERVER_KEYS.ABORTED when signal already aborted", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const logger = makeLogger();
|
||||
const exitCodes: number[] = [];
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
let bodyRan = false;
|
||||
await expect(
|
||||
runFunction(
|
||||
"abort-cmd",
|
||||
async () => {
|
||||
bodyRan = true;
|
||||
},
|
||||
{
|
||||
logger,
|
||||
exit: neverExit(exitCodes),
|
||||
signal: ac.signal,
|
||||
}
|
||||
)
|
||||
).rejects.toMatchObject({ code: SERVER_KEYS_CODES.ABORTED });
|
||||
expect(bodyRan).toBe(false);
|
||||
expect(exitCodes).toEqual([1]);
|
||||
expect(logger.out[0]).toBe(
|
||||
"E:[abort-cmd] aborted: SERVER_KEYS.ABORTED"
|
||||
);
|
||||
});
|
||||
|
||||
it("threads the AbortSignal into the body when not pre-aborted", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const logger = makeLogger();
|
||||
const exitCodes: number[] = [];
|
||||
const ac = new AbortController();
|
||||
const received = await runFunction(
|
||||
"thread-cmd",
|
||||
async (signal) => signal,
|
||||
{ logger, exit: neverExit(exitCodes), signal: ac.signal }
|
||||
);
|
||||
expect(received).toBe(ac.signal);
|
||||
});
|
||||
|
||||
it("default consoleLogger writes the success banner via process.stdout", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const stdoutWrites: string[] = [];
|
||||
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
try {
|
||||
(process.stdout as { write: unknown }).write = (
|
||||
chunk: string | Uint8Array
|
||||
): boolean => {
|
||||
stdoutWrites.push(
|
||||
typeof chunk === "string" ? chunk : String(chunk)
|
||||
);
|
||||
return true;
|
||||
};
|
||||
const result = await runFunction("default-cmd", async () => 42);
|
||||
expect(result).toBe(42);
|
||||
} finally {
|
||||
(process.stdout as { write: unknown }).write = origStdoutWrite;
|
||||
}
|
||||
expect(stdoutWrites.join("")).toContain("[default-cmd] ok");
|
||||
});
|
||||
|
||||
it("default consoleLogger writes via process.stderr and default exit calls process.exit on the failure path", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const stderrWrites: string[] = [];
|
||||
const stdoutWrites: string[] = [];
|
||||
const exitCalls: number[] = [];
|
||||
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
||||
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
const origExit = process.exit.bind(process);
|
||||
try {
|
||||
(process.stderr as { write: unknown }).write = (
|
||||
chunk: string | Uint8Array
|
||||
): boolean => {
|
||||
stderrWrites.push(
|
||||
typeof chunk === "string" ? chunk : String(chunk)
|
||||
);
|
||||
return true;
|
||||
};
|
||||
(process.stdout as { write: unknown }).write = (
|
||||
chunk: string | Uint8Array
|
||||
): boolean => {
|
||||
stdoutWrites.push(
|
||||
typeof chunk === "string" ? chunk : String(chunk)
|
||||
);
|
||||
return true;
|
||||
};
|
||||
(process as { exit: unknown }).exit = (
|
||||
code?: number | string | null
|
||||
): never => {
|
||||
exitCalls.push(typeof code === "number" ? code : -1);
|
||||
// Don't actually exit — let the wrapper re-throw.
|
||||
return undefined as never;
|
||||
};
|
||||
await expect(
|
||||
runFunction("defaults-fail-cmd", async () => {
|
||||
throw new ServerKeysError(
|
||||
SERVER_KEYS_CODES.VAULT_NOT_FOUND,
|
||||
"no defaults"
|
||||
);
|
||||
})
|
||||
).rejects.toBeInstanceOf(ServerKeysError);
|
||||
} finally {
|
||||
(process.stderr as { write: unknown }).write = origStderrWrite;
|
||||
(process.stdout as { write: unknown }).write = origStdoutWrite;
|
||||
(process as { exit: unknown }).exit = origExit;
|
||||
}
|
||||
expect(exitCalls).toEqual([1]);
|
||||
expect(stderrWrites.join("")).toContain(
|
||||
"[defaults-fail-cmd] SERVER_KEYS.VAULT_NOT_FOUND: no defaults"
|
||||
);
|
||||
});
|
||||
|
||||
it("default consoleLogger writes via process.stderr on the pre-aborted path", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const stderrWrites: string[] = [];
|
||||
const exitCalls: number[] = [];
|
||||
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
||||
const origExit = process.exit.bind(process);
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
try {
|
||||
(process.stderr as { write: unknown }).write = (
|
||||
chunk: string | Uint8Array
|
||||
): boolean => {
|
||||
stderrWrites.push(
|
||||
typeof chunk === "string" ? chunk : String(chunk)
|
||||
);
|
||||
return true;
|
||||
};
|
||||
(process as { exit: unknown }).exit = (
|
||||
code?: number | string | null
|
||||
): never => {
|
||||
exitCalls.push(typeof code === "number" ? code : -1);
|
||||
return undefined as never;
|
||||
};
|
||||
await expect(
|
||||
runFunction("defaults-abort-cmd", async () => {}, {
|
||||
signal: ac.signal,
|
||||
})
|
||||
).rejects.toMatchObject({ code: SERVER_KEYS_CODES.ABORTED });
|
||||
} finally {
|
||||
(process.stderr as { write: unknown }).write = origStderrWrite;
|
||||
(process as { exit: unknown }).exit = origExit;
|
||||
}
|
||||
expect(exitCalls).toEqual([1]);
|
||||
expect(stderrWrites.join("")).toContain(
|
||||
"[defaults-abort-cmd] aborted: SERVER_KEYS.ABORTED"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user