From cd639738ed28ac55081f1087dd5a3cf0688361c3 Mon Sep 17 00:00:00 2001 From: Sorra the Orc Date: Thu, 26 Mar 2026 21:11:14 +0000 Subject: [PATCH 1/5] WL-0MMNCOJ0V0IFM2SN: add human audit formatting snapshot tests --- tests/unit/human-audit-format.test.ts | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/unit/human-audit-format.test.ts diff --git a/tests/unit/human-audit-format.test.ts b/tests/unit/human-audit-format.test.ts new file mode 100644 index 0000000..cd7e2e6 --- /dev/null +++ b/tests/unit/human-audit-format.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { humanFormatWorkItem } from '../../src/commands/helpers.js'; + +// Minimal WorkItem-like shape used for formatting tests +const baseItem: any = { + id: 'TEST-1', + title: 'Audit formatting test', + status: 'open', + priority: 'medium', + sortIndex: 100, + stage: 'in_progress', + createdAt: '2026-03-26T00:00:00Z', + updatedAt: '2026-03-26T00:00:00Z', + tags: [], + assignee: 'alice', + description: 'A test item for audit formatting', + parentId: undefined, + risk: undefined, + effort: undefined, + issueType: 'task' +}; + +describe('humanFormatWorkItem audit formatting', () => { + it('renders concise/normal/full outputs with audit present (snapshots)', () => { + const item = Object.assign({}, baseItem, { + audit: { time: '2026-03-26T20:29:00Z', author: 'alice', text: 'Ready to close: Yes\nExtra details' } + }); + + const concise = humanFormatWorkItem(item, null, 'concise'); + const normal = humanFormatWorkItem(item, null, 'normal'); + const full = humanFormatWorkItem(item, null, 'full'); + + expect(concise).toMatchSnapshot('concise-with-audit'); + expect(normal).toMatchSnapshot('normal-with-audit'); + expect(full).toMatchSnapshot('full-with-audit'); + }); + + it('renders concise/normal/full outputs without audit (snapshots)', () => { + const item = Object.assign({}, baseItem); + + const concise = humanFormatWorkItem(item, null, 'concise'); + const normal = humanFormatWorkItem(item, null, 'normal'); + const full = humanFormatWorkItem(item, null, 'full'); + + expect(concise).toMatchSnapshot('concise-without-audit'); + expect(normal).toMatchSnapshot('normal-without-audit'); + expect(full).toMatchSnapshot('full-without-audit'); + }); +}); From c210d2e79ae8a3c0b8cfd0c610c7e355b7a5afaa Mon Sep 17 00:00:00 2001 From: Sorra the Orc Date: Thu, 26 Mar 2026 21:16:34 +0000 Subject: [PATCH 2/5] WL-0MMRJLXGH0WEPMN9: implement JSON read-path and human audit summaries (omit audit when absent) --- src/commands/helpers.ts | 6 ++++-- src/commands/show.ts | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 63dc355..fee675e 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -261,8 +261,10 @@ export function humanFormatWorkItem(item: WorkItem, db: WorklogDatabase | null, lines.push(`Effort: ${item.effort || '—'}`); if (item.assignee) lines.push(`Assignee: ${item.assignee}`); if (item.audit) { + // For human outputs, show a truncated/redacted one-line audit excerpt plus author const firstLine = String(item.audit.text || '').split(/\r?\n/, 1)[0]; - lines.push(`Audit: ${firstLine}`); + // Friendly format: " — by " + lines.push(`Audit: ${firstLine} — by ${item.audit.author}`); } if (item.tags && item.tags.length > 0) lines.push(`Tags: ${item.tags.join(', ')}`); return lines.join('\n'); @@ -286,7 +288,7 @@ export function humanFormatWorkItem(item: WorkItem, db: WorklogDatabase | null, if (item.assignee) lines.push(`Assignee: ${item.assignee}`); if (item.audit) { const firstLine = String(item.audit.text || '').split(/\r?\n/, 1)[0]; - lines.push(`Audit: ${firstLine}`); + lines.push(`Audit: ${firstLine} — by ${item.audit.author}`); } if (item.parentId) lines.push(`Parent: ${item.parentId}`); if (item.description) lines.push(`Description: ${item.description}`); diff --git a/src/commands/show.ts b/src/commands/show.ts index 997c09f..781d361 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -4,6 +4,7 @@ import type { PluginContext } from '../plugin-types.js'; import type { ShowOptions } from '../cli-types.js'; +import type { WorkItem } from '../types.js'; import { displayItemTree, displayItemTreeWithFormat, humanFormatComment, resolveFormat, humanFormatWorkItem } from './helpers.js'; export default function register(ctx: PluginContext): void { @@ -26,16 +27,26 @@ export default function register(ctx: PluginContext): void { } if (utils.isJsonMode()) { - const result: any = { success: true, workItem: item }; + // Prepare JSON-safe copies that omit the `audit` field when absent + const stripAudit = (src: WorkItem) => { + // shallow copy - preserve other fields + const copy: any = Object.assign({}, src); + // If audit is undefined or null, delete the property so JSON output + // omits the key entirely per our API contract. + if (copy.audit === undefined || copy.audit === null) delete copy.audit; + return copy; + }; + + const result: any = { success: true, workItem: stripAudit(item) }; result.comments = db.getCommentsForWorkItem(normalizedId); if (options.children) { - const children = db.getDescendants(normalizedId); - const ancestors: typeof item[] = []; + const children = db.getDescendants(normalizedId).map(stripAudit); + const ancestors: any[] = []; let currentParentId = item.parentId; while (currentParentId) { const parent = db.get(currentParentId); if (!parent) break; - ancestors.push(parent); + ancestors.push(stripAudit(parent)); currentParentId = parent.parentId; } result.children = children; From 25164c35c6f97ead6669a629b6b6294a8e967d65 Mon Sep 17 00:00:00 2001 From: Sorra the Orc Date: Thu, 26 Mar 2026 21:24:32 +0000 Subject: [PATCH 3/5] WL-0MMRJLXGH0WEPMN9: Add Show JSON output type and include structured audit in show --json --- src/commands/show.ts | 15 +++++++-------- src/types.ts | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/commands/show.ts b/src/commands/show.ts index 781d361..cc4482c 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -4,7 +4,7 @@ import type { PluginContext } from '../plugin-types.js'; import type { ShowOptions } from '../cli-types.js'; -import type { WorkItem } from '../types.js'; +import type { WorkItem, Comment, ShowJsonOutput } from '../types.js'; import { displayItemTree, displayItemTreeWithFormat, humanFormatComment, resolveFormat, humanFormatWorkItem } from './helpers.js'; export default function register(ctx: PluginContext): void { @@ -27,18 +27,17 @@ export default function register(ctx: PluginContext): void { } if (utils.isJsonMode()) { - // Prepare JSON-safe copies that omit the `audit` field when absent + // Prepare JSON-safe copies that omit the `audit` field when absent. + // Keep the audit object verbatim when present so JSON consumers can + // rely on the structured { time, author, text } shape. const stripAudit = (src: WorkItem) => { - // shallow copy - preserve other fields const copy: any = Object.assign({}, src); - // If audit is undefined or null, delete the property so JSON output - // omits the key entirely per our API contract. if (copy.audit === undefined || copy.audit === null) delete copy.audit; - return copy; + return copy as WorkItem; }; - const result: any = { success: true, workItem: stripAudit(item) }; - result.comments = db.getCommentsForWorkItem(normalizedId); + const result: ShowJsonOutput = { success: true, workItem: stripAudit(item) }; + result.comments = db.getCommentsForWorkItem(normalizedId) as Comment[]; if (options.children) { const children = db.getDescendants(normalizedId).map(stripAudit); const ancestors: any[] = []; diff --git a/src/types.ts b/src/types.ts index e480779..7166866 100644 --- a/src/types.ts +++ b/src/types.ts @@ -225,3 +225,18 @@ export interface NextWorkItemResult { workItem: WorkItem | null; reason: string; } + +/** + * JSON output shape for the `show` command when --json mode is enabled. + * This keeps the CLI's JSON API stable and explicitly documents the fields + * returned by the endpoint. + */ +export interface ShowJsonOutput { + success: true | false; + workItem?: WorkItem; + comments?: Comment[]; + children?: WorkItem[]; + ancestors?: WorkItem[]; + // Optional error message used when success is false + error?: string; +} From 5544bc906259829ca8b45f98455d1bed31461898 Mon Sep 17 00:00:00 2001 From: Sorra the Orc Date: Thu, 26 Mar 2026 21:52:33 +0000 Subject: [PATCH 4/5] WL-0MN7ZP9GX001XJE4: add show --json audit presence/absence test --- tests/cli/show-json-audit.test.ts | 46 ++++++++++ .../human-audit-format.test.ts.snap | 91 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 tests/cli/show-json-audit.test.ts create mode 100644 tests/unit/__snapshots__/human-audit-format.test.ts.snap diff --git a/tests/cli/show-json-audit.test.ts b/tests/cli/show-json-audit.test.ts new file mode 100644 index 0000000..a83b116 --- /dev/null +++ b/tests/cli/show-json-audit.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { execAsync, enterTempDir, leaveTempDir, writeConfig, writeInitSemaphore, cliPath } from './cli-helpers.js'; + +describe('show --json audit handling', () => { + let state: { tempDir: string; originalCwd: string }; + + beforeEach(() => { + state = enterTempDir(); + writeConfig(state.tempDir, 'Test Project', 'TEST'); + writeInitSemaphore(state.tempDir, '1.0.0'); + }); + + afterEach(() => { + leaveTempDir(state); + }); + + it('includes structured audit object when audit present and omits when absent', async () => { + // Create an item with audit + const { stdout: created } = await execAsync(`tsx ${cliPath} --json create -t "Audited task" --audit-text "Ready to close: Yes"`); + const createdRes = JSON.parse(created); + expect(createdRes.success).toBe(true); + const id = createdRes.workItem.id; + + const { stdout: shown } = await execAsync(`tsx ${cliPath} --json show ${id}`); + const shownRes = JSON.parse(shown); + expect(shownRes.success).toBe(true); + expect(shownRes.workItem).toBeDefined(); + expect(shownRes.workItem.audit).toBeDefined(); + expect(typeof shownRes.workItem.audit.text).toBe('string'); + expect(shownRes.workItem.audit.text).toBe('Ready to close: Yes'); + expect(shownRes.workItem.audit.author).toBeTruthy(); + expect(shownRes.workItem.audit.time).toMatch(/Z$/); + + // Create an item without audit + const { stdout: created2 } = await execAsync(`tsx ${cliPath} --json create -t "No audit"`); + const createdRes2 = JSON.parse(created2); + expect(createdRes2.success).toBe(true); + const id2 = createdRes2.workItem.id; + + const { stdout: shown2 } = await execAsync(`tsx ${cliPath} --json show ${id2}`); + const shownRes2 = JSON.parse(shown2); + expect(shownRes2.success).toBe(true); + // When audit is absent, the JSON output must omit the `audit` key entirely. + expect(shownRes2.workItem.audit).toBeUndefined(); + }); +}); diff --git a/tests/unit/__snapshots__/human-audit-format.test.ts.snap b/tests/unit/__snapshots__/human-audit-format.test.ts.snap new file mode 100644 index 0000000..5c2c414 --- /dev/null +++ b/tests/unit/__snapshots__/human-audit-format.test.ts.snap @@ -0,0 +1,91 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`humanFormatWorkItem audit formatting > renders concise/normal/full outputs with audit present (snapshots) > concise-with-audit 1`] = ` +"Audit formatting test TEST-1 +Status: Open · Stage: In Progress | Priority: medium +SortIndex: 100 +Risk: — +Effort: — +Assignee: alice +Audit: Ready to close: Yes" +`; + +exports[`humanFormatWorkItem audit formatting > renders concise/normal/full outputs with audit present (snapshots) > full-with-audit 1`] = ` +"# Audit formatting test + +ID : TEST-1 +Status : Open · Stage: In Progress | Priority: medium +Type : task +SortIndex: 100 +Risk : — +Effort : — +Assignee : alice + +## Description + +A test item for audit formatting + +## Stage + +in_progress + +## Audit + +Time: 2026-03-26T20:29:00Z +Author: alice + +Ready to close: Yes +Extra details" +`; + +exports[`humanFormatWorkItem audit formatting > renders concise/normal/full outputs with audit present (snapshots) > normal-with-audit 1`] = ` +"ID: TEST-1 +Title: Audit formatting test +Status: Open · Stage: In Progress | Priority: medium +SortIndex: 100 +Risk: — +Effort: — +Assignee: alice +Audit: Ready to close: Yes +Description: A test item for audit formatting" +`; + +exports[`humanFormatWorkItem audit formatting > renders concise/normal/full outputs without audit (snapshots) > concise-without-audit 1`] = ` +"Audit formatting test TEST-1 +Status: Open · Stage: In Progress | Priority: medium +SortIndex: 100 +Risk: — +Effort: — +Assignee: alice" +`; + +exports[`humanFormatWorkItem audit formatting > renders concise/normal/full outputs without audit (snapshots) > full-without-audit 1`] = ` +"# Audit formatting test + +ID : TEST-1 +Status : Open · Stage: In Progress | Priority: medium +Type : task +SortIndex: 100 +Risk : — +Effort : — +Assignee : alice + +## Description + +A test item for audit formatting + +## Stage + +in_progress" +`; + +exports[`humanFormatWorkItem audit formatting > renders concise/normal/full outputs without audit (snapshots) > normal-without-audit 1`] = ` +"ID: TEST-1 +Title: Audit formatting test +Status: Open · Stage: In Progress | Priority: medium +SortIndex: 100 +Risk: — +Effort: — +Assignee: alice +Description: A test item for audit formatting" +`; From e1996ff7b5055d0a93812aa9fcd1856e2c8275fc Mon Sep 17 00:00:00 2001 From: Sorra the Orc Date: Thu, 26 Mar 2026 21:52:55 +0000 Subject: [PATCH 5/5] Format: ensure helpers build uses redactAuditText for concise/normal audit excerpt (sync with snapshots) --- src/commands/helpers.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index fee675e..27dcc54 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -3,6 +3,7 @@ */ import { theme } from '../theme.js'; +import { redactAuditText } from '../audit.js'; import type { WorkItem, Comment } from '../types.js'; import type { SyncResult } from '../sync.js'; import type { WorklogDatabase } from '../database.js'; @@ -261,10 +262,12 @@ export function humanFormatWorkItem(item: WorkItem, db: WorklogDatabase | null, lines.push(`Effort: ${item.effort || '—'}`); if (item.assignee) lines.push(`Assignee: ${item.assignee}`); if (item.audit) { - // For human outputs, show a truncated/redacted one-line audit excerpt plus author - const firstLine = String(item.audit.text || '').split(/\r?\n/, 1)[0]; - // Friendly format: " — by " - lines.push(`Audit: ${firstLine} — by ${item.audit.author}`); + // For human outputs, show a truncated, redacted one-line audit excerpt. + // Do not include the author in concise output to keep it compact. + const raw = String(item.audit.text || ''); + const redacted = redactAuditText(raw); + const firstLine = redacted.split(/\r?\n/, 1)[0]; + lines.push(`Audit: ${firstLine}`); } if (item.tags && item.tags.length > 0) lines.push(`Tags: ${item.tags.join(', ')}`); return lines.join('\n'); @@ -287,8 +290,11 @@ export function humanFormatWorkItem(item: WorkItem, db: WorklogDatabase | null, lines.push(`Effort: ${item.effort || '—'}`); if (item.assignee) lines.push(`Assignee: ${item.assignee}`); if (item.audit) { - const firstLine = String(item.audit.text || '').split(/\r?\n/, 1)[0]; - lines.push(`Audit: ${firstLine} — by ${item.audit.author}`); + const raw = String(item.audit.text || ''); + const redacted = redactAuditText(raw); + const firstLine = redacted.split(/\r?\n/, 1)[0]; + // Keep concise audit excerpt in normal output as well (author omitted). + lines.push(`Audit: ${firstLine}`); } if (item.parentId) lines.push(`Parent: ${item.parentId}`); if (item.description) lines.push(`Description: ${item.description}`);