Skip to content

feat: Implement azdo boards work-item show command #238

Description

@tmeckel

Sub-issue of #138. Sibling of #136 (list), #203 (create). Hardened spec — do not re-derive decisions. Uses Go text templates via internal/template.Template (the same engine used by azdo pr view).

Command Description

Display the details of a single Azure Boards work item by integer ID. The command fetches the work item with Expand=All (so relations, fields, and links are returned in one call) and renders it as a Go text template. Optionally pull the comment thread and render it inline.

GET https://dev.azure.com/{organization}/{project}/_apis/wit/workitems/{id}?$expand=All&api-version=7.1

The body of the work item is rendered with the markdown template func so HTML descriptions render as terminal-friendly markdown.

Locked Decisions (do not re-derive)

# Decision Rationale
1 Use the vendored SDK workitemtracking.Client.GetWorkItem (not raw HTTP). Mock already generated at internal/mocks/workitemtracking_client_mock.go. Consistent with list and create siblings.
2 The work item is identified by an integer positional ID (e.g. azdo boards work-item show Fabrikam 12345). The --id flag is not offered. Mirrors az boards work-item show ergonomics. The ID is a single integer — clean as a positional.
3 ID is required. Non-numeric or zero / negative values rejected with util.FlagErrorf before the SDK call. Cheap pre-flight validation; REST 4xx is unhelpful.
4 Expand=All is hardcoded in the SDK args. No --expand flag. show is for human inspection; the user always wants relations + fields + links.
5 --comments flag (default false) fetches the work item's comment thread via GetComments and renders it inline. When false, the comments: block is omitted. Mirrors pr/view --comments. Comments are a second API call.
6 --relations flag (default false) controls whether the relations: block in the template is rendered. The data is always in the response (because Expand=All); the flag only controls template display. Cheap display switch; avoids clutter when relations are noise.
7 Use the Go text template engine from internal/template/template.go with an //go:embed show.tpl file. Mirror internal/cmd/pr/view/view.go and view.tpl structure: bold, hyperlink, s, timeago, timefmt, markdown, pluck, join, truncate, stripprefix, tablerow, tablerender. Add template funcs as needed. The user explicitly requested template rendering like pr/view.
8 Aliases: view, status. Primary name is show. cmd.Use: "show [ORGANIZATION/]PROJECT ID", cmd.Aliases: []string{"view", "status"}. Mirrors the pr/view aliasing pattern but with show as the primary.
9 JSON output passes the raw SDK *WorkItem to opts.exporter.Write. No view struct. Symmetric with #203 Decision 7.
10 No confirmation prompt. Show is read-only. Show is non-destructive.
11 --raw flag dumps the full SDK work item (and comments, if --comments) with spew.Dump to stderr for debugging. Mirrors pr/view --raw.
12 No new SDK client, no new helper, no new package beyond internal/cmd/boards/workitem/show. Reuse field helpers from internal/cmd/boards/workitem/shared/fields.go if they exist after #203. Mandate: minimal code.
13 Mock for GetWorkItem is already generated at internal/mocks/workitemtracking_client_mock.go:2261-.... Do not regenerate. Verified.

Command Signature

azdo boards work-item show [ORGANIZATION/]PROJECT ID
  • Aliases: view, status
  • cobra.ExactArgs(2)util.ExactArgs(2, "project and work item id required").
  • Positional parsing: args[0] → project scope (via util.ParseProjectScope); args[1] → integer ID (parsed in RunE).
  • ID is the second positional so azdo boards work-item show 12345 is rejected (cobra's ExactArgs(2) triggers before the scope split).

Flags

Flag Maps to Notes
--comments (bool, default false) GetComments call When true, fetches and renders the comment thread
--relations (bool, default false) template switch When true, template renders relations: block
--raw (bool) debug dump spew.Dump of full SDK work item (and comments) to stderr
--json / --jq / --template util.AddJSONFlags JSON export of raw SDK work item

JSON Output Contract

util.AddJSONFlags(cmd, &opts.exporter, []string{
    "id", "rev", "fields", "relations", "url", "_links", "commentVersionRef",
})

Pass the raw *workitemtracking.WorkItem returned by the SDK (Decision 9).

Template Output Contract (show.tpl)

The default template renders (see internal/cmd/pr/view/view.tpl for the established pattern):

url:           
id:            
rev:           
type:          
state:         
reason:        
title:         
assigned to:    ()  if present
created by:     ()  if present
created on:     ()
changed on:     ()
area:          
iteration:     
tags:          ; ; ...             if non-empty
priority:        if present
severity:        if present

description:
  

[relations:]                                  (only when --relations)
  - : 
  - : 
  ...

[comments:]                                   (only when --comments)
  --------------------------------------------------
  Thread ID: 
  Status: 

   commented  (Type: ):
  
  ...

Command Wiring

  • Package path: internal/cmd/boards/workitem/show
  • Files:
    • show.goNewCmd(ctx util.CmdContext) *cobra.Command + showOptions + runShow
    • show.tpl — Go text template
    • show_test.go — table-driven gomock tests
  • Update internal/cmd/boards/workitem/workitem.go to add showcmd "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/show" and cmd.AddCommand(showcmd.NewCmd(ctx)). Update the Example block.
  • Existing higher-level parents must already remain wired: boardswork-itemshow.

API Surface

Reuse the already-vendored client. No new SDK clients required.

  • workitemtracking.Client.GetWorkItemWork Items - Get Work Item (REST 7.1)
  • workitemtracking.Client.GetCommentsComments - List (REST 7.1) (only when --comments)
  • Constant: workitemtracking.WorkItemExpandValues.All = "all"
  • Template engine: internal/template.Template with bold, hyperlink, s, timeago, timefmt, markdown, pluck, join, truncate, stripprefix, tablerow, tablerender

Mock for GetWorkItem and GetComments are already generated at internal/mocks/workitemtracking_client_mock.go. No mock regeneration needed.

Implementation Approach (TDD, reuse-first, minimal)

Phase 1 — RED (tests first). Mirror setupFakeDeps from internal/cmd/boards/workitem/list/list_test.go:765-844. Add show_test.go with the following table-driven / behaviour tests, all using t.Parallel() and gomock:

  • TestNewCmd_RegistersAsShowLeaf — asserts cmd.Name() == "show", cmd.Aliases contains view and status, cmd.Use starts with show [ORGANIZATION/]PROJECT ID.
  • TestNewCmd_RequiresTwoArgs — runs with []string{"Fabrikam"}; asserts cobra.ExactArgs(2) triggers "project and work item id required".
  • TestRunShow_IDMustBeInteger — sets ID "abc"; asserts util.FlagErrorf returned.
  • TestRunShow_IDMustBePositive — sets ID 0 and -1; asserts util.FlagErrorf returned.
  • TestRunShow_BasicCall — sets [Fabrikam, 12345]; stubs wit.EXPECT().GetWorkItem(gomock.Any(), gomock.Any()); asserts *args.Id == 12345, *args.Project == "Fabrikam", *args.Expand == "all".
  • TestRunShow_CommentsFlag_TriggersGetComments — sets --comments; asserts both GetWorkItem and GetComments were called; assert GetComments args {Project: "Fabrikam", WorkItemId: 12345}.
  • TestRunShow_CommentsFlag_DefaultDoesNotCallGetComments — default flags; asserts GetComments was not called (use wit.EXPECT().GetComments(...).Times(0)).
  • TestRunShow_RelationsFlag_ControlsTemplateOnly — sets --relations; asserts the SDK call still happens with Expand=All regardless of the flag (data is always fetched).
  • TestRunShow_TemplateOutput_BasicFields — mocks return *WorkItem{Id: 12345, Rev: 3, Fields: {System.Title: "Login broken", ...}}; asserts rendered output contains all field labels and values.
  • TestRunShow_TemplateOutput_Hyperlink — asserts the url: line uses ANSI hyperlink escape sequence.
  • TestRunShow_TemplateOutput_AssignedTo_DisplayAndUnique — mocks return Fields: {System.AssignedTo: {displayName: "Alice", uniqueName: "alice@contoso.com"}}; asserts both render.
  • TestRunShow_TemplateOutput_DescriptionMarkdown — mocks return Fields: {System.Description: "<p>Hello</p>"}; asserts the rendered output contains the markdown-converted text (not the raw HTML).
  • TestRunShow_TemplateOutput_NoDescription — mocks return no System.Description; asserts the description: block shows None given.
  • TestRunShow_TemplateOutput_Tags — mocks return Fields: {System.Tags: "tag1; tag2"}; asserts both tags rendered.
  • TestRunShow_TemplateOutput_NoTags — mocks return no System.Tags; asserts the tags: line is omitted.
  • TestRunShow_TemplateOutput_RelationsIncluded — sets --relations; mocks return Relations: [{Rel: "System.LinkTypes.Hierarchy-Forward", Url: "..."}]; asserts relations block rendered.
  • TestRunShow_TemplateOutput_RelationsOmitted_Default — default flags; mocks return relations; asserts relations block omitted.
  • TestRunShow_TemplateOutput_CommentsIncluded — sets --comments; mocks return Comments: [...]; asserts comments block rendered with author display name, timeago, and markdown content.
  • TestRunShow_TemplateOutput_CommentsOmitted_Default — default flags; mocks return no GetComments call; asserts comments block omitted.
  • TestRunShow_JSONOutput — sets --json; mocks return work item; asserts JSON contains id, rev, fields, relations, url, _links, commentVersionRef.
  • TestRunShow_RawFlag — sets --raw; asserts spew.Dump was invoked.
  • TestRunShow_ProjectScopeParsing — table-driven: myorg/Fabrikam, Fabrikam, invalid ("org/proj/extra"), empty.
  • TestRunShow_InvalidProjectScope — asserts util.FlagErrorWrap returned.
  • TestRunShow_ClientFactoryError — stubs factory to return error; asserts wrapped error.
  • TestRunShow_SDKError — stubs SDK to return error; asserts wrapped error.
  • TestRunShow_GetCommentsError — sets --comments; stubs GetComments to return error; asserts wrapped error.
  • TestRunShow_OrganizationFromConfigDefault — when scopeArg is Fabrikam (no org), asserts clientFact.WorkItemTracking(ctx, defaultOrg) is called with the configured default.

Phase 2 — GREEN (minimal implementation). Strict reuse rules:

  • No new helpers beyond the inline parseWorkItemID(raw string) (int, error) (~5 lines) for the integer-parse + positive check.
  • Reuse util.ParseProjectScope, util.AddJSONFlags, util.FlagErrorf/FlagErrorWrap, util.ExactArgs, types.GetValue, types.ToPtr, ios.StartProgressIndicator/StopProgressIndicator, iostreams.Test as-is.
  • Reuse shared.FieldString / shared.FieldIdentityDisplay (promoted from list.go in feat: Implement azdo boards work-item create command #203) for System.* field reads.
  • Reuse internal/template.New(...).WithFuncs(...).Parse(show.tpl).ExecuteData(data) — exact same pattern as internal/cmd/pr/view/view.go:483-549.
  • Progress indicator: ios.StartProgressIndicator() + defer ios.StopProgressIndicator(); call ios.StopProgressIndicator() immediately before template execution.
  • Output split: JSON via opts.exporter.Write(ios, res) passing the raw SDK *WorkItem; template via template.New(...).ExecuteData(templateData{WorkItem: res, Comments: comments}).
  • Debug log at the point of the SDK call: organization, project, workItemId, expand, fetchComments.

Target delta: show.go ≤ ~140 LOC, show.tpl ≤ ~80 LOC, show_test.go ≤ ~500 LOC (28 tests), parent workitem.go +3 LOC, docs/boards_work-item_show.md regenerated via make docs. No changes to list.go, create.go, or shared/*.go.

Tooling and Verification Checklist

  • Run gofmt / gofumpt on touched files
  • go test ./internal/cmd/boards/workitem/...
  • go test ./...
  • make lint
  • make docs

Reference Existing Patterns

  • internal/cmd/pr/view/view.goprimary template-engine reference (Primary: view.go:483-549; viewOptions struct at view.go:23-31; template embed at view.go:33-34; JSON view struct at view.go:42-89).
  • internal/cmd/pr/view/view.tplprimary template-file reference (45 lines; same field-bullet style).
  • internal/cmd/boards/workitem/list/list.go — JSON/table split pattern, WIQL-builder, batch-fetch pattern.
  • internal/cmd/boards/workitem/create/create.go (feat: Implement azdo boards work-item create command #203) — closest sibling; mirror JSON/table split, raw-SDK JSON output, progress lifecycle, shared helper reuse.
  • internal/cmd/boards/workitem/list/list_test.go:765-844setupFakeDeps / stub* fixture; copy structure.
  • internal/cmd/boards/workitem/shared/fields.goFieldString / FieldIdentityDisplay (reuse, do not reimplement).
  • internal/mocks/workitemtracking_client_mock.go — mocks for GetWorkItem and GetComments (already generated, do not regenerate).
  • internal/azdo/factory.goClientFactory().WorkItemTracking(...) accessor (reuse).
  • internal/template/template.go — template engine + funcs (reuse, do not reimplement).

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions