-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_error_hints.py
More file actions
151 lines (111 loc) · 6.1 KB
/
Copy pathtest_error_hints.py
File metadata and controls
151 lines (111 loc) · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Anti-vacuous guards for the honest error-hint layer (functional honesty).
An error must SAY WHAT IT KNOWS. A hint that does not point at the real fix
actively misdirects, so these tests pin three properties that the frozen
``weft.plainweave.error.v1`` envelope must keep honouring additively:
1. Every :class:`ErrorCode` has a non-empty default hint, and VALIDATION /
NOT_FOUND never inherit the stale-state "Refresh local Plainweave state and
retry." blanket (their cause is bad input / a missing id, not staleness).
2. ``_error`` resolves an omitted hint from that map, threads an explicit hint
verbatim, and always coerces ``details`` to a dict.
3. The two dogfood findings (CONFLICT version guard; missing-actor VALIDATION)
emit precise, cause-appropriate hints and details end-to-end.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from plainweave.errors import ErrorCode, PlainweaveError
from plainweave.service import _DEFAULT_ERROR_HINTS, PlainweaveService
from plainweave.store import migrate
_BLANKET_HINT = "Refresh local Plainweave state and retry."
def service_for(tmp_path: Path) -> PlainweaveService:
db_path = tmp_path / ".plainweave" / "plainweave.db"
migrate(db_path, project_key="AUTH")
return PlainweaveService(db_path)
def test_default_hint_map_covers_every_error_code_non_empty() -> None:
for code in ErrorCode:
assert code in _DEFAULT_ERROR_HINTS, f"missing default hint for {code}"
hint = _DEFAULT_ERROR_HINTS[code]
assert hint and hint.strip(), f"empty default hint for {code}"
def test_validation_and_not_found_defaults_are_never_stale_state_hints() -> None:
for code in (ErrorCode.VALIDATION, ErrorCode.NOT_FOUND):
hint = _DEFAULT_ERROR_HINTS[code].lower()
assert _BLANKET_HINT.lower() not in hint
# No refresh/refetch-local-state phrasing either: the cause is the input,
# not local staleness. (CONFLICT may legitimately say "refetch".)
assert "refresh" not in hint
assert "refetch" not in hint
def test_error_resolves_default_hint_when_none(tmp_path: Path) -> None:
service = service_for(tmp_path)
error = service._error(ErrorCode.NOT_FOUND, "requirement not found: REQ-X")
assert error.hint == _DEFAULT_ERROR_HINTS[ErrorCode.NOT_FOUND]
assert error.hint
assert error.details == {}
def test_error_threads_explicit_hint_and_details_verbatim(tmp_path: Path) -> None:
service = service_for(tmp_path)
error = service._error(
ErrorCode.CONFLICT,
"boom",
hint="Retry with --expected-version 7.",
details={"expected_version": 3, "current_version": 7},
)
assert error.hint == "Retry with --expected-version 7."
assert error.details == {"expected_version": 3, "current_version": 7}
assert isinstance(error.details, dict)
def test_conflict_version_guard_says_what_it_knows(tmp_path: Path) -> None:
# add -> current version 0; approving with expected_version 1 trips the guard.
service = service_for(tmp_path)
draft = service.create_requirement(
"Reject expired bearer tokens", "The API shall reject expired tokens.", "human:john"
)
with pytest.raises(PlainweaveError) as exc_info:
service.approve_requirement(draft.id, actor="human:john", expected_version=1, idempotency_key="approve-1")
error = exc_info.value
assert error.code == ErrorCode.CONFLICT
assert error.details["current_version"] == 0
assert error.details["expected_version"] == 1
assert "current version 0" in error.message
assert "--expected-version 0" in error.hint
assert _BLANKET_HINT not in error.hint
def test_draft_revision_guard_says_what_it_knows(tmp_path: Path) -> None:
# The same optimistic-concurrency defect as the requirement-version guard, on
# a sibling field: updating a draft with a wrong expected_draft_revision must
# disclose the actual revision (message + details + a --expected-draft-revision hint).
service = service_for(tmp_path)
draft = service.create_requirement(
"Reject expired bearer tokens", "The API shall reject expired tokens.", "human:john"
)
with pytest.raises(PlainweaveError) as exc_info:
service.update_draft(draft.id, actor="human:john", statement="revised", expected_draft_revision=99)
error = exc_info.value
assert error.code == ErrorCode.CONFLICT
current = error.details["current_draft_revision"]
assert error.details["expected_draft_revision"] == 99
assert f"current draft revision {current}" in error.message
assert f"--expected-draft-revision {current}" in error.hint
assert _BLANKET_HINT not in error.hint
def test_missing_actor_error_points_at_actor_not_stale_state(tmp_path: Path) -> None:
service = service_for(tmp_path)
with pytest.raises(PlainweaveError) as exc_info:
service.create_requirement("Reject expired bearer tokens", "The API shall reject expired tokens.", "")
error = exc_info.value
assert error.code == ErrorCode.VALIDATION
assert "--actor" in error.hint
assert _BLANKET_HINT not in error.hint
# The missing-actor golden pins details:{}; enrichment must not silently grow it.
assert error.details == {}
def _assert_validation_without_blanket_hint(exc_info: pytest.ExceptionInfo[PlainweaveError]) -> None:
assert exc_info.value.code == ErrorCode.VALIDATION
assert exc_info.value.hint != _BLANKET_HINT
assert _BLANKET_HINT not in exc_info.value.hint
def test_no_service_path_validation_error_carries_the_blanket_hint(tmp_path: Path) -> None:
"""Guard: a VALIDATION error must never inherit the stale-state blanket hint."""
service = service_for(tmp_path)
with pytest.raises(PlainweaveError) as missing_actor:
service.create_requirement("t", "s", "")
_assert_validation_without_blanket_hint(missing_actor)
with pytest.raises(PlainweaveError) as bad_method:
service._validate_verification_method("nonsense")
_assert_validation_without_blanket_hint(bad_method)
with pytest.raises(PlainweaveError) as bad_status:
service._validate_evidence_status("nonsense")
_assert_validation_without_blanket_hint(bad_status)