Sub-issue of new boards work-item relation umbrella. Hardened spec — do not re-derive decisions. Sibling of #271 (add), #272 (list-type), #274 (show).
Command Description
Detach a relation (link) from one Azure Boards work item to one or more target work items. Mirrors az boards work-item relation remove. The command builds a JSON-Patch document containing remove operations on /relations/{index} and sends it to the server.
The REST surface is Work Items - Update (REST 7.1):
PATCH https://dev.azure.com/{organization}/_apis/wit/workitems/{id}?api-version=7.1-preview.3
Content-Type: application/json-patch+json
The Python reference implementation is at azure-devops/azext_devops/dev/boards/relations.py:60-91 (remove_relation function). The function:
- Fetches the source work item with
expand='All'.
- For each target ID, fetches the target work item to obtain its
url.
- Walks the source's
relations array, finding the entry whose rel matches the friendly relation type and whose url matches the target's URL; collects /relations/{index} remove ops.
- Fails with a CLI error if the number of
remove ops does not match the number of target IDs requested (Decision 15).
- Sends the PATCH.
- Re-fetches with
expand='All' to populate the response.
This is a destructive operation. The Python's commands.py:97 adds a confirmation prompt: 'Are you sure you want to remove this relation(s)?'.
Locked Decisions
| # |
Decision |
Rationale |
| 1 |
Org-scoped target. Use: "remove [ORGANIZATION/]ID". The source work item ID is unique within the organization. |
Mirrors az boards work-item relation remove. Symmetric with #271 (add). |
| 2 |
Aliases: []string{"r", "rm"}. |
Per AGENTS.md delete command convention. |
| 3 |
cobra.ExactArgs(1). |
One positional target: the source work item ID. |
| 4 |
Parse the single positional with util.ParseTargetWithDefaultOrganization(ctx, args[0]). scope.Targets[0] is the ID string. |
Existing helper handles 1- and 2-segment forms. Symmetric with #271 (add). |
| 5 |
No --project flag. |
Matches az boards work-item relation remove. |
| 6 |
ID must parse as a positive integer. If strconv.Atoi(scope.Targets[0]) fails or returns <= 0, return util.FlagErrorf("work item ID must be a positive integer; got %q", scope.Targets[0]) and do not call the SDK. |
Symmetric with #271 Decision 6, #269 Decision 14, #270 Decision 6. |
| 7 |
--relation-type is a single friendly name (e.g., parent, child). Case-insensitive match against the list returned by GetRelationTypes. |
Mirrors Python's get_system_relation_name. |
| 8 |
Invalid relation type surfaces a CLI error with the exact Python message: --relation-type is not valid. Use "azdo boards work-item relation list-type" command to list possible relation types in your project. |
Mirrors get_system_relation_name. |
| 9 |
--target-id is repeatable; multiple invocations concatenate. Each ID must parse as a positive integer. |
Mirrors Python's comma-separated form via target_id.split(',') plus the Go-native repeatable-flag form. |
| 10 |
--target-id is required (no --target-url support — Python only supports IDs in remove). |
Mirrors Python's remove_relation(id, relation_type, target_id, ...). |
| 11 |
For each target ID, call GetWorkItem(id) to obtain its url and use that URL when matching relations. |
Mirrors Python's client.get_work_item(target_work_item_id, expand='All'). |
| 12 |
Fetch the source work item with expand='All' to read its current relations array. |
Mirrors Python. |
| 13 |
Walk the source's relations array, finding each entry whose Rel matches the relation type's referenceName AND whose Url matches the target's URL. Collect a /relations/{index} remove op for each match. Indices are computed before any modification; the indices are positional in the SDK's relations slice. |
Mirrors Python's if relation.rel == relation_type_system_name and relation.url == target_work_item_url. |
| 14 |
Patch ops are applied in reverse index order (highest index first) so that earlier indices remain valid after each op. Test asserts this order. |
Mirrors Python's for relation in main_work_item.relations: ... index = index + 1 — but with reverse to avoid index drift. Decision 14 supersedes the Python's forward order: applying forward would shift indices after each removal, causing later ops to target the wrong relations. |
| 15 |
Validate: number of remove ops must equal number of target IDs requested. If fewer matches are found, return util.FlagErrorf with the exact Python message: "Id(s) supplied in --target-id is not valid". SDK is not called for UpdateWorkItem. |
Mirrors Python's if len(patch_document) != len(target_work_item_ids): raise CLIError('Id(s) supplied in --target-id is not valid'). |
| 16 |
Destructive — confirmation prompt is required with the exact Python prompt: Are you sure you want to remove this relation(s)?. Skip with --yes. On cancel, return util.ErrCancel. |
Mirrors Python's confirmation='Are you sure you want to remove this relation(s)?'. |
| 17 |
JSON output passes the raw SDK *WorkItem (the post-PATCH GetWorkItem with expand=All) to opts.exporter.Write. |
Mirrors #271 (add). |
| 18 |
Default output: table with columns TYPE, URL (mirroring the add leaf). |
Mirrors Python's transform_work_item_relations and #271's Table Output Contract. |
| 19 |
No --expand flag. All responses use expand='All' internally. |
Mirrors #271 Decision 12. |
| 20 |
No --target-url flag. Python's remove_relation does not support URLs. |
Mirrors Python. |
| 21 |
Uses vendored workitemtracking.Client.{UpdateWorkItem, GetWorkItem, GetRelationTypes}. |
Vendor verified. |
| 22 |
No new mocks needed. |
Already generated. |
| 23 |
No go mod tidy / go mod vendor / scripts/generate_mocks.sh work. |
|
| 24 |
No new shared helper beyond what the umbrella introduces. The RemoveRelationPatchOps helper (in internal/cmd/boards/workitem/relation/shared/relation.go) returns the list of /relations/{index} ops and the matches count, but the leaf owns the policy of when to error (Decision 15). |
Keep the leaf logic visible. |
| 25 |
Empty source work item relations (no relations at all) returns util.FlagErrorf("Id(s) supplied in --target-id is not valid") per Decision 15 (zero matches != len(targets)). |
Mirrors Python's if main_work_item.relations: guard, which falls through to the count check. |
| 26 |
No mutual-exclusion between --target-id and --target-url — --target-url is simply not supported (Decision 20). |
Matches Python surface. |
Command Signature
var removeCmd = &cobra.Command{
Use: "remove [ORGANIZATION/]ID",
Aliases: []string{"r", "rm"},
Short: "Remove a relation(s) from a work item.",
Long: heredoc.Doc(`
Detach one or more relations from an existing work item. The relation
type must be one of the friendly names returned by 'list-type'.
Targets are specified by work item ID.
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runRemove(cmd.Context(), opts, args[0])
},
}
Flags
| Flag |
Notes |
--id |
not a flag — positional [ORGANIZATION/]ID. |
--relation-type |
Required. Case-insensitive. |
--target-id (repeatable) |
Required. |
--yes |
Skip the confirmation prompt. |
--organization |
Default org from config. |
Also add util.AddJSONFlags(cmd, &opts.exporter, []string{"id", "rev", "fields", "url", "_links", "relations", "commentVersionRef"}).
Code Skeleton (canonical, copy verbatim)
§1. removeOptions struct
type removeOptions struct {
targetArg string
relationType string // --relation-type
targetIDs []string // --target-id (repeatable; comma-separated also accepted)
yes bool // --yes
exporter util.Exporter
}
§2. runRemove skeleton
func runRemove(cmdCtx util.CmdContext, opts *removeOptions, targetArg string) error {
ios, err := cmdCtx.IOStreams()
if err != nil { return err }
ios.StartProgressIndicator()
defer ios.StopProgressIndicator()
scope, err := util.ParseTargetWithDefaultOrganization(cmdCtx, targetArg)
if err != nil { return util.FlagErrorWrap(err) }
id, err := strconv.Atoi(scope.Targets[0])
if err != nil || id <= 0 {
return util.FlagErrorf("work item ID must be a positive integer; got %q", scope.Targets[0])
}
targetIDs, err := util.SplitAndTrimCSV(opts.targetIDs)
if err != nil { return util.FlagErrorWrap(err) }
if len(targetIDs) == 0 {
return util.FlagErrorf("--target-id must be provided")
}
for _, tid := range targetIDs {
n, err := strconv.Atoi(tid)
if err != nil || n <= 0 {
return util.FlagErrorf("target work item ID must be a positive integer; got %q", tid)
}
}
// Confirmation prompt (skipped with --yes).
if !opts.yes {
confirmed, err := cmdCtx.Prompter().Confirm("Are you sure you want to remove this relation(s)?", false)
if err != nil { return err }
if !confirmed { return util.ErrCancel }
}
wit, err := cmdCtx.ClientFactory().WorkItemTracking(cmdCtx.Context(), scope.Organization)
if err != nil { return err }
relRefName, err := shared.ResolveRelationType(cmdCtx.Context(), wit, opts.relationType)
if err != nil { return util.FlagErrorWrap(err) }
// Resolve target IDs to URLs.
targetURLs := make(map[string]struct{}, len(targetIDs))
for _, tid := range targetIDs {
n, _ := strconv.Atoi(tid)
target, err := wit.GetWorkItem(cmdCtx.Context(), workitemtracking.GetWorkItemArgs{Id: &n})
if err != nil {
return fmt.Errorf("failed to resolve target work item %d: %w", n, err)
}
if target.Url == nil || *target.Url == "" {
return fmt.Errorf("target work item %d has no URL; cannot remove relation", n)
}
targetURLs[*target.Url] = struct{}{}
}
// Fetch the source work item to find matching relations.
expand := workitemtracking.WorkItemExpandValues.All
src, err := wit.GetWorkItem(cmdCtx.Context(), workitemtracking.GetWorkItemArgs{
Id: &id,
Expand: &expand,
})
if err != nil { return err }
// Build a list of indices in reverse order (Decision 14).
indices := []int{}
if src.Relations != nil {
for i, rel := range *src.Relations {
if rel.Rel == nil || rel.Url == nil { continue }
if *rel.Rel != relRefName { continue }
if _, ok := targetURLs[*rel.Url]; !ok { continue }
indices = append(indices, i)
}
}
sort.Sort(sort.Reverse(sort.IntSlice(indices)))
if len(indices) != len(targetIDs) {
return util.FlagErrorf("Id(s) supplied in --target-id is not valid")
}
remove := webapi.OperationValues.Remove
doc := []webapi.JsonPatchOperation{}
for _, idx := range indices {
p := fmt.Sprintf("/relations/%d", idx)
doc = append(doc, webapi.JsonPatchOperation{Op: &remove, Path: &p})
}
_, err = wit.UpdateWorkItem(cmdCtx.Context(), workitemtracking.UpdateWorkItemArgs{
Document: &doc,
Id: &id,
})
if err != nil { return err }
populated, err := wit.GetWorkItem(cmdCtx.Context(), workitemtracking.GetWorkItemArgs{
Id: &id,
Expand: &expand,
})
if err != nil { return err }
if opts.exporter != nil {
return opts.exporter.Write(ios, populated)
}
tp, err := cmdCtx.Printer("list")
if err != nil { return err }
tp.AddColumns("TYPE", "URL")
if populated.Relations != nil {
for _, rel := range *populated.Relations {
tp.AddField(types.GetValue(rel.Rel, ""))
tp.AddField(types.GetValue(rel.Url, ""))
tp.EndRow()
}
}
return tp.Render()
}
JSON Output Contract
Pass the raw *workitemtracking.WorkItem (post-PATCH) to opts.exporter.Write. No view struct.
JSON fields exposed: id, rev, fields, url, _links, relations, commentVersionRef.
Table Output Contract
Use ctx.Printer("list") with 2 columns: TYPE, URL. Same as #271 (add).
Command Wiring
- Create
internal/cmd/boards/workitem/relation/remove/remove.go with package remove, factory func NewCmd(ctx util.CmdContext) *cobra.Command.
- Add
import "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/relation/remove" to internal/cmd/boards/workitem/relation/relation.go.
- Register in
relation.go with cmd.AddCommand(remove.NewCmd(ctx)).
- Reuse
internal/cmd/boards/workitem/relation/shared/relation.go (introduced by the umbrella) — shared.ResolveRelationType.
API Surface
Vendored SDK calls (from vendor/.../v7/workitemtracking/client.go):
// 1. Resolve friendly relation type
relTypes, _ := wit.GetRelationTypes(ctx, workitemtracking.GetRelationTypesArgs{})
// 2. Resolve each target ID to its URL
target, _ := wit.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{Id: &n})
// 3. Fetch the source work item
expand := workitemtracking.WorkItemExpandValues.All
src, _ := wit.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{Id: &id, Expand: &expand})
// 4. PATCH
_, _ = wit.UpdateWorkItem(ctx, workitemtracking.UpdateWorkItemArgs{Document: &doc, Id: &id})
// 5. Re-fetch with expand=All
populated, _ := wit.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{Id: &id, Expand: &expand})
Reference Existing Patterns
Reference implementations (for UX parity only):
- AzDO Extension
azure-devops/azext_devops/dev/boards/relations.py:60-91 - remove_relation function. Confirms: confirmation prompt, target-ID-only support, count-validation message.
- AzDO Extension
azure-devops/azext_devops/dev/boards/commands.py:97 - confirmation='Are you sure you want to remove this relation(s)?'.
- AzDO MCP Server (TypeScript): no dedicated remove-relation tool.
TDD Test Plan
| Test name |
Scenario |
Expected |
TestNewCmd_remove |
Inspect the command struct. |
Use == "remove [ORGANIZATION/]ID", Aliases == ["r", "rm"], Args == cobra.ExactArgs(1). |
Test_runRemove_minimal |
--relation-type parent --target-id 2. Source has 1 relation matching. |
One remove op on /relations/0. |
Test_runRemove_multipleTargets |
--target-id 2 --target-id 3; source has matching relations at indices [0, 2]. |
Two remove ops in reverse order: /relations/2 first, then /relations/0. |
Test_runRemove_commaSeparated |
--target-id 2,3. |
Same as multipleTargets. |
Test_runRemove_noMatch |
Source has no relations. |
Returns util.FlagErrorf with "Id(s) supplied in --target-id is not valid". SDK's UpdateWorkItem is not called. |
Test_runRemove_partialMatch |
2 targets requested, only 1 relation matches. |
Returns util.FlagErrorf with "Id(s) supplied in --target-id is not valid". SDK's UpdateWorkItem is not called. |
Test_runRemove_invalidRelationType |
--relation-type bogus. |
Returns util.FlagErrorf with "--relation-type is not valid...". SDK is not called. |
Test_runRemove_invalidSourceID |
Positional is "abc". |
Returns util.FlagErrorf with "work item ID must be a positive integer". SDK is not called. |
Test_runRemove_zeroSourceID |
Positional is "0". |
Returns util.FlagErrorf. SDK is not called. |
Test_runRemove_negativeSourceID |
Positional is "-5". |
Returns util.FlagErrorf. SDK is not called. |
Test_runRemove_invalidTargetID |
--target-id abc. |
Returns util.FlagErrorf with "target work item ID must be a positive integer". SDK is not called. |
Test_runRemove_noTargets |
No --target-id. |
Returns util.FlagErrorf with "--target-id must be provided". SDK is not called. |
Test_runRemove_targetIDNotFound |
Mock GetWorkItem for the target returns 404. |
Returns wrapped error; PATCH is not sent. |
Test_runRemove_confirmationPrompt_yes |
--yes set. |
Prompter.Confirm is not called; PATCH proceeds. |
Test_runRemove_confirmationPrompt_userConfirms |
No --yes; mock Prompter.Confirm returns (true, nil). |
PATCH proceeds. |
Test_runRemove_confirmationPrompt_userDeclines |
No --yes; mock Prompter.Confirm returns (false, nil). |
Returns util.ErrCancel. SDK is not called. |
Test_runRemove_orgScopeOnly |
Positional is "1234". |
args.Id == ptr(1234), scope.Organization is the default. |
Test_runRemove_explicitOrg |
Positional is "myorg/1234". |
args.Id == ptr(1234), scope.Organization is "myorg". |
Test_runRemove_APIError |
Mock UpdateWorkItem returns errors.New("boom"). |
Returns wrapped error. |
Test_runRemove_success_JSON |
--json flag set. |
Exporter receives the raw *WorkItem from the post-PATCH GetWorkItem. |
Test_runRemove_tableOutput |
Default output, mock returns a work item with 1 relation. |
Table has 1 row with the 2 columns populated. |
Test_runRemove_emptyRelationsAfterRemove |
Source had 1 relation; after PATCH the work item has 0 relations. |
Table has 0 rows; no error. |
Test_runRemove_indexOrdering |
Source has 3 relations; targets match indices [0, 1]. |
PATCH ops are in order: /relations/1, /relations/0 (reverse). |
Test_runRemove_relationTypeMatchIsCaseInsensitive |
--relation-type PARENT. |
Resolves to System.LinkTypes.Hierarchy-Reverse exactly as --relation-type parent. |
References
Sub-issue of new
boards work-item relationumbrella. Hardened spec — do not re-derive decisions. Sibling of #271 (add), #272 (list-type), #274 (show).Command Description
Detach a relation (link) from one Azure Boards work item to one or more target work items. Mirrors
az boards work-item relation remove. The command builds a JSON-Patch document containingremoveoperations on/relations/{index}and sends it to the server.The REST surface is Work Items - Update (REST 7.1):
The Python reference implementation is at
azure-devops/azext_devops/dev/boards/relations.py:60-91(remove_relationfunction). The function:expand='All'.url.relationsarray, finding the entry whoserelmatches the friendly relation type and whoseurlmatches the target's URL; collects/relations/{index}removeops.removeops does not match the number of target IDs requested (Decision 15).expand='All'to populate the response.This is a destructive operation. The Python's
commands.py:97adds a confirmation prompt:'Are you sure you want to remove this relation(s)?'.Locked Decisions
Use: "remove [ORGANIZATION/]ID". The source work item ID is unique within the organization.az boards work-item relation remove. Symmetric with #271 (add).Aliases: []string{"r", "rm"}.cobra.ExactArgs(1).util.ParseTargetWithDefaultOrganization(ctx, args[0]).scope.Targets[0]is the ID string.--projectflag.az boards work-item relation remove.strconv.Atoi(scope.Targets[0])fails or returns<= 0, returnutil.FlagErrorf("work item ID must be a positive integer; got %q", scope.Targets[0])and do not call the SDK.--relation-typeis a single friendly name (e.g.,parent,child). Case-insensitive match against the list returned byGetRelationTypes.get_system_relation_name.--relation-type is not valid. Use "azdo boards work-item relation list-type" command to list possible relation types in your project.get_system_relation_name.--target-idis repeatable; multiple invocations concatenate. Each ID must parse as a positive integer.target_id.split(',')plus the Go-native repeatable-flag form.--target-idis required (no--target-urlsupport — Python only supports IDs inremove).remove_relation(id, relation_type, target_id, ...).GetWorkItem(id)to obtain itsurland use that URL when matching relations.client.get_work_item(target_work_item_id, expand='All').expand='All'to read its currentrelationsarray.relationsarray, finding each entry whoseRelmatches the relation type'sreferenceNameAND whoseUrlmatches the target's URL. Collect a/relations/{index}removeop for each match. Indices are computed before any modification; the indices are positional in the SDK'srelationsslice.if relation.rel == relation_type_system_name and relation.url == target_work_item_url.for relation in main_work_item.relations: ... index = index + 1— but with reverse to avoid index drift. Decision 14 supersedes the Python's forward order: applying forward would shift indices after each removal, causing later ops to target the wrong relations.removeops must equal number of target IDs requested. If fewer matches are found, returnutil.FlagErrorfwith the exact Python message:"Id(s) supplied in --target-id is not valid". SDK is not called forUpdateWorkItem.if len(patch_document) != len(target_work_item_ids): raise CLIError('Id(s) supplied in --target-id is not valid').Are you sure you want to remove this relation(s)?. Skip with--yes. On cancel, returnutil.ErrCancel.confirmation='Are you sure you want to remove this relation(s)?'.*WorkItem(the post-PATCHGetWorkItemwithexpand=All) toopts.exporter.Write.TYPE, URL(mirroring theaddleaf).transform_work_item_relationsand #271's Table Output Contract.--expandflag. All responses useexpand='All'internally.--target-urlflag. Python'sremove_relationdoes not support URLs.workitemtracking.Client.{UpdateWorkItem, GetWorkItem, GetRelationTypes}.go mod tidy/go mod vendor/scripts/generate_mocks.shwork.RemoveRelationPatchOpshelper (ininternal/cmd/boards/workitem/relation/shared/relation.go) returns the list of/relations/{index}ops and the matches count, but the leaf owns the policy of when to error (Decision 15).relations(no relations at all) returnsutil.FlagErrorf("Id(s) supplied in --target-id is not valid")per Decision 15 (zero matches != len(targets)).if main_work_item.relations:guard, which falls through to the count check.--target-idand--target-url—--target-urlis simply not supported (Decision 20).Command Signature
Flags
--id[ORGANIZATION/]ID.--relation-type--target-id(repeatable)--yes--organizationAlso add
util.AddJSONFlags(cmd, &opts.exporter, []string{"id", "rev", "fields", "url", "_links", "relations", "commentVersionRef"}).Code Skeleton (canonical, copy verbatim)
§1.
removeOptionsstruct§2.
runRemoveskeletonJSON Output Contract
Pass the raw
*workitemtracking.WorkItem(post-PATCH) toopts.exporter.Write. No view struct.JSON fields exposed:
id,rev,fields,url,_links,relations,commentVersionRef.Table Output Contract
Use
ctx.Printer("list")with 2 columns:TYPE, URL. Same as #271 (add).Command Wiring
internal/cmd/boards/workitem/relation/remove/remove.gowithpackage remove, factoryfunc NewCmd(ctx util.CmdContext) *cobra.Command.import "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/relation/remove"tointernal/cmd/boards/workitem/relation/relation.go.relation.gowithcmd.AddCommand(remove.NewCmd(ctx)).internal/cmd/boards/workitem/relation/shared/relation.go(introduced by the umbrella) —shared.ResolveRelationType.API Surface
Vendored SDK calls (from
vendor/.../v7/workitemtracking/client.go):Reference Existing Patterns
internal/cmd/pipelines/variablegroup/delete/delete.go:30-170— primary destructive reference (confirmation prompt pattern).internal/cmd/boards/workitem/show— sibling for org-scoped parsing.internal/cmd/boards/workitem/delete(feat: Implementazdo boards work-item deletecommand #269) — sibling destructive command (confirmation pattern, plain message after success).internal/cmd/boards/workitem/add(Introduceazdo boards work-item relationcommand group #271) — sibling for table output and friendly-name fill.Reference implementations (for UX parity only):
azure-devops/azext_devops/dev/boards/relations.py:60-91-remove_relationfunction. Confirms: confirmation prompt, target-ID-only support, count-validation message.azure-devops/azext_devops/dev/boards/commands.py:97-confirmation='Are you sure you want to remove this relation(s)?'.TDD Test Plan
TestNewCmd_removeUse == "remove [ORGANIZATION/]ID",Aliases == ["r", "rm"],Args == cobra.ExactArgs(1).Test_runRemove_minimal--relation-type parent --target-id 2. Source has 1 relation matching.removeop on/relations/0.Test_runRemove_multipleTargets--target-id 2 --target-id 3; source has matching relations at indices[0, 2].removeops in reverse order:/relations/2first, then/relations/0.Test_runRemove_commaSeparated--target-id 2,3.multipleTargets.Test_runRemove_noMatchutil.FlagErrorfwith"Id(s) supplied in --target-id is not valid". SDK'sUpdateWorkItemis not called.Test_runRemove_partialMatchutil.FlagErrorfwith"Id(s) supplied in --target-id is not valid". SDK'sUpdateWorkItemis not called.Test_runRemove_invalidRelationType--relation-type bogus.util.FlagErrorfwith"--relation-type is not valid...". SDK is not called.Test_runRemove_invalidSourceID"abc".util.FlagErrorfwith"work item ID must be a positive integer". SDK is not called.Test_runRemove_zeroSourceID"0".util.FlagErrorf. SDK is not called.Test_runRemove_negativeSourceID"-5".util.FlagErrorf. SDK is not called.Test_runRemove_invalidTargetID--target-id abc.util.FlagErrorfwith"target work item ID must be a positive integer". SDK is not called.Test_runRemove_noTargets--target-id.util.FlagErrorfwith"--target-id must be provided". SDK is not called.Test_runRemove_targetIDNotFoundGetWorkItemfor the target returns 404.Test_runRemove_confirmationPrompt_yes--yesset.Prompter.Confirmis not called; PATCH proceeds.Test_runRemove_confirmationPrompt_userConfirms--yes; mockPrompter.Confirmreturns(true, nil).Test_runRemove_confirmationPrompt_userDeclines--yes; mockPrompter.Confirmreturns(false, nil).util.ErrCancel. SDK is not called.Test_runRemove_orgScopeOnly"1234".args.Id == ptr(1234), scope.Organization is the default.Test_runRemove_explicitOrg"myorg/1234".args.Id == ptr(1234), scope.Organization is"myorg".Test_runRemove_APIErrorUpdateWorkItemreturnserrors.New("boom").Test_runRemove_success_JSON--jsonflag set.*WorkItemfrom the post-PATCHGetWorkItem.Test_runRemove_tableOutputTest_runRemove_emptyRelationsAfterRemoveTest_runRemove_indexOrdering[0, 1]./relations/1,/relations/0(reverse).Test_runRemove_relationTypeMatchIsCaseInsensitive--relation-type PARENT.System.LinkTypes.Hierarchy-Reverseexactly as--relation-type parent.References
vendor/.../v7/workitemtracking/client.go:113, 137, 205.vendor/.../v7/workitemtracking/models.go:1425 (WorkItemRelation), :1435 (WorkItemRelationType).internal/mocks/workitemtracking_client_mock.go:669, 849, 1358.internal/azdo/factory.go:149.7.1-preview.3).boards work-item relationsubgroup. Sibling: Introduceazdo boards work-item relationcommand group #271, Implementazdo boards work-item relation addcommand #272, Implementazdo boards work-item relation removecommand #274.internal/cmd/boards/workitem/delete(feat: Implementazdo boards work-item deletecommand #269),internal/cmd/boards/workitem/add(Introduceazdo boards work-item relationcommand group #271).