diff --git a/README.md b/README.md
index 134d782..12e900c 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,8 @@ Unknown fields are round-tripped untouched. Filenames are frozen at creation so
A ticket id is the command:
```bash
-docket CORE-14 # show it, dependency context and all
+docket CORE-14 # show it, dependency context and all, with the body rendered as Markdown
+docket CORE-14 show --plain # the same content as bare text, body left raw
docket CORE-14 done # todo, wip, or done. The file follows
docket CORE-14 set [-t TEXT] [-p N] [-r A,B|none] [-ra A,B] [-rr A,B]
docket CORE-14 meta [KEY [VALUE]] [-c]
@@ -101,6 +102,7 @@ docket CORE-14 requires # one id per line, nothing when empty
docket CORE-14 required-by # the reverse direction, same shape
docket CORE-14 key # CORE
docket CORE-14 ready # true or false. Every dependency done?
+docket CORE-14 body # the raw Markdown body, no frontmatter
docket CORE-14 meta # the whole map as JSON
docket CORE-14 meta KEY # one value, bare, or JSON when it has structure
```
diff --git a/docs/tickets/todo/FEAT-16_ticketShowUsesOptionalFormatting.md b/docs/tickets/done/FEAT-16_ticketShowUsesOptionalFormatting.md
similarity index 97%
rename from docs/tickets/todo/FEAT-16_ticketShowUsesOptionalFormatting.md
rename to docs/tickets/done/FEAT-16_ticketShowUsesOptionalFormatting.md
index 47b2a35..4956c4a 100644
--- a/docs/tickets/todo/FEAT-16_ticketShowUsesOptionalFormatting.md
+++ b/docs/tickets/done/FEAT-16_ticketShowUsesOptionalFormatting.md
@@ -1,7 +1,7 @@
---
id: FEAT-16
title: Ticket Show Uses Optional Formatting
-status: todo
+status: done
priority: 2
requires: []
metadata: {}
diff --git a/roadmap.md b/roadmap.md
index e920db5..18199e1 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -11,13 +11,12 @@ graph TD
FEAT_6["FEAT-6
Templates for Tickets
per Key
p3 todo"]
FEAT_8["FEAT-8
Host Flag
p4 todo"]
FEAT_12["FEAT-12
Per Key Ticket Board
View
p2 todo"]
- FEAT_16["FEAT-16
Ticket Show Uses
Optional Formatting
p2 todo"]
end
FEAT_6 --> FEAT_12
classDef todoP2 fill:#495057,color:#fff,stroke:#ffd43b,stroke-width:2px
classDef todoP3 fill:#495057,color:#fff,stroke:#adb5bd,stroke-width:2px
classDef todoP4 fill:#495057,color:#fff,stroke:#6c757d,stroke-width:1px
- class FEAT_12,FEAT_16 todoP2
+ class FEAT_12 todoP2
class FEAT_6 todoP3
class BUG_6,FEAT_8 todoP4
```
diff --git a/src/docket/__init__.py b/src/docket/__init__.py
index c02698b..3a37944 100644
--- a/src/docket/__init__.py
+++ b/src/docket/__init__.py
@@ -8,4 +8,4 @@
# No type check to comply with hatch's requirements.
# Do not re-add.
-__version__ = "1.7.0"
+__version__ = "1.8.0"
diff --git a/src/docket/cli/commands.py b/src/docket/cli/commands.py
index ed1586a..bfac14c 100644
--- a/src/docket/cli/commands.py
+++ b/src/docket/cli/commands.py
@@ -16,7 +16,7 @@
from rich.text import Text
from docket.cli.grammar import ACCESSORS, EXIT_INVALID, EXIT_OK, EXIT_USAGE, OUTPUT_ARGUMENT, parseEditIdList, parseIdList, resolveGraphScope, resolveListFilters
-from docket.cli.output import STATUS_STYLES, Output, buildContextTable, relativeToRoot
+from docket.cli.output import STATUS_STYLES, Output, buildContextTable, buildTicketBody, buildTicketPanel, plainTicket, relativeToRoot
from docket.core.config import Config, discoverConfig
from docket.core.deploy import DeployReport, deploy, upgrade
from docket.core.handoff import HANDOFF_FILENAME, renderHandoff
@@ -30,7 +30,7 @@
# MARK: Constants
-# How each accessor in the grammar reads its answer off a ticket. The store is passed alongside the ticket because a derived answer, such as the reverse dependencies or readiness, cannot be read from one ticket alone. A list comes back for a list of ids, which `commandField` prints one per line.
+# How each accessor in the grammar reads its answer off a ticket. The store is passed alongside the ticket because a derived answer, such as the reverse dependencies or readiness, cannot be read from one ticket alone. A list comes back for a list of ids, or for the lines of the body, which `commandField` prints one per line so an empty one prints nothing at all.
FIELD_READERS: dict[str, Callable[[Store, Ticket], object]] = {
"title": lambda store, ticket: ticket.title,
"status": lambda store, ticket: ticket.status,
@@ -39,6 +39,7 @@
"required-by": lambda store, ticket: [entry["id"] for entry in dependencyContext(store.loadAll(), ticket.id)["requiredBy"]],
"key": lambda store, ticket: ticket.key,
"ready": lambda store, ticket: ticketReadiness(store.loadAll(), ticket.id).isReady,
+ "body": lambda store, ticket: ticket.trimmedBody.splitlines(),
}
# MARK: Functions
@@ -111,7 +112,7 @@ def commandShow(args: argparse.Namespace, store: Store, output: Output) -> int:
"""
Show a ticket with its resolved dependency context.
- The raw file carries bare ids in one direction only, so this resolves the titles and statuses the file deliberately does not duplicate. Use `cat` for the raw file.
+ The raw file carries bare ids in one direction only, so this resolves the titles and statuses the file deliberately does not duplicate. The body is rendered as Markdown unless `--plain` asks for the same content as bare text. Use `cat` for the raw file.
Args:
args: The parsed arguments.
@@ -124,10 +125,16 @@ def commandShow(args: argparse.Namespace, store: Store, output: Output) -> int:
loaded: TicketSet = store.loadAll()
ticket: Ticket = loaded.get(args.id)
- context = dependencyContext(loaded, args.id)
+ context: dict[str, list[dict[str, object]]] = dependencyContext(loaded, args.id)
+ root: Path = store.config.repoRoot
- output.print(Text(f"{ticket.id} {ticket.title}", style="bold"))
- output.print(f"status [{STATUS_STYLES.get(ticket.status, 'white')}]{ticket.status}[/] priority {ticket.priority} key {ticket.key}")
+ # Plain carries everything the styled form does, only without the styling or the rendering.
+ if args.plain:
+ output.raw(plainTicket(ticket, context, root))
+
+ return EXIT_OK
+
+ output.print(buildTicketPanel(ticket, root))
# Show both directions, since the reverse one is the whole reason the file can afford to store only forward edges.
output.print("")
@@ -136,7 +143,7 @@ def commandShow(args: argparse.Namespace, store: Store, output: Output) -> int:
output.print(buildContextTable("Required by", context["requiredBy"]))
output.print("")
- output.print(ticket.body.strip("\n"))
+ output.print(buildTicketBody(ticket))
return EXIT_OK
diff --git a/src/docket/cli/grammar.py b/src/docket/cli/grammar.py
index e6f00c7..431f62c 100644
--- a/src/docket/cli/grammar.py
+++ b/src/docket/cli/grammar.py
@@ -52,6 +52,7 @@
"required-by": "Print the ids of the tickets depending on this one, one per line. Prints nothing when there are none.",
"key": "Print the key portion of the ticket's id and nothing else.",
"ready": "Print whether every dependency is done, as a bare true or false.",
+ "body": "Print the ticket's body as raw Markdown, with no frontmatter. Prints nothing when it is empty.",
}
# The command that reads each frontmatter field. `id` has none, since it is what was typed to reach the ticket, and `metadata` is read through `meta` because that command also writes it. A field missing from here fails the suite, which is what keeps a new field from arriving without a way to read it.
@@ -409,7 +410,11 @@ def buildTicketParser(commands: argparse._SubParsersAction, priorityOptions: str
# Not required, so a bare id parses and falls through to showing the ticket.
ticketCommands = ticketParser.add_subparsers(dest="ticketCommand", metavar="COMMAND")
- ticketCommands.add_parser("show", help="Show the ticket with its resolved dependency context. This is what a bare id does.", formatter_class=RichHelpFormatter)
+ # A bare id never reaches the show parser, so the ticket parser carries the default it would otherwise leave unset.
+ ticketParser.set_defaults(plain=False)
+
+ showParser: argparse.ArgumentParser = ticketCommands.add_parser("show", help="Show the ticket with its resolved dependency context. This is what a bare id does.", formatter_class=RichHelpFormatter)
+ showParser.add_argument("--plain", action="store_true", help="Write the same content as bare text, with no styling and the body left as raw Markdown. Use `cat` for the raw file.")
# Every read is registered from the one table, so a new one needs no parser code of its own.
for accessor, accessorHelp in ACCESSORS.items():
diff --git a/src/docket/cli/output.py b/src/docket/cli/output.py
index d69cab7..602a105 100644
--- a/src/docket/cli/output.py
+++ b/src/docket/cli/output.py
@@ -6,19 +6,27 @@
# MARK: Imports
+import json
import sys
from pathlib import Path
from typing import Any, Iterable, Optional
from rich.console import Console
+from rich.markdown import Markdown
+from rich.panel import Panel
from rich.table import Table
from rich.text import Text
+from docket.core.ticket import Ticket
+
# MARK: Constants
# Styles for the status column, matching the intent of the mermaid classes without depending on them.
STATUS_STYLES: dict[str, str] = {"todo": "dim", "wip": "yellow", "done": "green"}
+# The columns of a dependency table, shared by its styled and plain forms so the two cannot drift.
+CONTEXT_COLUMNS: tuple[str, str, str] = ("ID", "STATUS", "TITLE")
+
# MARK: Classes
@@ -97,7 +105,7 @@ def value(self, value: Any) -> None:
value: The value to write.
"""
- if value is None or isinstance(value, (dict, list, tuple, bool)):
+ if isStructured(value):
self.json(value)
return
@@ -127,40 +135,201 @@ def error(self, message: str) -> None:
# MARK: Functions
-def buildContextTable(heading: str, entries: list[dict[str, object]]) -> Table:
+def isStructured(value: Any) -> bool:
"""
- Build the table showing one direction of a ticket's resolved dependencies.
+ Report whether a value has structure that only JSON can spell for a reader.
+
+ A mapping, a list, a boolean, or a null reads as Python's spelling when turned into text, which is neither what was written nor anything a shell could parse.
Args:
- heading: What to title the table.
- entries: The resolved records.
+ value: The value to classify.
Returns:
- The table.
+ Whether the value should be written as JSON.
"""
- table: Table = Table(title=heading, title_justify="left", box=None, pad_edge=False, title_style="bold")
- table.add_column("ID")
- table.add_column("STATUS")
- table.add_column("TITLE")
+ return value is None or isinstance(value, (dict, list, tuple, bool))
- if not entries:
- table.add_row("[dim]none[/dim]", "", "")
- return table
+def describeValue(value: Any) -> str:
+ """
+ Describe one value on a single line, as compact JSON when it has structure.
+
+ Args:
+ value: The value to describe.
+
+ Returns:
+ The value's text.
+ """
+
+ if isStructured(value):
+ return json.dumps(value, default=str)
+
+ return str(value)
+
+
+def ticketFieldRows(ticket: Ticket, root: Path) -> list[tuple[Text, Text]]:
+ """
+ Build the labeled rows describing a ticket's frontmatter, for both the styled and plain forms of `show`.
+
+ A map contributes one row per entry, labeled only on its first, so a long map reads as one group. An empty map contributes nothing.
+
+ Args:
+ ticket: The ticket to describe.
+ root: The directory to describe the ticket's path against.
+
+ Returns:
+ The label and value of each row.
+ """
+
+ # Style lives on the text itself, so the plain form only has to drop it.
+ rows: list[tuple[Text, Text]] = [
+ (Text("Status"), Text(ticket.status, style=STATUS_STYLES.get(ticket.status, "white"))),
+ (Text("Priority"), Text(str(ticket.priority))),
+ (Text("Key"), Text(ticket.key)),
+ (Text("File"), Text(relativeToRoot(ticket.path, root))),
+ ]
+
+ # Both maps are free-form, so every entry is shown rather than a chosen few.
+ for label, entries in (("Metadata", ticket.metadata), ("Extra", ticket.extra)):
+ for index, (name, value) in enumerate(entries.items()):
+ rows.append((Text(label if index == 0 else ""), Text(f"{name}: {describeValue(value)}")))
+
+ return rows
+
+
+def buildTicketPanel(ticket: Ticket, root: Path) -> Panel:
+ """
+ Build the panel heading a shown ticket, titled with its id and title and holding its frontmatter.
+
+ Args:
+ ticket: The ticket to describe.
+ root: The directory to describe the ticket's path against.
+
+ Returns:
+ The panel.
+ """
+
+ grid: Table = Table.grid(padding=(0, 2))
+ grid.add_column(style="bold")
+ grid.add_column()
+
+ for label, value in ticketFieldRows(ticket, root):
+ grid.add_row(label, value)
+
+ return Panel(grid, title=Text(f"{ticket.id} {ticket.title}", style="bold"), title_align="left", expand=False)
+
+
+def buildTicketBody(ticket: Ticket) -> Markdown:
+ """
+ Build the rendered form of a ticket's body.
+
+ Args:
+ ticket: The ticket whose body to render.
+
+ Returns:
+ The body as rendered Markdown.
+ """
+
+ return Markdown(ticket.trimmedBody)
+
+def contextRows(entries: list[dict[str, object]]) -> list[tuple[Text, Text, Text]]:
+ """
+ Build the rows of one direction of a ticket's resolved dependencies, for both the styled and plain forms of `show`.
+
+ Args:
+ entries: The resolved records.
+
+ Returns:
+ The id, status, and title of each row.
+ """
+
+ if not entries:
+ return [(Text("none", style="dim"), Text(""), Text(""))]
+
+ rows: list[tuple[Text, Text, Text]] = []
for entry in entries:
# A dependency naming a missing id is shown rather than hidden, since a broken link the reader cannot see is worse than one they can.
if not entry["exists"]:
- table.add_row(str(entry["id"]), Text("missing", style="bold red"), "[dim]no such ticket[/dim]")
+ rows.append((Text(str(entry["id"])), Text("missing", style="bold red"), Text("no such ticket", style="dim")))
continue
status: str = str(entry["status"])
- table.add_row(str(entry["id"]), Text(status, style=STATUS_STYLES.get(status, "white")), str(entry["title"]))
+ rows.append((Text(str(entry["id"])), Text(status, style=STATUS_STYLES.get(status, "white")), Text(str(entry["title"]))))
+
+ return rows
+
+
+def buildContextTable(heading: str, entries: list[dict[str, object]]) -> Table:
+ """
+ Build the table showing one direction of a ticket's resolved dependencies.
+
+ Args:
+ heading: What to title the table.
+ entries: The resolved records.
+
+ Returns:
+ The table.
+ """
+
+ table: Table = Table(title=heading, title_justify="left", box=None, pad_edge=False, title_style="bold", header_style="bold dim")
+ for column in CONTEXT_COLUMNS:
+ table.add_column(column)
+
+ for row in contextRows(entries):
+ table.add_row(*row)
return table
+def plainTicket(ticket: Ticket, context: dict[str, list[dict[str, object]]], root: Path) -> str:
+ """
+ Describe a shown ticket as bare text, carrying the same content as the styled form with no styling or rendering.
+
+ The body goes out exactly as written, so the Markdown survives for whatever reads it next.
+
+ Args:
+ ticket: The ticket to describe.
+ context: The ticket's resolved dependency context.
+ root: The directory to describe the ticket's path against.
+
+ Returns:
+ The text, ending in a newline.
+ """
+
+ # Head it the way the panel does, with the rows aligned the way its grid aligns them.
+ fieldRows: list[tuple[str, ...]] = [(label.plain, value.plain) for label, value in ticketFieldRows(ticket, root)]
+ sections: list[str] = [f"{ticket.id} {ticket.title}\n{_alignRows(fieldRows)}"]
+
+ # Both directions follow, each under its own heading with the same columns as the styled table.
+ for heading, entries in (("Requires", context["requires"]), ("Required by", context["requiredBy"])):
+ rows: list[tuple[str, ...]] = [CONTEXT_COLUMNS, *(tuple(cell.plain for cell in row) for row in contextRows(entries))]
+ sections.append(f"{heading}\n{_alignRows(rows)}")
+
+ # The body goes last and untouched, and an empty one adds nothing rather than a stray blank section.
+ if ticket.trimmedBody:
+ sections.append(ticket.trimmedBody)
+
+ return "\n\n".join(sections) + "\n"
+
+
+def _alignRows(rows: list[tuple[str, ...]]) -> str:
+ """
+ Align rows of cells into columns separated by two spaces, the way the styled grids space them.
+
+ Args:
+ rows: The rows to align.
+
+ Returns:
+ One line per row, joined by newlines, with no trailing whitespace.
+ """
+
+ widths: list[int] = [max(len(row[column]) for row in rows) for column in range(len(rows[0]))]
+
+ return "\n".join(" ".join(cell.ljust(width) for cell, width in zip(row, widths)).rstrip() for row in rows)
+
+
def relativeToRoot(path: Optional[Path], root: Path) -> str:
"""
Describe a path relative to a repository root, so output stays readable in a narrow terminal.
diff --git a/src/docket/core/ticket.py b/src/docket/core/ticket.py
index e990320..0c54fdb 100644
--- a/src/docket/core/ticket.py
+++ b/src/docket/core/ticket.py
@@ -96,6 +96,14 @@ def isDone(self) -> bool:
return self.status == STATUS_DONE
+ @property
+ def trimmedBody(self) -> str:
+ """
+ The body without the blank lines framing it, which is how every reader presents it.
+ """
+
+ return self.body.strip("\n")
+
# MARK: Functions
def toFrontmatter(self) -> dict[str, Any]:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 8405257..c67555c 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -188,6 +188,130 @@ def testShowReportsAMissingDependency(inRepo: Path, capsys: pytest.CaptureFixtur
assert "missing" in capsys.readouterr().out
+def testShowRendersTheBodyAsMarkdown(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ The body is rendered rather than printed, so its Markdown syntax does not reach the reader.
+ """
+
+ main(["new", "CORE", "App Shell", "--body", "Goal: a **window** that `opens`."])
+ capsys.readouterr()
+
+ main(["CORE-1", "show"])
+
+ out: str = capsys.readouterr().out
+
+ assert "Goal: a window that opens." in out
+ assert "**" not in out
+ assert "`" not in out
+
+
+def testShowListsTheFileAndFrontmatter(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ The header carries every field the file does, along with where the file lives.
+ """
+
+ main(["new", "CORE", "App Shell", "--priority", "1"])
+ capsys.readouterr()
+
+ main(["CORE-1", "show"])
+
+ out: str = capsys.readouterr().out
+
+ assert "CORE-1 App Shell" in out
+ assert "Priority" in out
+ assert "CORE-1_appShell.md" in out
+
+
+def testShowListsMetadataAndExtraFields(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ Both free-form maps are shown entry by entry, with structured values written as compact JSON.
+ """
+
+ main(["new", "CORE", "App Shell"])
+ main(["CORE-1", "meta", "video", "2026-01-devlog"])
+
+ # Add a field the schema does not recognize, the way a consumer repo would extend it.
+ path: Path = inRepo / "docs" / "tickets" / "todo" / "CORE-1_appShell.md"
+ path.write_text(path.read_text(encoding="utf-8").replace("---\n", "---\nowners: [ana, ben]\n", 1), encoding="utf-8")
+ capsys.readouterr()
+
+ main(["CORE-1", "show"])
+
+ out: str = capsys.readouterr().out
+
+ assert "Metadata" in out
+ assert "video: 2026-01-devlog" in out
+ assert "Extra" in out
+ assert 'owners: ["ana", "ben"]' in out
+
+
+def testShowHidesEmptyMaps(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ An empty map adds nothing, rather than a label with no value beside it.
+ """
+
+ main(["new", "CORE", "App Shell"])
+ capsys.readouterr()
+
+ main(["CORE-1", "show"])
+
+ out: str = capsys.readouterr().out
+
+ assert "Metadata" not in out
+ assert "Extra" not in out
+
+
+def testShowPlainLeavesTheBodyRaw(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ `--plain` keeps the resolved context but writes the body exactly as it was written, Markdown and all.
+ """
+
+ main(["new", "CORE", "App Shell"])
+ main(["new", "CORE", "Skirmish Setup", "--requires", "CORE-1", "--body", "Goal: a **window** that `opens`."])
+ capsys.readouterr()
+
+ assert main(["CORE-2", "show", "--plain"]) == EXIT_OK
+
+ out: str = capsys.readouterr().out
+
+ assert out.startswith("CORE-2 Skirmish Setup\n")
+ assert "Requires\nID STATUS TITLE\nCORE-1 todo App Shell\n" in out
+ assert "Required by\nID STATUS TITLE\nnone\n" in out
+ assert out.endswith("Goal: a **window** that `opens`.\n")
+
+
+def testShowPlainListsTheSameFields(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ Plain drops the styling, never the content, so every header row appears aligned as it does in the panel.
+ """
+
+ main(["new", "CORE", "App Shell"])
+ main(["CORE-1", "meta", "video", "2026-01-devlog"])
+ main(["CORE-1", "meta", "clip", "intro"])
+ capsys.readouterr()
+
+ main(["CORE-1", "show", "--plain"])
+
+ out: str = capsys.readouterr().out
+
+ assert "Status todo\n" in out
+ assert "Metadata video: 2026-01-devlog\n clip: intro\n" in out
+
+
+def testShowPlainWritesNoEscapeSequences(inRepo: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
+ """
+ Plain bypasses `rich` entirely, so a forced color terminal still receives bare text.
+ """
+
+ monkeypatch.setenv("FORCE_COLOR", "1")
+ main(["new", "CORE", "App Shell"])
+ capsys.readouterr()
+
+ main(["CORE-1", "show", "--plain"])
+
+ assert "\x1b[" not in capsys.readouterr().out
+
+
def testShowOnAnUnknownIdFails(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""
Showing a ticket that does not exist is an error.
@@ -832,6 +956,41 @@ def testAnEmptyIdListPrintsNothing(inRepo: Path, capsys: pytest.CaptureFixture[s
assert capsys.readouterr().out == ""
+def testBodyPrintsTheRawMarkdown(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ The body goes out exactly as written, with none of the frontmatter above it and none of the rendering `show` applies.
+ """
+
+ main(["new", "CORE", "App Shell", "--body", "Goal: a **window**.\n\nThen a `menu`."])
+ capsys.readouterr()
+
+ assert main(["CORE-1", "body"]) == EXIT_OK
+
+ out: str = capsys.readouterr().out
+
+ assert out.startswith("# App Shell\n")
+ assert out.endswith("Goal: a **window**.\n\nThen a `menu`.\n")
+ assert "title:" not in out
+ assert "\x1b" not in out
+
+
+def testAnEmptyBodyPrintsNothing(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """
+ No body is no lines, the same way no ids is, rather than one blank line.
+ """
+
+ main(["new", "CORE", "App Shell"])
+
+ # Cut everything after the closing delimiter, leaving a ticket that is only frontmatter.
+ path: Path = inRepo / "docs" / "tickets" / "todo" / "CORE-1_appShell.md"
+ text: str = path.read_text(encoding="utf-8")
+ path.write_text(text[: text.index("---\n", 4) + 4], encoding="utf-8")
+ capsys.readouterr()
+
+ assert main(["CORE-1", "body"]) == EXIT_OK
+ assert capsys.readouterr().out == ""
+
+
@pytest.mark.parametrize("accessor", sorted(ACCESSORS))
def testAnAccessorOnAnUnknownTicketFails(inRepo: Path, capsys: pytest.CaptureFixture[str], accessor: str) -> None:
"""
@@ -1570,11 +1729,11 @@ def testShorthandFlagsDriveNewAndSet(inRepo: Path, capsys: pytest.CaptureFixture
capsys.readouterr()
- main(["GEN-1"])
+ main(["GEN-1", "show", "--plain"])
out: str = capsys.readouterr().out
assert "Renamed" in out
- assert "priority 3" in out
+ assert "Priority 3" in out
def testShorthandFlagsDriveListAndGraph(inRepo: Path, capsys: pytest.CaptureFixture[str]) -> None: