Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/tools/manual-kpi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { fileURLToPath } from 'node:url'

const toolUrl = new URL('./manual-kpi.tool.ts', import.meta.url).href
const root = fileURLToPath(new URL('../..', import.meta.url))

const SCRIPT = `
const { manualKpiTool } = await import(${JSON.stringify(toolUrl)})
process.stdout.write('RESULT:' + (await manualKpiTool.invoke({})))
`

// readline writes the prompt to stdout, which would corrupt the test runner's own
// channel — so the tool runs in a child, the way a workflow step runs it.
function collect(typed: string | null): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', SCRIPT], {
cwd: root,
env: { ...process.env, MANUAL_ACTIVITIES: '' },
stdio: [typed === null ? 'ignore' : 'pipe', 'pipe', 'pipe'],
})

let out = ''

child.stdout?.on('data', (chunk) => (out += chunk))
child.stdin?.end(typed ?? '')

const timer = setTimeout(() => {
child.kill('SIGKILL')
reject(new Error('collect_manual_kpi_input never settled'))
}, 60_000)

child.on('error', reject)
child.on('exit', () => {
clearTimeout(timer)
resolve(out.slice(out.indexOf('RESULT:') + 'RESULT:'.length))
})
})
}

const activitiesFrom = (out: string) => JSON.parse(out).activities

test('returns no activities when stdin is closed rather than waiting for input that can never arrive', async () => {
const out = await collect(null)

assert.ok(out.startsWith('{'), 'the tool must settle on a closed stdin, not leave the job hanging')
assert.deepEqual(activitiesFrom(out), [])
})

test('still records an activity that arrived before stdin ended', async () => {
assert.deepEqual(activitiesFrom(await collect('Reviewed 3 PRs\n')), ['Reviewed 3 PRs'])
})

test('still treats a blank line as the end of input', async () => {
assert.deepEqual(activitiesFrom(await collect('Paired on the auth bug\n\n')), ['Paired on the auth bug'])
})
6 changes: 4 additions & 2 deletions src/tools/manual-kpi.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ function promptUser(question: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })

return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close()
rl.on('close', () => resolve(''))

rl.question(question, (answer) => {
resolve(answer.trim())

rl.close()
})
})
}
Expand Down
Loading