Files
Jack Chen a6fac9a00e fix(release-bot): make weekly failures visible and diagnosable (#545)
Two consecutive Friday runs failed without anyone noticing, and the one
that was noticed reported only "OMA release analysis failed." with no
cause at all. Both gaps are closed here, with the smaller defects the
same investigation surfaced.

- Name the cause when a run fails without any agent failing. A token
  budget stop leaves later tasks skipped and every agent result
  successful, so the message degenerated to a bare sentence and sent the
  reader to the raw [OMA] progress log. It now carries the run status
  with token totals, the run's error kind and flags, and every unfinished
  task with its status.
- Report a failed run as a GitHub issue, opening one or commenting on the
  open one. A weekly job whose only failure signal is the Actions tab is
  a job nobody reads. It uses the built-in token, so it needs no App
  permission and no new secret.
- Define what earns a changelog entry in the release contract. The bot
  reads .github/RELEASING.md as evidence, so the rule applied by hand in
  #523 and again in #543 is now applied by the bot itself.
- Retry the two synthesis tasks once, so a provider timeout or 5xx no
  longer ends the run. They carry no tools and read a few thousand
  tokens. The evidence tasks stay at zero because a second attempt
  re-calls their tools and would trip the exactly-one-call coverage
  assertion. Schema failures stay terminal either way.
- Separate comment-only diffs from behavior changes in the compatibility
  auditor's instructions. It reported a JSDoc-only edit as a latency and
  cost risk in #543.
- Echo the three validation commands. They dominate the step's wall clock
  and produced no output at all, so a hang there was indistinguishable
  from a slow model call.

The whole-run token ceiling becomes a named, validated option so the
budget-stop path is testable.
2026-08-22 00:42:31 +08:00

47 lines
1.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { NodeCommandRunner } from '../src/command.js'
/** Records what the child wrote through, without swallowing the test reporter. */
function recordStdout(): { chunks: string[]; restore: () => void } {
const chunks: string[] = []
const original = process.stdout.write.bind(process.stdout)
const spy = vi.spyOn(process.stdout, 'write').mockImplementation(
((chunk: unknown, ...rest: unknown[]) => {
chunks.push(String(chunk))
return (original as (...args: unknown[]) => boolean)(chunk, ...rest)
}) as typeof process.stdout.write,
)
return { chunks, restore: () => spy.mockRestore() }
}
describe('command output echo', () => {
it('captures output and stays silent by default', async () => {
const { chunks, restore } = recordStdout()
try {
const result = await new NodeCommandRunner().run(
process.execPath,
['-e', 'console.log("release-bot-silent-sentinel")'],
)
expect(result.stdout.trim()).toBe('release-bot-silent-sentinel')
expect(chunks.filter(chunk => chunk.includes('release-bot-silent-sentinel'))).toEqual([])
} finally {
restore()
}
})
it('echoes to the parent stream while still capturing', async () => {
const { chunks, restore } = recordStdout()
try {
const result = await new NodeCommandRunner().run(
process.execPath,
['-e', 'console.log("release-bot-echo-sentinel")'],
{ echo: true },
)
expect(result.stdout.trim()).toBe('release-bot-echo-sentinel')
expect(chunks.some(chunk => chunk.includes('release-bot-echo-sentinel'))).toBe(true)
} finally {
restore()
}
})
})