task new: einen zweiten Schreibweg in den Tracker (ein Posten, keine Seite, #132)
Files changed: - CHANGES.md - VERSION - docs/knowledge-and-commitment.md - instructions/wiki-ingest/SKILL.md - tools/CONTRACT.md - tools/chemenu/cli.py - tools/chemenu/commands/task_cmd.py - tools/chemenu/tasks/protocol.py - tools/chemenu/tasks/superproductivity.py - tools/chemenu/tests/test_instructions_cmd.py - tools/chemenu/tests/test_superproductivity.py - tools/chemenu/tests/test_task_cmd.py
This commit is contained in:
1 parent
e07d1ca42a
commit
cfbe3ea83e
12 files changed
+845
-45
No files matched your search
@@ -563,6 +563,157 @@ def test_api_reader_source_is_live_and_needs_no_network():
|
||||
assert source.kind == sp.ACCESS_API
|
||||
|
||||
|
||||
# --- write path: create_item (Gitea #132) --------------------------------------
|
||||
|
||||
def _make_write_handler(state: dict, *, token: str = "test-token"):
|
||||
"""A stub API supporting `GET /projects`, `GET /tags` and `POST /tasks`
|
||||
only - the three routes `create_item` ever touches. `state["posted"]`
|
||||
collects every request body `POST /tasks` received, so a test can assert
|
||||
on the exact fields sent (or that nothing was sent at all)."""
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self): # noqa: N802
|
||||
if self.path == "/health":
|
||||
self._reply(200, {"ok": True})
|
||||
return
|
||||
if self.headers.get("Authorization") != f"Bearer {token}":
|
||||
self._reply(401, {"error": "unauthorized"})
|
||||
return
|
||||
if self.path == "/projects":
|
||||
self._reply(200, state.get("projects", []))
|
||||
return
|
||||
if self.path == "/tags":
|
||||
self._reply(200, state.get("tags", []))
|
||||
return
|
||||
if self.path == "/tasks":
|
||||
# `find_project`'s preflight goes through the full
|
||||
# `SuperProductivityApiReader`, which always reads all three
|
||||
# routes (module docstring) - `create_item` itself never
|
||||
# reads this one.
|
||||
self._reply(200, state.get("tasks", []))
|
||||
return
|
||||
self._reply(404, {"error": "not found"})
|
||||
|
||||
def do_POST(self): # noqa: N802
|
||||
if self.headers.get("Authorization") != f"Bearer {token}":
|
||||
self._reply(401, {"error": "unauthorized"})
|
||||
return
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
payload = json.loads(self.rfile.read(length)) if length else {}
|
||||
if self.path == "/tasks":
|
||||
state.setdefault("posted", []).append(payload)
|
||||
self._reply(201, {"id": "new-task", **payload})
|
||||
return
|
||||
self._reply(404, {"error": "not found"})
|
||||
|
||||
def _reply(self, code: int, payload) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, *args): # silence stderr noise during the test run
|
||||
pass
|
||||
|
||||
return Handler
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _write_api_server(state: dict, *, token: str = "test-token"):
|
||||
handler_cls = _make_write_handler(state, token=token)
|
||||
server = http.server.HTTPServer(("127.0.0.1", 0), handler_cls)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=2)
|
||||
|
||||
|
||||
def _writer_for(server: http.server.HTTPServer, *, token: str = "test-token") -> sp.SuperProductivityWriter:
|
||||
cfg_ = _api_cfg(server, token=token)
|
||||
reader = sp.SuperProductivityApiReader(cfg_)
|
||||
return sp.SuperProductivityWriter(cfg_, reader)
|
||||
|
||||
|
||||
def test_create_item_posts_project_id_resolved_from_the_read_path():
|
||||
state = {"projects": [
|
||||
{"id": "p1", "title": "Ship Chemenu 7.0", "created": _ms(2026, 1, 1),
|
||||
"taskIds": [], "backlogTaskIds": []},
|
||||
]}
|
||||
with _write_api_server(state) as server:
|
||||
writer = _writer_for(server)
|
||||
writer.create_item("Rueckruf beim Kunden", project_name="ship CHEMENU 7.0")
|
||||
|
||||
assert len(state["posted"]) == 1
|
||||
assert state["posted"][0] == {"title": "Rueckruf beim Kunden", "projectId": "p1"}
|
||||
|
||||
|
||||
def test_create_item_refuses_an_unknown_project_and_posts_nothing():
|
||||
state = {"projects": []}
|
||||
with _write_api_server(state) as server:
|
||||
writer = _writer_for(server)
|
||||
with pytest.raises(ValidationError, match="No project named"):
|
||||
writer.create_item("x", project_name="No Such Project")
|
||||
assert "posted" not in state
|
||||
|
||||
|
||||
def test_create_item_inbox_route_uses_the_fixed_inbox_project_id():
|
||||
state = {"projects": []}
|
||||
with _write_api_server(state) as server:
|
||||
writer = _writer_for(server)
|
||||
writer.create_item("Beleg ablegen", project_name=None)
|
||||
|
||||
assert state["posted"][0]["projectId"] == sp.INBOX_PROJECT_ID
|
||||
|
||||
|
||||
def test_create_item_sets_the_waiting_tag_and_due_day():
|
||||
state = {
|
||||
"projects": [{"id": "p1", "title": "Ship Chemenu 7.0", "created": _ms(2026, 1, 1),
|
||||
"taskIds": [], "backlogTaskIds": []}],
|
||||
"tags": [{"id": "tag-wait", "title": "Waiting"}],
|
||||
}
|
||||
with _write_api_server(state) as server:
|
||||
writer = _writer_for(server)
|
||||
writer.create_item(
|
||||
"Nachfassen beim Elektriker", project_name="Ship Chemenu 7.0",
|
||||
waiting=True, follow_up_at=date(2026, 4, 1),
|
||||
)
|
||||
|
||||
posted = state["posted"][0]
|
||||
assert posted["tagIds"] == ["tag-wait"]
|
||||
assert posted["dueDay"] == "2026-04-01"
|
||||
|
||||
|
||||
def test_create_item_carries_the_freetext_backref_in_notes():
|
||||
state = {"projects": [{"id": "p1", "title": "Ship Chemenu 7.0", "created": _ms(2026, 1, 1),
|
||||
"taskIds": [], "backlogTaskIds": []}]}
|
||||
with _write_api_server(state) as server:
|
||||
writer = _writer_for(server)
|
||||
writer.create_item(
|
||||
"Rueckruf", project_name="Ship Chemenu 7.0", notes="Source - Kundenmail 2026-09-20",
|
||||
)
|
||||
|
||||
assert state["posted"][0]["notes"] == "Source - Kundenmail 2026-09-20"
|
||||
|
||||
|
||||
def test_create_item_waiting_without_the_tag_refuses_and_posts_nothing():
|
||||
"""Gitea #132's own acceptance criterion: a missing `waiting` tag must
|
||||
fail loud, never create an item without the status it was asked for."""
|
||||
state = {
|
||||
"projects": [{"id": "p1", "title": "Ship Chemenu 7.0", "created": _ms(2026, 1, 1),
|
||||
"taskIds": [], "backlogTaskIds": []}],
|
||||
"tags": [],
|
||||
}
|
||||
with _write_api_server(state) as server:
|
||||
writer = _writer_for(server)
|
||||
with pytest.raises(ValidationError, match="waiting"):
|
||||
writer.create_item("x", project_name="Ship Chemenu 7.0", waiting=True)
|
||||
assert "posted" not in state
|
||||
|
||||
|
||||
# --- equivalence: both access paths agree on the same fixture (Gitea #133) --------
|
||||
|
||||
def test_snapshot_and_api_readers_agree_on_the_same_fixture(tmp_path):
|
||||
|
||||
Reference in new issue
Block a user