You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
--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
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: ):
...
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.
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_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_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_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.
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.go — primary 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.tpl — primary template-file reference (45 lines; same field-bullet style).
Sub-issue of #138. Sibling of #136 (
list), #203 (create). Hardened spec — do not re-derive decisions. Uses Go text templates viainternal/template.Template(the same engine used byazdo 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.The body of the work item is rendered with the
markdowntemplate func so HTML descriptions render as terminal-friendly markdown.Locked Decisions (do not re-derive)
workitemtracking.Client.GetWorkItem(not raw HTTP). Mock already generated atinternal/mocks/workitemtracking_client_mock.go.listandcreatesiblings.azdo boards work-item show Fabrikam 12345). The--idflag is not offered.az boards work-item showergonomics. The ID is a single integer — clean as a positional.util.FlagErrorfbefore the SDK call.Expand=Allis hardcoded in the SDK args. No--expandflag.showis for human inspection; the user always wants relations + fields + links.--commentsflag (default false) fetches the work item's comment thread viaGetCommentsand renders it inline. When false, thecomments:block is omitted.pr/view --comments. Comments are a second API call.--relationsflag (default false) controls whether therelations:block in the template is rendered. The data is always in the response (becauseExpand=All); the flag only controls template display.internal/template/template.gowith an//go:embed show.tplfile. Mirrorinternal/cmd/pr/view/view.goandview.tplstructure:bold,hyperlink,s,timeago,timefmt,markdown,pluck,join,truncate,stripprefix,tablerow,tablerender. Add template funcs as needed.pr/view.view,status. Primary name isshow.cmd.Use: "show [ORGANIZATION/]PROJECT ID",cmd.Aliases: []string{"view", "status"}.pr/viewaliasing pattern but withshowas the primary.*WorkItemtoopts.exporter.Write. No view struct.--rawflag dumps the full SDK work item (and comments, if--comments) withspew.Dumpto stderr for debugging.pr/view --raw.internal/cmd/boards/workitem/show. Reuse field helpers frominternal/cmd/boards/workitem/shared/fields.goif they exist after #203.GetWorkItemis already generated atinternal/mocks/workitemtracking_client_mock.go:2261-.... Do not regenerate.Command Signature
view,statuscobra.ExactArgs(2)—util.ExactArgs(2, "project and work item id required").args[0]→ project scope (viautil.ParseProjectScope);args[1]→ integer ID (parsed inRunE).IDis the second positional soazdo boards work-item show 12345is rejected (cobra'sExactArgs(2)triggers before the scope split).Flags
--comments(bool, default false)GetCommentscall--relations(bool, default false)relations:block--raw(bool)spew.Dumpof full SDK work item (and comments) to stderr--json/--jq/--templateutil.AddJSONFlagsJSON Output Contract
Pass the raw
*workitemtracking.WorkItemreturned by the SDK (Decision 9).Template Output Contract (
show.tpl)The default template renders (see
internal/cmd/pr/view/view.tplfor the established pattern):Command Wiring
internal/cmd/boards/workitem/showshow.go—NewCmd(ctx util.CmdContext) *cobra.Command+showOptions+runShowshow.tpl— Go text templateshow_test.go— table-driven gomock testsinternal/cmd/boards/workitem/workitem.goto addshowcmd "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/show"andcmd.AddCommand(showcmd.NewCmd(ctx)). Update theExampleblock.boards→work-item→show.API Surface
Reuse the already-vendored client. No new SDK clients required.
workitemtracking.Client.GetWorkItem→ Work Items - Get Work Item (REST 7.1)workitemtracking.Client.GetComments→ Comments - List (REST 7.1) (only when--comments)workitemtracking.WorkItemExpandValues.All="all"internal/template.Templatewithbold,hyperlink,s,timeago,timefmt,markdown,pluck,join,truncate,stripprefix,tablerow,tablerenderMock for
GetWorkItemandGetCommentsare already generated atinternal/mocks/workitemtracking_client_mock.go. No mock regeneration needed.Implementation Approach (TDD, reuse-first, minimal)
Phase 1 — RED (tests first). Mirror
setupFakeDepsfrominternal/cmd/boards/workitem/list/list_test.go:765-844. Addshow_test.gowith the following table-driven / behaviour tests, all usingt.Parallel()andgomock:TestNewCmd_RegistersAsShowLeaf— assertscmd.Name() == "show",cmd.Aliasescontainsviewandstatus,cmd.Usestarts withshow [ORGANIZATION/]PROJECT ID.TestNewCmd_RequiresTwoArgs— runs with[]string{"Fabrikam"}; assertscobra.ExactArgs(2)triggers "project and work item id required".TestRunShow_IDMustBeInteger— sets ID"abc"; assertsutil.FlagErrorfreturned.TestRunShow_IDMustBePositive— sets ID0and-1; assertsutil.FlagErrorfreturned.TestRunShow_BasicCall— sets[Fabrikam, 12345]; stubswit.EXPECT().GetWorkItem(gomock.Any(), gomock.Any()); asserts*args.Id == 12345,*args.Project == "Fabrikam",*args.Expand == "all".TestRunShow_CommentsFlag_TriggersGetComments— sets--comments; asserts bothGetWorkItemandGetCommentswere called; assertGetCommentsargs{Project: "Fabrikam", WorkItemId: 12345}.TestRunShow_CommentsFlag_DefaultDoesNotCallGetComments— default flags; assertsGetCommentswas not called (usewit.EXPECT().GetComments(...).Times(0)).TestRunShow_RelationsFlag_ControlsTemplateOnly— sets--relations; asserts the SDK call still happens withExpand=Allregardless 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 theurl:line uses ANSI hyperlink escape sequence.TestRunShow_TemplateOutput_AssignedTo_DisplayAndUnique— mocks returnFields: {System.AssignedTo: {displayName: "Alice", uniqueName: "alice@contoso.com"}}; asserts both render.TestRunShow_TemplateOutput_DescriptionMarkdown— mocks returnFields: {System.Description: "<p>Hello</p>"}; asserts the rendered output contains the markdown-converted text (not the raw HTML).TestRunShow_TemplateOutput_NoDescription— mocks return noSystem.Description; asserts thedescription:block showsNone given.TestRunShow_TemplateOutput_Tags— mocks returnFields: {System.Tags: "tag1; tag2"}; asserts both tags rendered.TestRunShow_TemplateOutput_NoTags— mocks return noSystem.Tags; asserts thetags:line is omitted.TestRunShow_TemplateOutput_RelationsIncluded— sets--relations; mocks returnRelations: [{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 returnComments: [...]; 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 containsid,rev,fields,relations,url,_links,commentVersionRef.TestRunShow_RawFlag— sets--raw; assertsspew.Dumpwas invoked.TestRunShow_ProjectScopeParsing— table-driven:myorg/Fabrikam,Fabrikam, invalid ("org/proj/extra"), empty.TestRunShow_InvalidProjectScope— assertsutil.FlagErrorWrapreturned.TestRunShow_ClientFactoryError— stubs factory to return error; asserts wrapped error.TestRunShow_SDKError— stubs SDK to return error; asserts wrapped error.TestRunShow_GetCommentsError— sets--comments; stubsGetCommentsto return error; asserts wrapped error.TestRunShow_OrganizationFromConfigDefault— when scopeArg isFabrikam(no org), assertsclientFact.WorkItemTracking(ctx, defaultOrg)is called with the configured default.Phase 2 — GREEN (minimal implementation). Strict reuse rules:
parseWorkItemID(raw string) (int, error)(~5 lines) for the integer-parse + positive check.util.ParseProjectScope,util.AddJSONFlags,util.FlagErrorf/FlagErrorWrap,util.ExactArgs,types.GetValue,types.ToPtr,ios.StartProgressIndicator/StopProgressIndicator,iostreams.Testas-is.shared.FieldString/shared.FieldIdentityDisplay(promoted from list.go in feat: Implementazdo boards work-item createcommand #203) forSystem.*field reads.internal/template.New(...).WithFuncs(...).Parse(show.tpl).ExecuteData(data)— exact same pattern asinternal/cmd/pr/view/view.go:483-549.ios.StartProgressIndicator()+defer ios.StopProgressIndicator(); callios.StopProgressIndicator()immediately before template execution.opts.exporter.Write(ios, res)passing the raw SDK*WorkItem; template viatemplate.New(...).ExecuteData(templateData{WorkItem: res, Comments: comments}).Target delta:
show.go≤ ~140 LOC,show.tpl≤ ~80 LOC,show_test.go≤ ~500 LOC (28 tests), parentworkitem.go+3 LOC,docs/boards_work-item_show.mdregenerated viamake docs. No changes tolist.go,create.go, orshared/*.go.Tooling and Verification Checklist
gofmt/gofumpton touched filesgo test ./internal/cmd/boards/workitem/...go test ./...make lintmake docsReference Existing Patterns
internal/cmd/pr/view/view.go— primary template-engine reference (Primary:view.go:483-549;viewOptionsstruct atview.go:23-31; template embed atview.go:33-34; JSON view struct atview.go:42-89).internal/cmd/pr/view/view.tpl— primary 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: Implementazdo boards work-item createcommand #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-844—setupFakeDeps/stub*fixture; copy structure.internal/cmd/boards/workitem/shared/fields.go—FieldString/FieldIdentityDisplay(reuse, do not reimplement).internal/mocks/workitemtracking_client_mock.go— mocks forGetWorkItemandGetComments(already generated, do not regenerate).internal/azdo/factory.go—ClientFactory().WorkItemTracking(...)accessor (reuse).internal/template/template.go— template engine + funcs (reuse, do not reimplement).References