sanity-check

star 5.6k

Run an E2E smoke test that installs rivetkit packages from npm in an isolated temp project, starts the hello-world counter actor, then verifies both HTTP actions and WebSocket actions + events work end-to-end. Use when the user asks to sanity check, smoke test, or verify a rivetkit release/preview-publish works.

rivet-dev By rivet-dev schedule Updated 4/24/2026

name: sanity-check description: Run an E2E smoke test that installs rivetkit packages from npm in an isolated temp project, starts the hello-world counter actor, then verifies both HTTP actions and WebSocket actions + events work end-to-end. Use when the user asks to sanity check, smoke test, or verify a rivetkit release/preview-publish works.

Sanity Check

Run a quick end-to-end sanity check of a published rivetkit version: copy the hello-world example to a temp directory, install the specified package version from the public npm registry, start the dev server, and run a client test script that verifies both HTTP actions and WebSocket connections with event broadcasting.

When to use

  • User wants to verify a published rivetkit version works (e.g., rivetkit@0.0.0-pr.4701.a818b77, rivetkit@latest, rivetkit@2.5.0)
  • After a preview publish to verify the build is functional
  • After a release to verify the package installs and runs correctly
  • User says "sanity check", "smoke test", "verify the build", or "test this version"

Inputs

  1. Version or tag (required) — explicit pkg-pr-new preview, npm dist-tag, or semver. If not provided in the user's message, ask for it.
  2. Additional test behavior (optional) — e.g., "also verify workflows persist" or "check that KV works." If provided, extend src/index.ts + test.mjs using the menu in "Extending with custom tests" below.

Usage

  • /sanity-check <version> — run in a temp directory on the host
  • /sanity-check docker <version> — run inside a node:22 Docker container
  • /sanity-check <version> <custom instructions> — any extra instructions (e.g. "also hit a KV action", "verify state persists across restart", "use pnpm", "test on node 20")

<version> is any npm-resolvable spec: an explicit pkg-pr-new preview (0.0.0-pr.4701.a818b77), an npm dist-tag (latest, rc, next), or a semver (2.3.0-rc.4). If the user omits it, ask for it.

What it tests

  1. npm install of rivetkit, @rivetkit/react, and the platform-specific @rivetkit/rivetkit-napi-* native binding from the public npm registry
  2. Boot the hello-world counter actor server via registry.start() on port 6420
  3. HTTP path: call counter.increment(5), counter.increment(3), counter.getCount() and assert the values
  4. WebSocket path: open a .connect() conn, subscribe to the newCount event, call increment(10), assert the action response AND the broadcast event value match
  5. Report the resolved versions of rivetkit + @rivetkit/rivetkit-napi

Requirements

  • Node.js 22+ (or Docker with node:22)
  • This repo available locally — only used to copy examples/hello-world/src/ as the seed. Does NOT use any of its node_modules or workspace links.

Steps

1. Set up the test project

REPO_ROOT=$(git rev-parse --show-toplevel)
SANITY_DIR=$(mktemp -d /tmp/rivetkit-sanity-XXXXXX)
cp -r "$REPO_ROOT/examples/hello-world/src" "$REPO_ROOT/examples/hello-world/tsconfig.json" "$SANITY_DIR/"
cd "$SANITY_DIR"

Write package.json with <VERSION> substituted:

{
  "name": "rivetkit-sanity",
  "private": true,
  "type": "module",
  "dependencies": {
    "rivetkit": "<VERSION>",
    "@rivetkit/react": "<VERSION>"
  },
  "devDependencies": {
    "tsx": "^4",
    "typescript": "^5"
  }
}

For pkg-pr-new previews, <VERSION> can be the bare version string (0.0.0-pr.4701.a818b77) — npm resolves it directly. If that fails, fall back to the URL form: "rivetkit": "https://pkg.pr.new/rivet-dev/rivet/rivetkit@<short-sha>".

2. Write test.mjs

import { createClient } from "rivetkit/client";
import { spawn } from "node:child_process";

const ENDPOINT = "http://localhost:6420";

console.log("Starting counter actor server...");
const server = spawn("npx", ["tsx", "src/index.ts"], {
  stdio: ["ignore", "pipe", "pipe"],
});
let log = "";
server.stdout.on("data", (d) => (log += d));
server.stderr.on("data", (d) => (log += d));

async function waitForServer(timeoutMs = 30000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const r = await fetch(`${ENDPOINT}/health`);
      if (r.ok) return;
    } catch {}
    await new Promise((r) => setTimeout(r, 500));
  }
  console.error("--- server log ---\n" + log);
  throw new Error("server did not become ready");
}

let exitCode = 0;
try {
  await waitForServer();
  const client = createClient(ENDPOINT);

  // --- HTTP actions ---
  console.log("Testing HTTP actions...");
  const counter = client.counter.getOrCreate(["sanity"]);
  const a = await counter.increment(5);
  const b = await counter.increment(3);
  const c = await counter.getCount();
  if (a !== 5) throw new Error(`increment(5) => ${a}, expected 5`);
  if (b !== 8) throw new Error(`increment(3) => ${b}, expected 8`);
  if (c !== 8) throw new Error(`getCount() => ${c}, expected 8`);
  console.log(`  HTTP: increment(5)=${a}, increment(3)=${b}, getCount()=${c}`);

  // --- WebSocket actions + events ---
  console.log("Testing WebSocket + events...");
  const ws = client.counter.getOrCreate(["sanity-ws"]).connect();
  await new Promise((res, rej) => {
    const t = setTimeout(() => rej(new Error("ws open timeout")), 10000);
    ws.onOpen(() => {
      clearTimeout(t);
      res();
    });
    ws.onError(rej);
  });
  const eventPromise = new Promise((res, rej) => {
    const t = setTimeout(() => rej(new Error("newCount event timeout")), 5000);
    ws.on("newCount", (v) => {
      clearTimeout(t);
      res(v);
    });
  });
  const wsCount = await ws.increment(10);
  const eventValue = await eventPromise;
  if (wsCount !== 10) throw new Error(`ws increment(10) => ${wsCount}, expected 10`);
  if (eventValue !== 10) throw new Error(`newCount event => ${eventValue}, expected 10`);
  console.log(`  WS: increment(10)=${wsCount}, event=${eventValue}`);
  await ws.dispose();

  console.log("\n✅ E2E TEST PASSED");
} catch (err) {
  console.error(`\n❌ E2E TEST FAILED: ${err.message || err}`);
  console.error("--- server log (last 2KB) ---\n" + log.slice(-2000));
  exitCode = 1;
} finally {
  server.kill("SIGKILL");
  process.exit(exitCode);
}

3. Install + run

Default (host):

cd "$SANITY_DIR"
npm install
node test.mjs

If you need to inspect a failure after the fact, tee the output:

node test.mjs 2>&1 | tee /tmp/sanity-check.log
echo "exit=$?"

Docker mode:

docker run --rm \
  -v "$SANITY_DIR":/app \
  -w /app \
  node:22 \
  bash -c "npm install && timeout 120 node test.mjs"

4. Report installed versions

Surface the resolved versions (rivetkit's exports doesn't expose ./package.json, so read the file directly):

node -e "
for (const p of ['rivetkit','@rivetkit/react','@rivetkit/rivetkit-napi']) {
  try {
    const v = JSON.parse(require('fs').readFileSync('node_modules/'+p+'/package.json','utf8')).version;
    console.log(p, v);
  } catch (e) { console.log(p, '(not installed:', e.code || e.message, ')'); }
}
"

5. Report results

Tell the user:

  • Resolved versions (rivetkit + @rivetkit/rivetkit-napi)
  • HTTP path results
  • WebSocket path results + event value
  • ✅ or ❌ with the last 2KB of server log on failure

6. Clean up

rm -rf "$SANITY_DIR"

Extending with custom tests

If the user asks for extra behavior, modify src/index.ts and add assertions to test.mjs before running. Common asks and where to slot them:

  • KV round-trip: add kv actions (setKv: (c, key, val) => c.kv.set(key, val), getKv: (c, key) => c.kv.get(key)), then in test.mjs call set → get and assert.
  • Workflow: add a workflow action, await its completion, assert the final state.
  • SQLite + migrations: add a migration and a query action, call it, assert rows.
  • State persistence: increment, kill the server (server.kill("SIGTERM")), await exit, respawn, call getCount(), assert value preserved.
  • Multiple actor instances: use different keys, verify isolation.

Start from the base test above; layer additions rather than rewriting it.

Rules

  • Always use a fresh temp directory — never run in the repo itself.
  • Always install from the public npm registry or pkg-pr-new — never use local workspace links / file: deps.
  • Pin rivetkit + @rivetkit/react to the exact user-specified version; let npm's optionalDependencies resolve the right @rivetkit/rivetkit-napi-<platform> binary automatically.
  • If npm install fails to resolve a bare pkg-pr-new version string, retry using the https://pkg.pr.new/rivet-dev/rivet/<pkg>@<short-sha> URL form.
  • If the server doesn't reach /health in 30s, dump the last 2KB of server stderr/stdout before failing — most install/runtime issues show up there (missing native binary, wrong Node version, port collision).
  • On Docker mode, run rm -rf $SANITY_DIR only after docker run --rm exits so container-created node_modules get cleaned by the --rm flag.
Install via CLI
npx skills add https://github.com/rivet-dev/rivet --skill sanity-check
Repository Details
star Stars 5,584
call_split Forks 190
navigation Branch main
article Path SKILL.md
More from Creator