Tape is intentionally minimal: it writes TAP output to stdout and gets out of the way. That simplicity is its strength, but it also means Tape has no daemon, no scheduler, and no built-in visibility into whether it ran at all. If the CI job that triggers your Tape suite is accidentally disabled or filtered out, you get silence — not failure.
This is the silent suite-failure problem. HTTP monitors can't help — there's nothing to ping. The fix is heartbeat monitoring: your Tape run pings a unique URL after every successful execution, and if Vigilmon doesn't receive that ping within the expected window, you get alerted.
This tutorial shows you how to instrument Tape test pipelines with Vigilmon heartbeat monitors so you know when tests stop running, not just when they fail.
What You'll Cover
- Heartbeat monitoring for Tape test suites
- Detecting suites that stop running entirely
- Parsing TAP output to send a conditional heartbeat
- Wrapping Tape with a shell script or Node.js runner
- Integrating with CI/CD pipelines (GitHub Actions, GitLab CI)
Prerequisites
- A Node.js project with Tape installed
- A free account at vigilmon.online
The Problem: What Tape's Built-In Output Misses
Tape produces clean TAP output and a non-zero exit code on failure — but only when it runs. It won't alert you when:
- A CI path filter silently excludes your test files
- An
import/requireerror causes the test file to throw before anytest()calls register - A scheduled test job is disabled or misconfigured
- A TAP consumer (tap-reporter, tap-spec, tap-nyc) crashes and swallows the exit code
- Tape's process hangs on an unresolved promise after the last
t.end()call
Vigilmon's heartbeat pattern closes these gaps by requiring an explicit "I finished successfully" signal that wraps Tape from outside its TAP stream.
Step 1: Create a Heartbeat Monitor in Vigilmon
- Log in to Vigilmon and click New Monitor → Heartbeat.
- Name it something specific:
Tape Unit Tests — mainorTape Integration — nightly. - Set the Expected interval to match your test schedule. For suites that run on every push, use 2 hours. For nightly suites, use 25 hours.
- Set the Grace period to 30 minutes for push-triggered suites, or 1 hour for scheduled suites.
- Save and copy the Ping URL — it looks like
https://vigilmon.online/api/heartbeat/<unique-id>.
Step 2: Wrap Tape in a Node.js Runner
The most reliable approach is a thin Node.js wrapper that runs Tape as a child process and pings Vigilmon only when Tape exits with code 0.
Create run-tests.js in your project root:
// run-tests.js
const { spawnSync } = require("child_process");
const https = require("https");
const heartbeatUrl = process.env.VIGILMON_HEARTBEAT_URL || "";
// Run all test files via `tape` CLI
const result = spawnSync(
"node",
["node_modules/.bin/tape", "tests/**/*.test.js"],
{
stdio: "inherit",
shell: true,
}
);
if (result.status !== 0) {
process.exit(result.status || 1);
}
// Tests passed — ping Vigilmon
if (!heartbeatUrl) {
process.exit(0);
}
const url = new URL(heartbeatUrl);
const req = https.request(
{
hostname: url.hostname,
path: url.pathname + url.search,
method: "POST",
timeout: 10000,
},
(res) => {
res.resume();
console.log("[Vigilmon] Heartbeat ping sent.");
process.exit(0);
}
);
req.on("error", (err) => {
console.error("[Vigilmon] Heartbeat ping failed:", err.message);
process.exit(0); // tests passed; don't fail the build on a monitoring hiccup
});
req.on("timeout", () => {
req.destroy();
console.error("[Vigilmon] Heartbeat ping timed out.");
process.exit(0);
});
req.end();
Update package.json:
{
"scripts": {
"test": "node run-tests.js"
}
}
Step 3: Alternative — Use tap-parser to Inspect TAP Output
If you pipe Tape through a TAP consumer (e.g. tap-spec, tap-dot), you can parse the TAP stream directly and fire the heartbeat when you see # ok.
Install tap-parser:
npm install --save-dev tap-parser
Create vigilmon-tap-consumer.js:
// vigilmon-tap-consumer.js — pipe: tape tests/ | node vigilmon-tap-consumer.js
const { Parser } = require("tap-parser");
const https = require("https");
const heartbeatUrl = process.env.VIGILMON_HEARTBEAT_URL || "";
const parser = new Parser({ bail: false });
process.stdin.pipe(parser);
parser.pipe(process.stdout); // pass through so other consumers still see TAP
parser.on("complete", (results) => {
if (!heartbeatUrl) return;
if (!results.ok || results.fail > 0) return; // failures — let window expire
if (results.count === 0) return; // zero tests — treat as failure
const url = new URL(heartbeatUrl);
const req = https.request(
{
hostname: url.hostname,
path: url.pathname + url.search,
method: "POST",
timeout: 10000,
},
(res) => {
res.resume();
}
);
req.on("error", () => {});
req.on("timeout", () => req.destroy());
req.end();
});
Use it like this:
tape tests/**/*.test.js | node vigilmon-tap-consumer.js
Or in package.json:
{
"scripts": {
"test": "tape tests/**/*.test.js | node vigilmon-tap-consumer.js"
}
}
Step 4: Integrate with GitHub Actions
# .github/workflows/test.yml
name: Tape Tests
on:
push:
branches: [main, develop]
schedule:
- cron: "0 3 * * *" # nightly 03:00 UTC
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Tape
env:
VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_TAPE_HEARTBEAT_URL }}
run: npm test
# run-tests.js pings Vigilmon on exit code 0
Store the heartbeat URL in GitHub repo → Settings → Secrets and variables → Actions → New repository secret:
- Name:
VIGILMON_TAPE_HEARTBEAT_URL - Value: ping URL from Step 1
If you prefer to own the ping at the workflow level rather than in the runner script:
- name: Run Tape
run: npx tape tests/**/*.test.js
- name: Ping Vigilmon heartbeat
if: success()
run: |
curl -fsS -X POST "${{ secrets.VIGILMON_TAPE_HEARTBEAT_URL }}" \
--max-time 10 \
--retry 3 \
--retry-delay 2
Step 5: Integrate with GitLab CI
# .gitlab-ci.yml
test:tape:
image: node:20-alpine
stage: test
script:
- npm ci
- VIGILMON_HEARTBEAT_URL=$VIGILMON_TAPE_HEARTBEAT_URL npm test
only:
- main
- schedules
Set VIGILMON_TAPE_HEARTBEAT_URL in GitLab → Settings → CI/CD → Variables.
For the after-script approach:
test:tape:
image: node:20-alpine
stage: test
script:
- npm ci
- npx tape tests/**/*.test.js
after_script:
- |
if [ "$CI_JOB_STATUS" = "success" ]; then
curl -fsS -X POST "$VIGILMON_TAPE_HEARTBEAT_URL" --max-time 10 --retry 3
fi
Step 6: Monitor Multiple Test Suites Separately
Tape projects often split tests into unit, integration, and end-to-end files with different run schedules. Create one heartbeat monitor per group:
| Suite | Glob | Monitor name | Interval |
|---|---|---|---|
| Unit tests | tests/unit/**/*.test.js | Tape Unit — push | 2 h |
| Integration tests | tests/integration/**/*.test.js | Tape Integration — push | 4 h |
| E2E tests | tests/e2e/**/*.test.js | Tape E2E — nightly | 25 h |
# .github/workflows/test.yml (multi-suite)
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: "npm" }
- run: npm ci
- run: npx tape tests/unit/**/*.test.js
env:
VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_TAPE_UNIT_HEARTBEAT_URL }}
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: "npm" }
- run: npm ci
- run: npx tape tests/integration/**/*.test.js
env:
VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_TAPE_INTEGRATION_HEARTBEAT_URL }}
Step 7: Alert Channels
In Vigilmon, go to Notifications → New Channel and configure:
- Email — immediate alert when a heartbeat is missed
- Slack webhook — ping your
#devor#ci-alertschannel
When a Tape suite stops pinging:
🔴 MISSED HEARTBEAT: Tape Unit — push
Last ping: 3 hours ago
Expected interval: 2 hours
When it recovers:
✅ HEARTBEAT RECOVERED: Tape Unit — push
Gap: 3 hours
What You're Now Catching
| Silent failure mode | How Vigilmon detects it |
|---|---|
| CI path filter excludes test files | Tape never runs, no ping → alert |
| Import error before any test() calls | Process exits non-zero, no ping → alert |
| CI job disabled or never triggered | No ping within interval → alert |
| TAP consumer crashes and swallows exit code | Wrapper script captures raw exit code; no ping → alert |
| Tape hangs on unresolved promise | Process never exits, no ping → alert after grace period |
| Tests fail — non-zero TAP exit code | Runner script exits early, no ping → alert |
Tape is minimal by design — and so is the monitoring pattern it needs. A single wrapper script or TAP consumer gives you external proof that your tests ran and passed, every time they were supposed to.
Start monitoring your Tape pipelines today — register free at vigilmon.online.