throughline 3.2.0__tar.gz → 3.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {throughline-3.2.0/src/throughline.egg-info → throughline-3.3.0}/PKG-INFO +3 -3
- {throughline-3.2.0 → throughline-3.3.0}/README.md +2 -2
- {throughline-3.2.0 → throughline-3.3.0}/pyproject.toml +1 -1
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/cli.py +103 -75
- {throughline-3.2.0 → throughline-3.3.0/src/throughline.egg-info}/PKG-INFO +3 -3
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline.egg-info/SOURCES.txt +1 -0
- throughline-3.3.0/tests/test_ratify_several.py +94 -0
- {throughline-3.2.0 → throughline-3.3.0}/LICENSE +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/NOTICE +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/setup.cfg +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/__init__.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/dump.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/filters.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/fingerprint.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/graph.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/grounding.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/identity.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/inject.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/model.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/ratification.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/schema.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/schema_ops.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/storage.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/tomledit.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/uid.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/validate.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline/version.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline.egg-info/dependency_links.txt +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline.egg-info/entry_points.txt +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline.egg-info/requires.txt +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/src/throughline.egg-info/top_level.txt +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_amend.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_doctor.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_engine.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_filters.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_ratification_change.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_ratification_withdrawal.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_ratifier_correction.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_ratify_status.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_schema_ops.py +0 -0
- {throughline-3.2.0 → throughline-3.3.0}/tests/test_yaml_loader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: throughline
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: A Git-native requirements management tool with a scope-avalanche grounding layer: permanent UIDs, one file per item, typed links, suspect detection, and a CI-gating check.
|
|
5
5
|
Author-email: Henry J Grech-Cini <henry.grechcini@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -36,10 +36,10 @@ version control; a `check` command validates the whole graph and gates CI.
|
|
|
36
36
|
|
|
37
37
|
**Dogfooded:** throughline's own spec is itself a throughline project —
|
|
38
38
|
<!-- tl:count type == 'system_requirement' -->
|
|
39
|
-
|
|
39
|
+
165
|
|
40
40
|
<!-- tl:end --> system requirements,
|
|
41
41
|
<!-- tl:count type == 'user_requirement' -->
|
|
42
|
-
|
|
42
|
+
29
|
|
43
43
|
<!-- tl:end --> user requirements, and
|
|
44
44
|
<!-- tl:count type == 'nfr' -->
|
|
45
45
|
21
|
|
@@ -8,10 +8,10 @@ version control; a `check` command validates the whole graph and gates CI.
|
|
|
8
8
|
|
|
9
9
|
**Dogfooded:** throughline's own spec is itself a throughline project —
|
|
10
10
|
<!-- tl:count type == 'system_requirement' -->
|
|
11
|
-
|
|
11
|
+
165
|
|
12
12
|
<!-- tl:end --> system requirements,
|
|
13
13
|
<!-- tl:count type == 'user_requirement' -->
|
|
14
|
-
|
|
14
|
+
29
|
|
15
15
|
<!-- tl:end --> user requirements, and
|
|
16
16
|
<!-- tl:count type == 'nfr' -->
|
|
17
17
|
21
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "throughline"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.3.0"
|
|
8
8
|
description = "A Git-native requirements management tool with a scope-avalanche grounding layer: permanent UIDs, one file per item, typed links, suspect detection, and a CI-gating check."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -1784,91 +1784,115 @@ def _confirm(question: str) -> bool:
|
|
|
1784
1784
|
|
|
1785
1785
|
|
|
1786
1786
|
def cmd_ratify(args) -> int:
|
|
1787
|
+
"""Ratify one or more items (SR-0199). Every item named is checked before any
|
|
1788
|
+
is rendered or signed, so a run that cannot complete writes nothing; the
|
|
1789
|
+
ratifier is asked once; each item is then shown and confirmed on its own,
|
|
1790
|
+
because the signature is per item (UR-0029)."""
|
|
1787
1791
|
try:
|
|
1788
1792
|
project = load_project(args.path)
|
|
1789
1793
|
except ProjectError as e:
|
|
1790
1794
|
return _err(str(e))
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
# Refuse before rendering anything or asking anyone (SR-0195)
|
|
1800
|
-
#
|
|
1801
|
-
#
|
|
1802
|
-
#
|
|
1803
|
-
# below read the same graph.
|
|
1795
|
+
uids = list(args.uids)
|
|
1796
|
+
if not uids:
|
|
1797
|
+
# A proposed item is the usual target, but ratify tolerates any item (the
|
|
1798
|
+
# grounding layer decides what it means), so the picker lists all live items.
|
|
1799
|
+
uid = _resolve_uid(project, None, "ratify", "UID")
|
|
1800
|
+
if uid is None:
|
|
1801
|
+
return USAGE
|
|
1802
|
+
uids = [uid]
|
|
1803
|
+
# Refuse before rendering anything or asking anyone (SR-0195), and for the
|
|
1804
|
+
# whole run before any item in it (SR-0199). The old order asked who was taking
|
|
1805
|
+
# accountability and only then discovered that nothing could be signed, which
|
|
1806
|
+
# taught that the prompt was a formality. The index is built once and handed to
|
|
1807
|
+
# ratify, so the question asked here and the writes below read the same graph.
|
|
1804
1808
|
idx = Index.build(project)
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1809
|
+
items = []
|
|
1810
|
+
for uid in uids:
|
|
1811
|
+
item = project.get(uid)
|
|
1812
|
+
if item is None:
|
|
1813
|
+
return _err(f"{uid} does not exist")
|
|
1814
|
+
obstacle = ratification_obstacle(project.schema, idx, item,
|
|
1815
|
+
replacing=args.replacing)
|
|
1816
|
+
if obstacle is not None:
|
|
1817
|
+
return _err(obstacle if len(uids) == 1
|
|
1818
|
+
else f"{obstacle} — nothing in this run was ratified")
|
|
1819
|
+
items.append(item)
|
|
1813
1820
|
# Resolved once, before anything is shown, so the difference put in front of
|
|
1814
1821
|
# the ratifier and the gate below are the same answer (SR-0165).
|
|
1815
|
-
|
|
1822
|
+
changes = {item.uid: change_since_ratification(project, item) for item in items}
|
|
1816
1823
|
interactive = _interactive()
|
|
1817
|
-
if interactive:
|
|
1818
|
-
|
|
1819
|
-
|
|
1824
|
+
if not interactive:
|
|
1825
|
+
for item in items:
|
|
1826
|
+
if changes[item.uid].stale and not args.accept_change:
|
|
1827
|
+
return _err(
|
|
1828
|
+
f"{item.uid} has changed since "
|
|
1829
|
+
f"{item.attrs.get(RATIFIED_BY_ATTR, 'a human')} ratified it and this "
|
|
1830
|
+
"session cannot show you what changed — pass --accept-change to "
|
|
1831
|
+
"record a signature over a change accepted unseen, or run this on "
|
|
1832
|
+
"a terminal to see it first")
|
|
1820
1833
|
# Offer the identity this repository already signs commits with (SR-0156). It
|
|
1821
1834
|
# is only ever a default: _resolve_value shows it and takes it on assent, and a
|
|
1822
1835
|
# non-interactive session that names no ratifier is refused, not signed for.
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1836
|
+
# Asked once for the run — the person does not change between items — and on a
|
|
1837
|
+
# terminal not until the first item has been shown, so the reader knows what
|
|
1838
|
+
# they are being asked about while they are being asked (UR-0029, SR-0195).
|
|
1839
|
+
by = None
|
|
1840
|
+
if not interactive:
|
|
1841
|
+
by = _resolve_value(args.by, "ratifier", "--by",
|
|
1842
|
+
default=default_ratifier(args.path))
|
|
1843
|
+
if by is None:
|
|
1844
|
+
return USAGE
|
|
1845
|
+
rc = OK
|
|
1846
|
+
for item in items:
|
|
1847
|
+
uid = item.uid
|
|
1848
|
+
change = changes[uid]
|
|
1849
|
+
# Ratifying is taking accountability, so the content comes before the
|
|
1850
|
+
# signature (UR-0029). Non-interactive runs render nothing: there is no
|
|
1851
|
+
# reader to serve, and output nobody reads is noise in CI.
|
|
1834
1852
|
if interactive:
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
"
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1853
|
+
_render_for_ratification(
|
|
1854
|
+
project, item, emit=lambda line: print(line, file=sys.stderr))
|
|
1855
|
+
# Re-ratifying is taking accountability afresh, so what moved since the
|
|
1856
|
+
# last signature goes in the path of this one (SR-0167). Placed after the
|
|
1857
|
+
# item and immediately before the stop, because that is where a reader
|
|
1858
|
+
# is deciding. Painted as git would paint it: colour on a terminal, none
|
|
1859
|
+
# when the reader has asked for none (NO_COLOR, https://no-color.org).
|
|
1860
|
+
if change.stale:
|
|
1861
|
+
for line in render_change(
|
|
1862
|
+
change, ratifier=item.attrs.get(RATIFIED_BY_ATTR),
|
|
1863
|
+
columns=shutil.get_terminal_size((80, 24)).columns,
|
|
1864
|
+
colour=not os.environ.get("NO_COLOR")):
|
|
1865
|
+
print(line, file=sys.stderr)
|
|
1866
|
+
print("", file=sys.stderr)
|
|
1867
|
+
# The stop that makes the rendering more than decoration, asked whether
|
|
1868
|
+
# or not --by was supplied (SR-0195) — a fully specified command is
|
|
1869
|
+
# exactly how a bulk or habitual ratification is run, and display
|
|
1870
|
+
# without a stop is a warning that scrolled past. SR-0120 permits it:
|
|
1871
|
+
# confirming an act is not prompting for a value. Declining writes
|
|
1872
|
+
# nothing for this item and is not an error; the user was asked and
|
|
1873
|
+
# answered, and the run goes on to the next item (SR-0199).
|
|
1874
|
+
if by is None:
|
|
1875
|
+
by = _resolve_value(args.by, "ratifier", "--by",
|
|
1876
|
+
default=default_ratifier(args.path))
|
|
1877
|
+
if by is None:
|
|
1878
|
+
return USAGE
|
|
1879
|
+
question = (f"replace the ratifier recorded on {uid} with {by}?"
|
|
1880
|
+
if args.replacing else f"ratify {uid} as {by}?")
|
|
1881
|
+
if not _confirm(question):
|
|
1882
|
+
print(f"{uid} not ratified", file=sys.stderr)
|
|
1883
|
+
continue
|
|
1884
|
+
try:
|
|
1885
|
+
item = ratify(project, uid, by=by, index=idx,
|
|
1886
|
+
by_id=getattr(args, "by_id", None),
|
|
1887
|
+
replacing=args.replacing)
|
|
1888
|
+
except IdentityError as e:
|
|
1889
|
+
return _err(str(e))
|
|
1890
|
+
except (ProjectError, GroundingError, SchemaError) as e:
|
|
1891
|
+
return _err(str(e))
|
|
1892
|
+
write_item(item, project.register_of(item.uid))
|
|
1893
|
+
identifier = item.attrs.get(RATIFIED_ID_ATTR)
|
|
1894
|
+
print(f"{uid} ratified by {by}" + (f" ({identifier})" if identifier else ""))
|
|
1895
|
+
return rc
|
|
1872
1896
|
|
|
1873
1897
|
|
|
1874
1898
|
def cmd_invalidate(args) -> int:
|
|
@@ -2329,8 +2353,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2329
2353
|
s.set_defaults(func=cmd_subgraph)
|
|
2330
2354
|
|
|
2331
2355
|
s = sub.add_parser("ratify", help="a human takes accountability for an item")
|
|
2332
|
-
s
|
|
2333
|
-
|
|
2356
|
+
# A list, as withdraw's is (SR-0197): several proposed items are the usual
|
|
2357
|
+
# shape of a machine-authored batch, and each is still shown and confirmed on
|
|
2358
|
+
# its own (SR-0199). A front end that read the single `uid` by name must now
|
|
2359
|
+
# read `uids`.
|
|
2360
|
+
s.add_argument("uids", nargs="*", metavar="UID",
|
|
2361
|
+
help="item(s) to ratify, in order (omit on a terminal to pick one)")
|
|
2334
2362
|
s.add_argument("--by", default=None,
|
|
2335
2363
|
help="ratifier name (omit on a terminal to be prompted; "
|
|
2336
2364
|
"defaults to the identity this repository signs with)")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: throughline
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: A Git-native requirements management tool with a scope-avalanche grounding layer: permanent UIDs, one file per item, typed links, suspect detection, and a CI-gating check.
|
|
5
5
|
Author-email: Henry J Grech-Cini <henry.grechcini@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -36,10 +36,10 @@ version control; a `check` command validates the whole graph and gates CI.
|
|
|
36
36
|
|
|
37
37
|
**Dogfooded:** throughline's own spec is itself a throughline project —
|
|
38
38
|
<!-- tl:count type == 'system_requirement' -->
|
|
39
|
-
|
|
39
|
+
165
|
|
40
40
|
<!-- tl:end --> system requirements,
|
|
41
41
|
<!-- tl:count type == 'user_requirement' -->
|
|
42
|
-
|
|
42
|
+
29
|
|
43
43
|
<!-- tl:end --> user requirements, and
|
|
44
44
|
<!-- tl:count type == 'nfr' -->
|
|
45
45
|
21
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Ratify takes several items in one run (UR-0032, SR-0199): every item is checked
|
|
2
|
+
before any is signed, the ratifier is asked once, and each item is signed on its own.
|
|
3
|
+
These run without a terminal, so the per-item rendering and stop are not exercised
|
|
4
|
+
here; the single-item tests that cover them are unchanged in meaning."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from throughline.cli import main as cli_main
|
|
12
|
+
from throughline.storage import load_project
|
|
13
|
+
|
|
14
|
+
CONFIG_EXTRA = '''
|
|
15
|
+
[types.intent]
|
|
16
|
+
attrs.origin = { type = "enum", values = ["human", "ai", "hybrid"] }
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cli(argv) -> int:
|
|
21
|
+
return cli_main([str(a) for a in argv])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def graph(tmp_path) -> Path:
|
|
26
|
+
"""An intent and three proposed requirements grounded on it."""
|
|
27
|
+
root = tmp_path / "proj"
|
|
28
|
+
assert _cli(["-C", root, "init", "--no-demo"]) == 0
|
|
29
|
+
cfg = root / "throughline.toml"
|
|
30
|
+
cfg.write_text(cfg.read_text(encoding="utf-8") + CONFIG_EXTRA, encoding="utf-8")
|
|
31
|
+
assert _cli(["-C", root, "new", "INT", "--type", "intent", "--title", "Why",
|
|
32
|
+
"--text", "V.", "--origin", "human", "--no-interactive"]) == 0
|
|
33
|
+
for n in ("R1", "R2", "R3"):
|
|
34
|
+
assert _cli(["-C", root, "new", "REQ", "--title", n, "--text",
|
|
35
|
+
f"The Tool shall do {n}.", "--ground", "INT-0001",
|
|
36
|
+
"--ground-type", "implements", "--origin", "ai",
|
|
37
|
+
"--no-interactive"]) == 0
|
|
38
|
+
return root
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ratifiers(root: Path) -> dict[str, str | None]:
|
|
42
|
+
p = load_project(root)
|
|
43
|
+
return {u: p.get(u).attrs.get("ratified_by") for u in ("REQ-0001", "REQ-0002", "REQ-0003")}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_several_items_are_ratified_in_one_run_in_the_order_given(graph, capsys):
|
|
47
|
+
assert _cli(["-C", graph, "ratify", "REQ-0002", "REQ-0001", "--by", "Ada Lovelace"]) == 0
|
|
48
|
+
out = capsys.readouterr().out.splitlines()
|
|
49
|
+
assert out == ["REQ-0002 ratified by Ada Lovelace", "REQ-0001 ratified by Ada Lovelace"]
|
|
50
|
+
assert _ratifiers(graph) == {"REQ-0001": "Ada Lovelace", "REQ-0002": "Ada Lovelace",
|
|
51
|
+
"REQ-0003": None}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_one_item_still_works_as_before(graph, capsys):
|
|
55
|
+
assert _cli(["-C", graph, "ratify", "REQ-0003", "--by", "Ada Lovelace"]) == 0
|
|
56
|
+
assert capsys.readouterr().out.strip() == "REQ-0003 ratified by Ada Lovelace"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_an_unknown_item_refuses_the_whole_run_before_anything_is_written(graph, capsys):
|
|
60
|
+
rc = _cli(["-C", graph, "ratify", "REQ-0001", "REQ-0009", "REQ-0002", "--by", "Ada"])
|
|
61
|
+
assert rc != 0
|
|
62
|
+
assert "REQ-0009 does not exist" in capsys.readouterr().err
|
|
63
|
+
assert _ratifiers(graph) == {"REQ-0001": None, "REQ-0002": None, "REQ-0003": None}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_an_item_that_cannot_be_ratified_refuses_the_whole_run(graph, capsys):
|
|
67
|
+
"""The obstacle is found for every item before any is signed (SR-0195 for the
|
|
68
|
+
run, not just the first item): here REQ-0002 is already signed by the same
|
|
69
|
+
content, which a plain re-ratification refuses."""
|
|
70
|
+
assert _cli(["-C", graph, "ratify", "REQ-0002", "--by", "Ada Lovelace"]) == 0
|
|
71
|
+
capsys.readouterr()
|
|
72
|
+
rc = _cli(["-C", graph, "ratify", "REQ-0001", "REQ-0002", "REQ-0003", "--by", "Ada Lovelace"])
|
|
73
|
+
assert rc != 0
|
|
74
|
+
err = capsys.readouterr().err
|
|
75
|
+
assert "REQ-0002" in err and "nothing in this run was ratified" in err
|
|
76
|
+
assert _ratifiers(graph) == {"REQ-0001": None, "REQ-0002": "Ada Lovelace", "REQ-0003": None}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_a_stale_item_in_the_batch_refuses_the_run_without_accept_change(graph, capsys):
|
|
80
|
+
assert _cli(["-C", graph, "ratify", "REQ-0002", "--by", "Ada Lovelace"]) == 0
|
|
81
|
+
assert _cli(["-C", graph, "amend", "REQ-0002", "--text", "The Tool shall do R2 twice."]) == 0
|
|
82
|
+
capsys.readouterr()
|
|
83
|
+
rc = _cli(["-C", graph, "ratify", "REQ-0001", "REQ-0002", "--by", "Ada Lovelace"])
|
|
84
|
+
assert rc != 0
|
|
85
|
+
assert "--accept-change" in capsys.readouterr().err
|
|
86
|
+
assert _ratifiers(graph)["REQ-0001"] is None
|
|
87
|
+
assert _cli(["-C", graph, "ratify", "REQ-0001", "REQ-0002", "--by", "Ada Lovelace",
|
|
88
|
+
"--accept-change"]) == 0
|
|
89
|
+
assert _ratifiers(graph)["REQ-0001"] == "Ada Lovelace"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_no_item_off_a_terminal_is_a_usage_error(graph, capsys):
|
|
93
|
+
assert _cli(["-C", graph, "ratify", "--by", "Ada"]) == 2
|
|
94
|
+
assert "no item given to ratify" in capsys.readouterr().err
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|