change: Implement 7: Document the tool-calling contract, statuses, exit codes and native-process usage
Repository Validate / repository-checks (pull_request) Successful in 19s

Rewrote README.md as the delivered tool-calling contract for the calling AI agent and added tests/test_readme.py to keep it synchronized with the implementation.

README.md now documents:
- Native-process usage only: `python -m tools.example` with no Docker, container image or packaging stage, Python >= 3.11, no runtime third-party dependencies.
- Every parameter exactly as produced by `--help`: positional `REQUEST.json`, `-t/--topic`, `--width`, `--height`, `-f/--format` (`png|ico`), `-s/--source` (default `wikimedia_commons`), `-c/--count` (1-50, default 10), `--download`, `-o/--output-dir`, `--resize`, `--dry-run`; width/height documented as minimums and the "document XOR flags" rule.
- The JSON request document form used by `examples/input.json`: field table, types, defaults, optional `version`, and the document-level invalid_request rules.
- The single-document JSON response schema: fixed keys `status`, `source`, `request`, `items`, `error`; item fields (actual format/width/height, `url`/`file_url`, `size_bytes`), download record, resize fragment and the verified-save failure record.
- The fixed status vocabulary with its exit codes: `ok`=0, `dry_run`=0, `invalid_request`=2, `no_format`=3, `no_size`=4, `access_error`=5, `partial_download`=6, plus why no_match/outage are distinguishable.
- Approved free sources (`wikimedia_commons`, `openverse`) with no API keys, tokens or registration, the approved HTTPS host allowlist, and the registry extension point (`SourceAdapter`, `SourceRegistry.register/unregister`, `build_registry()`/`default_registry()`, host allowlist extension).
- Both decided behaviours: `--download` without `--output-dir` is an invalid request (invalid_request / exit 2), and partial saves are reported as `partial_download` (exit 6) with `download_incomplete` details while saved paths are still reported.
- The real test entrypoint `pytest` plus the compileall/help/smoke commands.

tests/test_readme.py asserts the README documents every long option the real parser accepts, the accepted formats, every status with its exit code from STATUS_EXIT_CODES, the response keys, every registered source id, the registry extension names, both product decisions, native-process wording, and the real pytest/compileall/help/smoke commands.

Tests:
Ran the facade check set with .venv/bin on PATH after `prepare_environment(component=tools, mode=restore)`:
- `pytest -q` -> 163 passed, 1 skipped (pre-existing tests/test_live_source.py live-network skip: Wikimedia upload host returns HTTP 429 from this sandbox).
- `python -m compileall -q tools tests` -> ok.
- `python -m tools.example --help` -> exit 0.
- `python -m tools.example examples/input.json` -> exit 0, one JSON dry_run envelope.
- `pytest tests/test_readme.py -q` -> 5 passed.
Also confirmed `git merge-base --is-ancestor origin/task/1-task HEAD` (target branch already integrated, zero diff), so no integration step was required.

Progress:
Delivered the documentation slice for issue 9. README.md was a two-line placeholder; it is now the full tool-calling contract: CLI parameters matching `--help` output, the JSON request-document form for examples/input.json, the single-document response schema, the fixed status vocabulary with exit codes (ok/dry_run=0, invalid_request=2, no_format=3, no_size=4, access_error=5, partial_download=6), credential-free sources and the registry extension point, the decided --download-without---output-dir invalid-request behaviour, partial_download reporting for incomplete saves, and plain native-process execution with the real pytest entrypoint. Added tests/test_readme.py (5 tests) so CI fails if documentation drifts from build_parser(), STATUS_EXIT_CODES, ACCEPTED_FORMATS or the default registry. No protected facade paths or source behaviour changed; existing obligations preserved (full suite green apart from the pre-existing live-source skip).

Published by developer-agent through Tool Backend.

Closes #9
This commit was merged in pull request #17.
This commit is contained in:
Tool Backend
2026-11-03 02:40:29 +00:00
parent d506c7a652
commit 2e8e70e765
3 changed files with 414 additions and 2 deletions
+92
View File
@@ -0,0 +1,92 @@
"""Keep README.md synchronized with the delivered CLI contract.
The documentation is part of the product: the calling agent relies on the
parameter list, the status vocabulary and the exit codes stated in README.md.
These checks fail when the implementation moves on and the documentation is
left describing something the tool no longer does.
"""
from __future__ import annotations
from pathlib import Path
from tools.envelope import STATUS_EXIT_CODES
from tools.errors import ACCEPTED_FORMATS
from tools.example import build_parser
from tools.sources import default_registry
README = Path(__file__).resolve().parent.parent / "README.md"
def readme_text() -> str:
assert README.is_file(), "README.md is missing"
text = README.read_text(encoding="utf-8")
assert text.strip(), "README.md is empty"
return text
def long_options() -> list[str]:
"""Return every ``--long`` option the real parser accepts."""
options: list[str] = []
for action in build_parser()._actions:
for option in action.option_strings:
if option.startswith("--"):
options.append(option)
return sorted(set(options))
def test_readme_documents_every_cli_parameter() -> None:
text = readme_text()
options = long_options()
assert {"--topic", "--width", "--height", "--format", "--source", "--count",
"--download", "--output-dir", "--resize", "--dry-run"} <= set(options)
missing = [option for option in options if option == "--help" and option not in text]
missing += [
option
for option in options
if option != "--help" and f"`{option}" not in text
]
assert not missing, f"README.md does not document parameters: {missing}"
# The positional request document and both invocation styles are documented.
assert "REQUEST.json" in text
assert "python -m tools.example examples/input.json" in text
# Format vocabulary in the README matches the accepted formats.
for image_format in ACCEPTED_FORMATS:
assert image_format in text
def test_readme_status_table_matches_exit_codes() -> None:
text = readme_text()
for status, exit_code in STATUS_EXIT_CODES.items():
assert f"| `{status}` | {exit_code} |" in text, (
f"README.md status table disagrees for {status!r} -> {exit_code}"
)
def test_readme_documents_response_schema_and_sources() -> None:
text = readme_text()
for key in ('"status"', '"source"', '"request"', '"items"', '"error"'):
assert key in text, f"README.md does not document response key {key}"
for source_id in default_registry().available_sources():
assert source_id in text, f"README.md does not document source {source_id!r}"
assert "SourceAdapter" in text and "SourceRegistry" in text
assert "API key" in text
def test_readme_documents_product_decisions() -> None:
text = readme_text()
# --download without --output-dir is an invalid request.
assert "--download" in text and "without `--output-dir` is an invalid request" in text
# Partial saves have their own status.
assert "partial_download" in text
assert "invalid_request" in text
# Native process only: no Docker or container packaging.
assert "Docker" in text and "container" in text
def test_readme_documents_the_real_test_entrypoint() -> None:
text = readme_text()
assert "\npytest\n" in text, "README.md must document the real pytest entrypoint"
assert "python -m compileall tools tests" in text
assert "python -m tools.example --help" in text