A live test-health dashboard: tracks pass/fail history for npm test, warns before a push with a known-failing last run, and opens a real panel with /test-pulse.

<h1 align="center">test-pulse</h1>
A live test-health dashboard for npm test:<br/> pass/fail history, a push gate, and a real panel - not just a toast.
Code that looks done and code that's actually been tested are not the same thing, and it's easy to lose track of which one you're looking at mid-session. When an agent, or you moving fast, is making changes across several files, "did the tests pass after that last edit" is a question that's cheap to skip and expensive to be wrong about once it's pushed.
test-pulse keeps the answer visible instead of implicit. It remembers the last real result, surfaces it in a real panel instead of a toast you glance past, and refuses git push outright when that last recorded run failed, with the reason and how long ago it failed.
One honest limit: the push gate only sees git push run as a Bash tool call inside Claude Code, and it only knows about runs it actually recorded. It protects your agent-driven workflow, not a git push you type directly in your own terminal, and it has nothing to say about a test suite it never saw run.
Three things, working off the same history:
npm test by hand or hit the dashboard's button - as pass/fail + duration, kept across sessions.git push: if the last recorded run failed, the push is refused outright, with the reason and how long ago it failed. No history at all (tests never run this session) is never itself a reason to block - only a known failure is./test-pulse: the detected test command, the last result, a dot-per-run history strip, and a "Run tests now" button that re-runs them and redraws live - not a toast, an actual docked interactive pane.$ /test-pulse
┌ Test Pulse ──────────────────────────────┐
│ npm test │
│ ✔ passing · 4m ago · took 2.3s │
│ ●●●●✕●● last 7 runs │
│ [ Run tests now ] │
└────────────────────────────────────────────┘
/plugin install test-pulse --marketplace thisismairaj/test-pulse
Works on any project with a real "test" script in package.json (not npm's default "echo \"Error: no test specified\" && exit 1" placeholder, which is treated as no test command configured). No other setup.
Five hooks, each doing one job:
session.start registers the /test-pulse slash command.command.run on test-pulse opens the dashboard as a real Pane ($.ui.open), not a toast - a docked panel the engine places and this plugin draws.tool.call on Bash (observer) times and records any command that matches the detected test command, so running tests by hand keeps the history current, not just the panel's own button.tool.call on Bash (a real gate, with .catch) refuses git push outright when the last recorded run failed - the push never executes.ui.render on { component: 'Pane', requestId: 'test-pulse' } draws the panel itself: history, last result, and a Button whose onPress calls $.tool.call to actually re-run the tests, then $.ui.invalidate to redraw with the new result.History lives in $.store (JSON, kept across sessions and hot reloads), the last 15 runs. The push gate fails open on its own errors - a bug in this plugin should never be the reason git push stops working.
Built and iterated inside a live Claude Code session using a dev-mod's hot reload - see token-weather for more on that workflow. No build step; the hooks module runs directly as TypeScript/JSX.
claude plugin validate .
MIT
hooks/register.tsx 163 lines1import type { Register } from 'claude-code'
2
3const PANE_ID = 'test-pulse'
4const HISTORY_KEY = 'history'
5const MAX_HISTORY = 15
6
7type Run = { pass: boolean; durationMs: number; at: string }
8
9async function detectTestCommand($: any): Promise<string | null> {
10 try {
11 const raw = await $.fs.read('package.json')
12 const pkg = JSON.parse(raw)
13 const script = pkg?.scripts?.test
14 if (typeof script !== 'string' || script.length === 0) return null
15 if (/no test specified/i.test(script)) return null
16 return 'npm test'
17 } catch {
18 return null
19 }
20}
21
22async function recordRun($: any, pass: boolean, durationMs: number) {
23 const history = ((await $.store.get(HISTORY_KEY)) as Run[] | undefined) ?? []
24 const updated = [...history, { pass, durationMs, at: new Date().toISOString() }].slice(-MAX_HISTORY)
25 await $.store.set(HISTORY_KEY, updated)
26}
27
28async function runTestsNow($: any, command: string) {
29 const start = Date.now()
30 const res = await $.tool.call({ tool: 'Bash', command })
31 const pass = !(res && typeof res === 'object' && 'isError' in res && res.isError)
32 await recordRun($, pass, Date.now() - start)
33 $.ui.invalidate('ui.render')
34}
35
36function timeAgo(iso: string): string {
37 const ms = Date.now() - new Date(iso).getTime()
38 const mins = Math.round(ms / 60000)
39 if (mins < 1) return 'just now'
40 if (mins < 60) return `${mins}m ago`
41 return `${Math.round(mins / 60)}h ago`
42}
43
44export const register: Register = on => {
45 on('session.start', async ($, e, next) => {
46 await $.command.register({
47 name: 'test-pulse',
48 description: 'Open the test-health dashboard.',
49 })
50 return next(e)
51 })
52
53 on('command.run', { command: 'test-pulse' }, async ($, e, next) => {
54 const opened = await $.ui.open({ id: PANE_ID, title: 'Test Pulse', closeOnEscape: true })
55 return {
56 text: opened.isPlaced
57 ? 'Opened the test-health dashboard.'
58 : 'Widen the terminal to see the test-pulse dashboard.',
59 }
60 })
61
62 // Record a run whenever the project's own test command happens to be run
63 // by hand too, not only from the dashboard's button, so the history stays
64 // current no matter how the tests actually got run.
65 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
66 const command = (e as { command?: string }).command
67 const testCommand = await detectTestCommand($)
68 const isTestRun = testCommand !== null && typeof command === 'string' && command.trim() === testCommand
69
70 const start = Date.now()
71 const res = await next(e)
72
73 if (isTestRun) {
74 try {
75 const pass = !(res && typeof res === 'object' && 'isError' in res && res.isError)
76 await recordRun($, pass, Date.now() - start)
77 } catch {
78 // never let history-tracking affect the real test result
79 }
80 }
81
82 return res
83 })
84
85 // A real gate: refuses `git push` outright when the last recorded test
86 // run failed. No history at all (tests never run this session) is not
87 // itself a reason to block - only a known failure is.
88 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
89 const command = (e as { command?: string }).command
90
91 if (typeof command === 'string' && /\bgit\s+push\b/.test(command)) {
92 try {
93 const history = ((await $.store.get(HISTORY_KEY)) as Run[] | undefined) ?? []
94 const last = history[history.length - 1]
95 if (last && !last.pass) {
96 return {
97 deny:
98 `test-pulse: the last recorded test run failed (${timeAgo(last.at)}). ` +
99 'Fix it, or run the tests again and let them pass, before pushing. ' +
100 'Open /test-pulse to see the history.',
101 }
102 }
103 } catch {
104 // scanning failed - never block a push over our own bug
105 }
106 }
107
108 return next(e)
109 }).catch(($, e, next) => next(e))
110
111 on('ui.render', { component: 'Pane', requestId: PANE_ID }, async ($, e, next) => {
112 const { Box, Text, Button } = $.ui.resolve(e)
113 const history = ((await $.store.get(HISTORY_KEY)) as Run[] | undefined) ?? []
114 const testCommand = await detectTestCommand($)
115 const last = history[history.length - 1]
116
117 if (testCommand === null) {
118 return (
119 <Box flexDirection="column">
120 <Text bold>Test Pulse</Text>
121 <Text dimColor>No "test" script found in this project's package.json.</Text>
122 </Box>
123 )
124 }
125
126 return (
127 <Box flexDirection="column">
128 <Text bold>Test Pulse</Text>
129 <Text dimColor>{testCommand}</Text>
130 <Text> </Text>
131 {last ? (
132 <Text color={last.pass ? 'green' : 'red'}>
133 {last.pass ? '✔ passing' : '✘ failing'} · {timeAgo(last.at)} · took{' '}
134 {(last.durationMs / 1000).toFixed(1)}s
135 </Text>
136 ) : (
137 <Text dimColor>No runs recorded yet.</Text>
138 )}
139 {history.length > 0 && (
140 <Text>
141 {history.map((run, i) => (
142 <Text key={`run-${i}`} color={run.pass ? 'green' : 'red'}>
143 {run.pass ? '●' : '✕'}
144 </Text>
145 ))}
146 <Text dimColor> last {history.length} run{history.length === 1 ? '' : 's'}</Text>
147 </Text>
148 )}
149 <Text> </Text>
150 <Button
151 key="run-now"
152 variant="primary"
153 onPress={() => {
154 void runTestsNow($, testCommand)
155 }}
156 >
157 Run tests now
158 </Button>
159 </Box>
160 )
161 })
162}
163