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>
66 lines
2.5 KiB
TypeScript
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([])
|
|
})
|
|
})
|