Shutdown Check

Search documentation

Find a page or section

Run the same shutdown test from JavaScript or TypeScript and inspect the result.

The shutdown-check Node API is useful when the check belongs inside a test suite or custom script. It runs the same engine as the CLI, but it returns data instead of printing output or exiting your process.

Import the package

ES modules:

import { checkShutdown, defineConfig } from "shutdown-check";

CommonJS, supported since 1.0.1:

const { checkShutdown, defineConfig } = require("shutdown-check");

TypeScript declarations are included for both entry points.

Exports

ExportUse it when
checkShutdownYou have an input object and want validation plus a full run
defineConfigYou want type checking and completion while writing config
parseConfigYou want to validate an object and apply defaults
loadConfigYou want to read and validate a JSON file
runCheckYou already have a validated CheckConfig
junitXmlYou want to turn a CheckResult into JUnit XML

The package also exports the config, result, diagnostic, and timeline types.

Run a check with checkShutdown

checkShutdown() is the recommended high-level function. It validates the input, fills in defaults, runs the check, performs cleanup, and resolves to a CheckResult.

test/shutdown.test.ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { checkShutdown } from "shutdown-check";
 
test("finishes active requests on SIGTERM", { timeout: 30_000 }, async () => {
  const result = await checkShutdown({
    command: ["node", "dist/server.js"],
    env: { PORT: "3510" },
    baseUrl: "http://127.0.0.1:3510",
    readiness: { path: "/health" },
    workload: {
      path: "/slow",
      bodyIncludes: "work complete",
      started: { type: "response-headers" },
    },
    shutdown: {
      deadlineMs: 10_000,
      readinessWithdrawal: true,
    },
  });
 
  assert.equal(result.pass, true, `${result.code}: ${result.message}`);
});

A normal shutdown failure does not throw. It resolves with pass: false and a diagnostic code. Invalid config rejects with a validation Error.

function checkShutdown(input: CheckConfigInput, baseDirectory?: string): Promise<CheckResult>

Validates input with parseConfig() and runs the check. Relative paths resolve from baseDirectory (default process.cwd()). It starts and cleans up its own process, and returns pass: false for a failed check rather than throwing; it throws only for an invalid config.

Resolve relative paths

The second argument is the base directory used for relative cwd values. It defaults to process.cwd().

const result = await checkShutdown(config, import.meta.dirname);

Pass the test file's directory when the test runner may use another working directory.

Type a config with defineConfig

defineConfig() returns the object unchanged. It exists to provide editor completion and catch invalid types before the test runs.

shutdown.config.ts
import { defineConfig } from "shutdown-check";
 
export default defineConfig({
  command: ["node", "dist/server.js"],
  baseUrl: "http://127.0.0.1:3510",
  readiness: { path: "/health", timeoutMs: 30_000 },
  workload: {
    path: "/slow",
    concurrent: 2,
    started: { type: "response-headers" },
  },
  shutdown: { deadlineMs: 15_000 },
});

Import the object and pass it to checkShutdown(). The CLI does not load TypeScript config files.

function defineConfig<T extends CheckConfigInput>(input: T): T

Returns input unchanged. Use it for editor completion and type checking when you write a config in TypeScript.

Validate input with parseConfig

parseConfig() validates an unknown object and returns a complete CheckConfig with defaults applied.

import { parseConfig } from "shutdown-check";
 
const config = parseConfig(
  {
    command: ["node", "dist/server.js"],
    baseUrl: "http://127.0.0.1:3510",
    readiness: { path: "/health" },
    workload: {
      path: "/slow",
      started: { type: "response-headers" },
    },
  },
  process.cwd()
);
 
console.log(config.shutdown.deadlineMs); // 10000

The optional second argument controls how relative cwd is resolved. Validation throws an Error naming the first invalid field.

Read a JSON config with loadConfig

loadConfig() reads a file, parses its JSON, validates it, and resolves cwd from the file's folder.

import { loadConfig } from "shutdown-check";
 
const config = await loadConfig("config/shutdown-check.json");

File errors, JSON syntax errors, and validation errors are thrown. The messages match the ones used by the CLI.

function parseConfig(raw: unknown, baseDirectory?: string): CheckConfig

Validates a config object and fills in every default, resolving cwd from baseDirectory (default process.cwd()). Throws an Error naming the first invalid field.

function loadConfig(file: string): Promise<CheckConfig>

Reads and validates a JSON config file. Relative paths in it resolve from the file's folder.

Build a custom runner

Use runCheck() after parseConfig() or loadConfig() when you need custom logging or report handling.

scripts/check-shutdown.ts
import { mkdir, writeFile } from "node:fs/promises";
import { junitXml, loadConfig, runCheck } from "shutdown-check";
 
const config = await loadConfig("shutdown-check.json");
const result = await runCheck(config);
 
for (const item of result.timeline) {
  const detail = item.detail ? ` — ${item.detail}` : "";
  console.log(`+${item.ms} ms ${item.event}${detail}`);
}
 
await mkdir("reports", { recursive: true });
await writeFile(
  "reports/shutdown.xml",
  junitXml(result, "api-server shutdown")
);
 
if (!result.pass) {
  console.error(`${result.code}: ${result.message}`);
  if (result.stderr) console.error(result.stderr);
  process.exitCode = 1;
}

runCheck() expects a valid CheckConfig; it does not validate a hand-built object again. Bypassing validation can lead to SC002 or SC999.

function runCheck(config: CheckConfig): Promise<CheckResult>

Runs the check with an already validated config, from parseConfig() or loadConfig().

function junitXml(result: CheckResult, suiteName?: string): string

Turns a result into JUnit XML: one suite (suiteName, default "shutdown-check") with one test case, "graceful shutdown". The timeline goes in system-out, stderr in system-err, and a failed check adds a failure element typed with its code.

The optional second argument to junitXml() names the test suite. It defaults to shutdown-check.

Understand CheckResult

Every completed check resolves to the same object printed by CLI --json.

OptionTypeDefaultDescription
pass*boolean—true only for SC000.
code*string—The diagnostic code: SC000 on a pass, the first failure otherwise.
message*string—What happened, in one sentence, with the values observed.
timeline*TimelineEvent[]—Every step of the check in order, with milliseconds since it started.
stdout*string—The last 8 KiB the service wrote to stdout.
stderr*string—The last 8 KiB the service wrote to stderr.

Important fields:

  • pass is the value to assert in a test.
  • code is SC000 on a pass or the first failure code.
  • message is the human-readable result.
  • timeline records each observed step.
  • stdout and stderr contain the captured service output tails.
  • startedAt and durationMs describe the run.

Each item in timeline has this shape:

OptionTypeDefaultDescription
ms*number—Milliseconds since the check started, rounded.
event*string—What happened: "process launched", "service ready", "signal sent", "work request finished", "process exited" and so on.
detailstring—The values behind it, such as "HTTP 503" or "code=0, signal=none".

The output reference lists every event name and its detail.

What throws?

SituationcheckShutdown()runCheck()
Invalid input configRejects with ErrorNot validated
Expected check failureResolves with pass: falseResolves with pass: false
Unexpected error during the runResolves with SC999Resolves with SC999

Wrap calls in try/catch when configuration may be invalid. For an ordinary test failure, inspect the returned code and timeline.

Cleanup and parallel tests

Before resolving, the API closes its client requests, clears timers, and force-kills the service process group when necessary. A passing service has already exited and closed its port by itself.

Parallel checks must use separate ports and isolated test data. Otherwise one run may fail with SC001 or affect another run's workload.