singularity-forge/src/tests/headless-events.test.ts
Juan Francisco Lebrero fe0f4f35e6 feat: add --events flag for JSONL stream filtering (#1000)
Allow orchestrators to filter the JSONL event stream to specific event
types, reducing stdout noise. The filter applies only to output —
internal processing (completion detection, supervised mode, answer
injection) is unaffected.

- New `--events <types>` flag (comma-separated, implies `--json`)
- Filter applied at stdout write point, all events still processed internally
- Updated help-text and SKILL.md with examples
- Tests for argument parsing and filter matching logic
2026-03-17 17:35:44 -06:00

151 lines
5.3 KiB
TypeScript

/**
* Tests for `--events` flag — JSONL event stream filtering.
*
* Validates argument parsing and the event filter logic used by
* the headless orchestrator to reduce stdout noise for orchestrators.
*
* Uses extracted parsing logic (mirrors headless.ts) to avoid
* transitive @gsd/native import that breaks in test environment.
*/
import test from 'node:test'
import assert from 'node:assert/strict'
// ─── Extracted parsing logic (mirrors headless.ts) ─────────────────────────
interface HeadlessOptions {
timeout: number
json: boolean
model?: string
command: string
commandArgs: string[]
context?: string
contextText?: string
auto?: boolean
verbose?: boolean
maxRestarts?: number
supervised?: boolean
responseTimeout?: number
answers?: string
eventFilter?: Set<string>
}
function parseHeadlessArgs(argv: string[]): HeadlessOptions {
const options: HeadlessOptions = {
timeout: 300_000,
json: false,
command: 'auto',
commandArgs: [],
}
const args = argv.slice(2)
let positionalStarted = false
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg === 'headless') continue
if (!positionalStarted && arg.startsWith('--')) {
if (arg === '--timeout' && i + 1 < args.length) {
options.timeout = parseInt(args[++i], 10)
} else if (arg === '--json') {
options.json = true
} else if (arg === '--model' && i + 1 < args.length) {
options.model = args[++i]
} else if (arg === '--context' && i + 1 < args.length) {
options.context = args[++i]
} else if (arg === '--context-text' && i + 1 < args.length) {
options.contextText = args[++i]
} else if (arg === '--auto') {
options.auto = true
} else if (arg === '--verbose') {
options.verbose = true
} else if (arg === '--max-restarts' && i + 1 < args.length) {
options.maxRestarts = parseInt(args[++i], 10)
} else if (arg === '--answers' && i + 1 < args.length) {
options.answers = args[++i]
} else if (arg === '--events' && i + 1 < args.length) {
options.eventFilter = new Set(args[++i].split(','))
options.json = true
} else if (arg === '--supervised') {
options.supervised = true
options.json = true
} else if (arg === '--response-timeout' && i + 1 < args.length) {
options.responseTimeout = parseInt(args[++i], 10)
}
} else if (!positionalStarted) {
positionalStarted = true
options.command = arg
} else {
options.commandArgs.push(arg)
}
}
return options
}
// ─── parseHeadlessArgs: --events flag ──────────────────────────────────────
test('--events parses comma-separated event types into a Set', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--events', 'agent_end,extension_ui_request', 'auto'])
assert.ok(opts.eventFilter instanceof Set)
assert.equal(opts.eventFilter!.size, 2)
assert.ok(opts.eventFilter!.has('agent_end'))
assert.ok(opts.eventFilter!.has('extension_ui_request'))
})
test('--events implies --json', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--events', 'agent_end', 'auto'])
assert.equal(opts.json, true)
})
test('--events with single type', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--events', 'agent_end', 'auto'])
assert.equal(opts.eventFilter!.size, 1)
assert.ok(opts.eventFilter!.has('agent_end'))
})
test('no --events flag means no filter', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--json', 'auto'])
assert.equal(opts.eventFilter, undefined)
})
test('--events with all common types', () => {
const types = 'agent_start,agent_end,tool_execution_start,tool_execution_end,extension_ui_request'
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--events', types, 'auto'])
assert.equal(opts.eventFilter!.size, 5)
})
test('--events combined with other flags', () => {
const opts = parseHeadlessArgs(['node', 'gsd', 'headless', '--timeout', '60000', '--events', 'agent_end', '--verbose', 'next'])
assert.equal(opts.timeout, 60000)
assert.equal(opts.verbose, true)
assert.equal(opts.command, 'next')
assert.ok(opts.eventFilter!.has('agent_end'))
assert.equal(opts.json, true)
})
// ─── Event filter matching logic ───────────────────────────────────────────
test('filter allows matching event types', () => {
const filter = new Set(['agent_end', 'extension_ui_request'])
assert.ok(filter.has('agent_end'))
assert.ok(filter.has('extension_ui_request'))
assert.ok(!filter.has('message_update'))
assert.ok(!filter.has('tool_execution_start'))
})
test('no filter allows all event types (undefined check)', () => {
const filter: Set<string> | undefined = undefined
const shouldEmit = (type: string) => !filter || filter.has(type)
assert.ok(shouldEmit('agent_end'))
assert.ok(shouldEmit('message_update'))
assert.ok(shouldEmit('tool_execution_start'))
})
test('empty filter blocks all events', () => {
const filter = new Set<string>()
const shouldEmit = (type: string) => !filter || filter.has(type)
assert.ok(!shouldEmit('agent_end'))
assert.ok(!shouldEmit('message_update'))
})