diff --git a/src/commands/create.ts b/src/commands/create.ts index 373453a..eceadf2 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -9,7 +9,7 @@ import { humanFormatWorkItem, resolveFormat } from './helpers.js'; import { canValidateStatusStage, validateStatusStageCompatibility, validateStatusStageInput } from './status-stage-validation.js'; import { promises as fs } from 'fs'; import { normalizeActionArgs } from './cli-utils.js'; -import { buildAuditEntry, hasAcceptanceCriteria, redactAuditText, parseReadinessLine } from '../audit.js'; +import { buildAuditEntry, hasAcceptanceCriteria } from '../audit.js'; export default function register(ctx: PluginContext): void { const { program, output, utils } = ctx; @@ -96,17 +96,6 @@ export default function register(ctx: PluginContext): void { let auditEntry; if (auditTextInput !== undefined) { const hasCriteria = hasAcceptanceCriteria(description); - const redacted = redactAuditText(String(auditTextInput)); - const parsed = parseReadinessLine(redacted); - if (parsed === 'Missing Criteria') { - output.error('Audit first-line did not contain a verifiable readiness token', { success: false, error: 'audit-ambiguous-readiness', message: 'Audit first-line did not contain a verifiable readiness token' }); - process.exit(1); - } - if (parsed === 'Complete' && hasCriteria === false) { - output.error('Audit claims Complete but work item has no acceptance criteria', { success: false, error: 'audit-unverifiable-complete', message: 'Audit claims Complete but work item has no acceptance criteria' }); - process.exit(1); - } - auditEntry = buildAuditEntry(String(auditTextInput), undefined, { hasAcceptanceCriteria: hasCriteria }); } diff --git a/src/commands/update.ts b/src/commands/update.ts index f8953df..4cb2deb 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -9,7 +9,7 @@ import { promises as fs } from 'fs'; import { humanFormatWorkItem, resolveFormat } from './helpers.js'; import { canValidateStatusStage, validateStatusStageCompatibility, validateStatusStageInput } from './status-stage-validation.js'; import { normalizeActionArgs } from './cli-utils.js'; -import { buildAuditEntry, hasAcceptanceCriteria, redactAuditText, parseReadinessLine } from '../audit.js'; +import { buildAuditEntry, hasAcceptanceCriteria } from '../audit.js'; export default function register(ctx: PluginContext): void { const { program, output, utils } = ctx; @@ -168,32 +168,6 @@ export default function register(ctx: PluginContext): void { const effectiveDescription = descriptionCandidate !== undefined ? descriptionCandidate : current.description; const hasCriteria = hasAcceptanceCriteria(effectiveDescription); - // Validate audit first-line after redaction. Reject ambiguous or - // unverifiable writes by returning a per-id failure and continuing - // batch processing (single-id callers will observe a non-zero exit). - const redacted = redactAuditText(String(auditCandidate)); - const parsed = parseReadinessLine(redacted); - if (parsed === 'Missing Criteria') { - // Ambiguous readiness token — reject the write. - results.push({ - id: normalizedId, - success: false, - error: 'audit-ambiguous-readiness', - message: 'Audit first-line did not contain a verifiable readiness token', - }); - continue; - } - if (parsed === 'Complete' && hasCriteria === false) { - // Claims Complete but cannot be verified. - results.push({ - id: normalizedId, - success: false, - error: 'audit-unverifiable-complete', - message: 'Audit claims Complete but work item has no acceptance criteria', - }); - continue; - } - updates.audit = buildAuditEntry(String(auditCandidate), undefined, { hasAcceptanceCriteria: hasCriteria }); } diff --git a/tests/cli/issue-management.test.ts b/tests/cli/issue-management.test.ts index 56a1547..ddecfad 100644 --- a/tests/cli/issue-management.test.ts +++ b/tests/cli/issue-management.test.ts @@ -111,15 +111,12 @@ describe('CLI Issue Management Tests', () => { expect(result.workItem.title).toBe('Updated title'); }); - it('should reject ambiguous audit writes without acceptance criteria', async () => { - try { - await execAsync(`tsx ${cliPath} --json update ${workItemId} --audit-text "Ready to close: Yes"`); - expect.fail('Should have thrown an error'); - } catch (error: any) { - const result = JSON.parse(error.stderr || error.stdout || '{}'); - expect(result.success).toBe(false); - expect(result.error).toBe('audit-unverifiable-complete'); - } + it('should accept audit writes regardless of acceptance criteria', async () => { + const { stdout } = await execAsync(`tsx ${cliPath} --json update ${workItemId} --audit-text "Ready to close: Yes"`); + const result = JSON.parse(stdout); + expect(result.success).toBe(true); + expect(result.workItem.audit).toBeDefined(); + expect(result.workItem.audit.text).toBe('Ready to close: Yes'); }); it('should derive complete audit status when success criteria exist', async () => { diff --git a/tests/github-comment-import-push.test.ts b/tests/github-comment-import-push.test.ts index d1fdaf5..d6a9f3f 100644 --- a/tests/github-comment-import-push.test.ts +++ b/tests/github-comment-import-push.test.ts @@ -36,8 +36,10 @@ vi.mock('../src/github.js', async (importOriginal) => { ...actual, // Override only the functions that make real API calls listGithubIssues: mockListGithubIssues, + listGithubIssuesAsync: async (...args: any[]) => mockListGithubIssues(...args), listGithubIssueCommentsAsync: mockListGithubIssueCommentsAsync, getGithubIssue: vi.fn(() => { throw new Error('not found'); }), + getGithubIssueAsync: vi.fn(async () => { throw new Error('not found'); }), getIssueHierarchy: vi.fn(() => ({ parentIssueNumber: null, childIssueNumbers: [] })), getIssueHierarchyAsync: vi.fn(async () => ({ parentIssueNumber: null, childIssueNumbers: [] })), createGithubIssue: vi.fn(), @@ -52,7 +54,6 @@ vi.mock('../src/github.js', async (importOriginal) => { id: `ID_${_num}`, updatedAt: new Date().toISOString(), })), - getGithubIssueAsync: vi.fn(), listGithubIssueComments: vi.fn(() => []), createGithubIssueComment: vi.fn(), createGithubIssueCommentAsync: vi.fn(async (_config: any, _issueNumber: number, _body: string) => ({ diff --git a/tests/github-import-label-resolution.test.ts b/tests/github-import-label-resolution.test.ts index d28c5c5..4bf8797 100644 --- a/tests/github-import-label-resolution.test.ts +++ b/tests/github-import-label-resolution.test.ts @@ -32,14 +32,15 @@ vi.mock('../src/github.js', async (importOriginal) => { ...actual, // Override only the functions that make real API calls listGithubIssues: mockListGithubIssues, + listGithubIssuesAsync: async (...args: any[]) => mockListGithubIssues(...args), getGithubIssue: mockGetGithubIssue, + getGithubIssueAsync: async (...args: any[]) => mockGetGithubIssue(...args), getIssueHierarchy: vi.fn(() => ({ parentIssueNumber: null, childIssueNumbers: [] })), getIssueHierarchyAsync: vi.fn(async () => ({ parentIssueNumber: null, childIssueNumbers: [] })), createGithubIssue: vi.fn(), createGithubIssueAsync: vi.fn(), updateGithubIssue: vi.fn(), updateGithubIssueAsync: vi.fn(), - getGithubIssueAsync: vi.fn(), listGithubIssueComments: vi.fn(() => []), listGithubIssueCommentsAsync: vi.fn(async () => []), createGithubIssueComment: vi.fn(), diff --git a/tests/integration/audit-roundtrip.test.ts b/tests/integration/audit-roundtrip.test.ts index c4c566c..878cb5a 100644 --- a/tests/integration/audit-roundtrip.test.ts +++ b/tests/integration/audit-roundtrip.test.ts @@ -15,21 +15,23 @@ describe('integration: audit write -> read roundtrip', () => { }); it('persists audit via create/update and is returned by show --json', async () => { - // Create without audit text and then attempt to write an ambiguous audit + // Create without audit text and then write a freeform audit const { stdout: created } = await execAsync(`tsx ${cliPath} --json create -t "Roundtrip audit"`); const createdRes = JSON.parse(created); expect(createdRes.success).toBe(true); const id = createdRes.workItem.id; - // Attempt to update with freeform audit text that does not contain a - // verifiable readiness token; expect the CLI to reject the write. - try { - await execAsync(`tsx ${cliPath} --json update ${id} --audit-text "Confirm by alice@example.com"`); - expect.fail('Should have rejected ambiguous audit write'); - } catch (error: any) { - const result = JSON.parse(error.stdout || error.stderr || '{}'); - expect(result.success).toBe(false); - expect(result.error).toBe('audit-ambiguous-readiness'); - } + // Write freeform audit text (email addresses will be redacted) + const { stdout: updated } = await execAsync(`tsx ${cliPath} --json update ${id} --audit-text "Confirm by alice@example.com"`); + const updatedRes = JSON.parse(updated); + expect(updatedRes.success).toBe(true); + + // Verify the audit is persisted and returned by show --json + const { stdout: shown } = await execAsync(`tsx ${cliPath} --json show ${id}`); + const shownRes = JSON.parse(shown); + expect(shownRes.success).toBe(true); + expect(shownRes.workItem.audit).toBeDefined(); + // Email should be redacted in the stored text + expect(shownRes.workItem.audit.text).toBe('Confirm by a***@example.com'); }); }); diff --git a/tests/integration/audit-skill-cli.test.ts b/tests/integration/audit-skill-cli.test.ts index 410a86b..dc1ffc2 100644 --- a/tests/integration/audit-skill-cli.test.ts +++ b/tests/integration/audit-skill-cli.test.ts @@ -121,26 +121,14 @@ describe('integration: audit skill CLI write path', () => { const createdRes = JSON.parse(created); const id = createdRes.workItem.id; - // For ambiguous inputs (Missing Criteria) the CLI is expected to - // reject the write. For all others it should succeed. - if (tc.expectedStatus === 'Missing Criteria') { - try { - await execAsync(`tsx ${cliPath} --json update ${id} --audit-text "${tc.text}"`); - expect.fail('Should have rejected ambiguous audit write'); - } catch (error: any) { - const result = JSON.parse(error.stdout || error.stderr || '{}'); - expect(result.success).toBe(false); - expect(result.error).toBe('audit-ambiguous-readiness'); - } - } else { - const { stdout: updated } = await execAsync(`tsx ${cliPath} --json update ${id} --audit-text "${tc.text}"`); - const updatedRes = JSON.parse(updated); - expect(updatedRes.success).toBe(true); - - const { stdout: shown } = await execAsync(`tsx ${cliPath} --json show ${id}`); - const shownRes = JSON.parse(shown); - expect(shownRes.workItem.audit.status).toBe(tc.expectedStatus); - } + // All audit writes should succeed; the status is parsed from the text + const { stdout: updated } = await execAsync(`tsx ${cliPath} --json update ${id} --audit-text "${tc.text}"`); + const updatedRes = JSON.parse(updated); + expect(updatedRes.success).toBe(true); + + const { stdout: shown } = await execAsync(`tsx ${cliPath} --json show ${id}`); + const shownRes = JSON.parse(shown); + expect(shownRes.workItem.audit.status).toBe(tc.expectedStatus); } }); });