import { describe, expect, test } from 'bun:test'
import type { SDKMessage } from '../../../entrypoints/agentSdkTypes.js'
import {
AUTOFIX_RESULT_TAG,
extractAutofixResultFromLog,
} from '../extractAutofixResult.js'
function hookProgressMessage(stdout: string): SDKMessage {
return {
type: 'system',
subtype: 'hook_progress',
stdout,
} as unknown as SDKMessage
}
function assistantTextMessage(text: string): SDKMessage {
return {
type: 'assistant',
message: {
content: [{ type: 'text', text }],
},
} as unknown as SDKMessage
}
const sampleTag = (summary: string): string =>
`<${AUTOFIX_RESULT_TAG}>
42
${summary}
green
${summary}
${AUTOFIX_RESULT_TAG}>`
describe('extractAutofixResultFromLog', () => {
test('returns null on empty log', () => {
expect(extractAutofixResultFromLog([])).toBeNull()
})
test('returns null when no tag present', () => {
const log = [
assistantTextMessage('just some normal text without the tag'),
hookProgressMessage('hook output without tag'),
]
expect(extractAutofixResultFromLog(log)).toBeNull()
})
test('extracts from hook stdout', () => {
const tag = sampleTag('fixed lint error')
const log = [hookProgressMessage(`prefix\n${tag}\nsuffix`)]
const result = extractAutofixResultFromLog(log)
expect(result).toBe(tag)
})
test('extracts from assistant text', () => {
const tag = sampleTag('typecheck fixed')
const log = [assistantTextMessage(`Done!\n${tag}`)]
expect(extractAutofixResultFromLog(log)).toBe(tag)
})
test('extracts from hook_response subtype too', () => {
const tag = sampleTag('via hook_response')
const log = [
{
type: 'system',
subtype: 'hook_response',
stdout: tag,
} as unknown as SDKMessage,
]
expect(extractAutofixResultFromLog(log)).toBe(tag)
})
test('returns the latest tag when multiple appear in different messages', () => {
const older = sampleTag('older attempt')
const newer = sampleTag('newer attempt')
const log = [
assistantTextMessage(`first try\n${older}`),
assistantTextMessage(`retry\n${newer}`),
]
expect(extractAutofixResultFromLog(log)).toBe(newer)
})
test('returns null when open tag exists but close tag is missing (truncated)', () => {
const log = [
assistantTextMessage(
`<${AUTOFIX_RESULT_TAG}>\ngot cut off mid-write...`,
),
]
expect(extractAutofixResultFromLog(log)).toBeNull()
})
test('returns earlier complete tag when latest open tag is truncated within the same block', () => {
// Retry scenario: a full result was emitted, then a second result tag
// started but got cut off. We should surface the earlier complete pair
// rather than dropping the whole block.
const complete = sampleTag('earlier complete result')
const truncated = `<${AUTOFIX_RESULT_TAG}>\ntruncated retry...`
const log = [assistantTextMessage(`${complete}\n${truncated}`)]
expect(extractAutofixResultFromLog(log)).toBe(complete)
})
test('walks backwards so hook stdout from later in log wins over earlier assistant text', () => {
const earlier = sampleTag('via assistant first')
const later = sampleTag('via hook later')
const log = [
assistantTextMessage(`some output\n${earlier}`),
hookProgressMessage(later),
]
expect(extractAutofixResultFromLog(log)).toBe(later)
})
test('ignores tag-shaped strings that span across messages (no concatenation)', () => {
// Open tag in one message, close tag in another — should NOT be stitched.
const log = [
assistantTextMessage(`<${AUTOFIX_RESULT_TAG}>\npart 1`),
assistantTextMessage(`part 2\n${AUTOFIX_RESULT_TAG}>`),
]
expect(extractAutofixResultFromLog(log)).toBeNull()
})
test('extracts when assistant content is a string (not block array)', () => {
// Some SDK paths emit assistant content as a raw string instead of
// a content-block array. Current implementation skips those — verify
// graceful no-op rather than crash.
const log = [
{
type: 'assistant',
message: { content: sampleTag('string content') },
} as unknown as SDKMessage,
]
expect(extractAutofixResultFromLog(log)).toBeNull()
})
})