Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
}

Expand Down
28 changes: 1 addition & 27 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
}

Expand Down
15 changes: 6 additions & 9 deletions tests/cli/issue-management.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion tests/github-comment-import-push.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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) => ({
Expand Down
3 changes: 2 additions & 1 deletion tests/github-import-label-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
24 changes: 13 additions & 11 deletions tests/integration/audit-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
28 changes: 8 additions & 20 deletions tests/integration/audit-skill-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
});
Loading