Skip to content
Merged
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
12 changes: 10 additions & 2 deletions src/commands/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -261,7 +262,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];
// 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(', ')}`);
Expand All @@ -285,7 +290,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) {
const firstLine = String(item.audit.text || '').split(/\r?\n/, 1)[0];
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}`);
Expand Down
20 changes: 15 additions & 5 deletions src/commands/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import type { PluginContext } from '../plugin-types.js';
import type { ShowOptions } from '../cli-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 {
Expand All @@ -26,16 +27,25 @@ export default function register(ctx: PluginContext): void {
}

if (utils.isJsonMode()) {
const result: any = { success: true, workItem: item };
result.comments = db.getCommentsForWorkItem(normalizedId);
// 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) => {
const copy: any = Object.assign({}, src);
if (copy.audit === undefined || copy.audit === null) delete copy.audit;
return copy as WorkItem;
};

const result: ShowJsonOutput = { success: true, workItem: stripAudit(item) };
result.comments = db.getCommentsForWorkItem(normalizedId) as Comment[];
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;
Expand Down
15 changes: 15 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
46 changes: 46 additions & 0 deletions tests/cli/show-json-audit.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
91 changes: 91 additions & 0 deletions tests/unit/__snapshots__/human-audit-format.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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"
`;
49 changes: 49 additions & 0 deletions tests/unit/human-audit-format.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading