task new: einen zweiten Schreibweg in den Tracker (ein Posten, keine Seite, #132)
CI / verify (push) Successful in 55s
Release / release (push) Successful in 38s

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:
torben committed 2026-09-22 11:55:06 +02:00
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):