Files
commitea/packages/core/src/agent/capture-work.test.ts
Croissant Le Doux 6e8a6a15bc feat: capture_work — braindump → decomposed issues → filed in gitea (P4)
The last big agent capability. In the Capture screen, a rough braindump runs real
big-model decomposition into a small, estimated issue set; you review/edit the
labels and approve, and the issues are opened in gitea. This is the one place the
big model earns its keep (docs/agent-tools.md).

core (@commitea/core):
- capture-work: PROPOSE_ISSUES_TOOL + CAPTURE_SYSTEM; captureWork(complete, dump)
  forces a single structured decomposition and returns validated issues; parseCaptureArgs
  drops blank titles + invalid est/p labels. ProposedIssue / CaptureProposal.
- gitea client: createIssue({title, body?, labelIds?}) → POST /issues, normalized.

app:
- model bridge model:capture runs captureWork on the (loaded) big model.
- gitea bridge gitea:createIssues opens each approved issue with its est/* + p/*
  labels (reusing the #41 label-id resolver — zero-pollution, no invented labels).
- Capture screen: when a model is configured, "Brew tickets" runs real capture and
  "Approve all" files the set; otherwise the scripted demo interview runs. Fixed a
  race — the brew handler re-checks model status at click time so a configured
  model never falls into the scripted path before status resolves.

Verified: 108 core tests green (7 capture + createIssue added), desktop typecheck
clean, 14 fixture e2e green. Gated live e2e against gemma-4-26b: the auth braindump
→ 3 real tickets ("Resolve token refresh + session staleness" est/3d p/1, "Fix
webhook double-firing" est/2d p/2, "Write auth setup docs" est/1d p/3), reviewable
and editable; Discard so the test files nothing (createIssue POST is unit-tested).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:20:00 -04:00

66 lines
2.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { CompletionResult } from './chat-client.js'
import { captureWork, parseCaptureArgs } from './capture-work.js'
describe('parseCaptureArgs', () => {
it('validates issues and keeps only real titles + valid labels', () => {
const p = parseCaptureArgs({
issues: [
{ title: 'Fix token refresh', body: 'dies silently', estimate: 'est/2d', priority: 'p/1' },
{ title: ' ', body: 'blank title dropped' },
{ title: 'Docs', estimate: 'est/9d', priority: 'urgent' }, // invalid labels → dropped to undefined
],
consequence: 'Beta slips a day',
})
expect(p.issues).toHaveLength(2)
expect(p.issues[0]).toEqual({ title: 'Fix token refresh', body: 'dies silently', estimate: 'est/2d', priority: 'p/1' })
expect(p.issues[1]).toEqual({ title: 'Docs', body: '', estimate: undefined, priority: undefined })
expect(p.consequence).toBe('Beta slips a day')
})
it('tolerates a missing/!array issues field', () => {
expect(parseCaptureArgs({}).issues).toEqual([])
expect(parseCaptureArgs({ issues: 'nope' }).issues).toEqual([])
})
})
describe('captureWork', () => {
it('forces propose_issues and returns the validated set', async () => {
const result: CompletionResult = {
content: '',
toolCalls: [
{
id: 'c1',
name: 'propose_issues',
arguments: JSON.stringify({
issues: [{ title: 'Retry token refresh with backoff', body: '', estimate: 'est/2d', priority: 'p/2' }],
consequence: 'no material shift',
}),
},
],
}
let sawTool = ''
const proposal = await captureWork(async (_m, tools) => {
sawTool = tools?.[0]?.name ?? ''
return result
}, 'auth is flaky, token refresh dies')
expect(sawTool).toBe('propose_issues')
expect(proposal.issues[0].title).toBe('Retry token refresh with backoff')
expect(proposal.consequence).toBe('no material shift')
})
it('returns an empty set when the model answers without the tool', async () => {
const proposal = await captureWork(async () => ({ content: 'I need more detail.', toolCalls: [] }), 'vague')
expect(proposal.issues).toEqual([])
})
it('survives malformed tool arguments', async () => {
const proposal = await captureWork(
async () => ({ content: '', toolCalls: [{ id: 'c1', name: 'propose_issues', arguments: '{not json' }] }),
'x',
)
expect(proposal.issues).toEqual([])
})
})