mootup 0.2.2__tar.gz → 0.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.
- {mootup-0.2.2 → mootup-0.3.0}/PKG-INFO +1 -1
- mootup-0.3.0/docs/specs/oas-mock-refresh.md +658 -0
- {mootup-0.2.2 → mootup-0.3.0}/pyproject.toml +1 -1
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/__init__.py +1 -1
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/config.py +94 -8
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/launch.py +34 -6
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/scaffold.py +65 -2
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/team_profile.py +16 -0
- mootup-0.3.0/src/moot/templates/claude/hooks/auto-orient.sh +21 -0
- mootup-0.3.0/src/moot/templates/claude/hooks/git-guard.sh +56 -0
- mootup-0.3.0/src/moot/templates/claude/hooks/grep-baseline-diff.sh +41 -0
- mootup-0.3.0/src/moot/templates/claude/hooks/handoff-status-check.sh +40 -0
- mootup-0.3.0/src/moot/templates/claude/settings.json +51 -0
- mootup-0.3.0/src/moot/templates/skills/memory-audit/SKILL.md +85 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/skills/product-workflow/SKILL.md +2 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-3/team.toml +13 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4/team.toml +15 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-observer/team.toml +17 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-parallel/team.toml +17 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-split-leader/team.toml +17 -0
- mootup-0.3.0/src/moot/templates/teams/loop-6/CLAUDE.md +43 -0
- mootup-0.3.0/src/moot/templates/teams/loop-6/README.md +9 -0
- mootup-0.3.0/src/moot/templates/teams/loop-6/team.toml +161 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_auth.py +36 -2
- mootup-0.3.0/tests/test_config.py +406 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_launch.py +107 -5
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_provision.py +23 -1
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_scaffold.py +77 -32
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_templates.py +177 -21
- mootup-0.2.2/tests/test_config.py +0 -144
- mootup-0.2.2/tests/test_example.py +0 -88
- {mootup-0.2.2 → mootup-0.3.0}/.github/workflows/publish.yml +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/.gitignore +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/LICENSE +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/README.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/__main__.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/__init__.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/channel_adapter.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/channel_runner.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/mcp_adapter.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/mcp_runner.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/notification_core.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/notify_runner.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/adapters/tmux_delivery.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/auth.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/cli.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/devcontainer.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/id_encoding.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/lifecycle.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/models.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/provision.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/response_format.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/CLAUDE.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/devcontainer/devcontainer.json +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/devcontainer/post-create.sh +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/devcontainer/run-moot-channel.sh +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/devcontainer/run-moot-mcp.sh +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/devcontainer/run-moot-notify.sh +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/skills/doc-curation/SKILL.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/skills/handoff/SKILL.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/skills/leader-workflow/SKILL.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/skills/librarian-workflow/SKILL.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/skills/spec-checklist/SKILL.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/skills/verify/SKILL.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-3/CLAUDE.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-3/README.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4/CLAUDE.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4/README.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-observer/CLAUDE.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-observer/README.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-parallel/CLAUDE.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-parallel/README.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-split-leader/CLAUDE.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/src/moot/templates/teams/loop-4-split-leader/README.md +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/__init__.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_adapters/__init__.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_cli.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_devcontainer.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_lifecycle.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_models.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_package.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_response_format.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/tests/test_security.py +0 -0
- {mootup-0.2.2 → mootup-0.3.0}/uv.lock +0 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# oas-mock-refresh
|
|
2
|
+
|
|
3
|
+
**Status:** Design spec
|
|
4
|
+
**Author:** Spec
|
|
5
|
+
**Run:** X (2026-04-18)
|
|
6
|
+
**Repo:** `mootup-io/moot`
|
|
7
|
+
**Feat branch:** `feat/oas-mock-refresh` from `main` @ `1546cf9`
|
|
8
|
+
**OAS source:** `/workspaces/convo/docs/api/openapi.yaml` (convo Run W, commit `b6f6d13` — OpenAPI 3.1.0)
|
|
9
|
+
|
|
10
|
+
## 1. Summary
|
|
11
|
+
|
|
12
|
+
Two intertwined hygiene passes in one run:
|
|
13
|
+
|
|
14
|
+
1. **Refresh stale `respx` mocks** in `mootup-io/moot/tests/` so endpoint paths, request params, and response bodies match the current convo backend. The scaffold flow has silently drifted to a new endpoint (`GET /api/actors/me/agents`) that none of the mocks stub — all 9 `test_scaffold` tests fail with `AllMockedAssertionError`. Anchor per endpoint: OAS for path/method/params (authoritative), backend route handlers and Pydantic models (authoritative for response body — OAS emits `additionalProperties: true` so it does not constrain body shape).
|
|
15
|
+
2. **Retire dead tests** that have been failing since the alpha bring-up: `test_example.py` (6 tests tracking a non-existent `examples/markdraft/` directory) and `test_templates.py::test_publish_doc_exists` (tests a non-existent `docs/publish.md`).
|
|
16
|
+
|
|
17
|
+
Mechanical-lift pipeline. No semantic design decisions. Three D-decisions documented in § 4, all resolved in-draft.
|
|
18
|
+
|
|
19
|
+
## 2. Baseline (cross-repo first run — remeasured at feat-tip)
|
|
20
|
+
|
|
21
|
+
BASELINE-FROZEN @ `1546cf9` in `/workspaces/convo/mootup-io/moot/.worktrees/spec/`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
$ .venv/bin/python -m pytest
|
|
25
|
+
124 tests collected
|
|
26
|
+
15 failed, 109 passed in 3.17s
|
|
27
|
+
|
|
28
|
+
Failures:
|
|
29
|
+
tests/test_example.py::test_moot_toml_valid (examples/markdraft/ missing)
|
|
30
|
+
tests/test_example.py::test_devcontainer_json_valid (examples/markdraft/ missing)
|
|
31
|
+
tests/test_example.py::test_post_create_installs_moot (examples/markdraft/ missing)
|
|
32
|
+
tests/test_example.py::test_runner_scripts_unchanged (examples/markdraft/ missing)
|
|
33
|
+
tests/test_example.py::test_gitignore_entries (examples/markdraft/ missing)
|
|
34
|
+
tests/test_scaffold.py::test_init_greenfield_rotates_and_installs
|
|
35
|
+
tests/test_scaffold.py::test_init_conflict_stages_claude_md
|
|
36
|
+
tests/test_scaffold.py::test_init_conflict_stages_skill
|
|
37
|
+
tests/test_scaffold.py::test_init_conflict_stages_devcontainer
|
|
38
|
+
tests/test_scaffold.py::test_init_force_rotates_keys
|
|
39
|
+
tests/test_scaffold.py::test_init_adopt_fresh_install_overwrites
|
|
40
|
+
tests/test_scaffold.py::test_init_rotate_key_failure_does_not_persist
|
|
41
|
+
tests/test_scaffold.py::test_init_warns_on_non_git_repo
|
|
42
|
+
tests/test_scaffold.py::test_init_placeholder_substitution
|
|
43
|
+
tests/test_templates.py::test_publish_doc_exists (docs/publish.md missing)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
$ .venv/bin/python -m pyright
|
|
48
|
+
12 errors, 0 warnings, 0 informations
|
|
49
|
+
src/moot/adapters/mcp_adapter.py (11 errors — pre-existing, out of scope)
|
|
50
|
+
tests/test_launch.py (1 error — pre-existing, out of scope)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**No pytest-xdist** in moot-cli (`pyproject.toml` test deps: `pytest>=8.0`, `pytest-asyncio>=0.24`, `respx>=0.22`). All test commands in this spec use plain `pytest` — do NOT add `-n auto`.
|
|
54
|
+
|
|
55
|
+
**Pre-existing pyright errors (12) are out of scope.** This run touches only `tests/test_scaffold.py`, `tests/test_example.py`, `tests/test_templates.py`. Ship gate is "no NEW pyright errors introduced" — baseline total may remain at 12.
|
|
56
|
+
|
|
57
|
+
## 3. Scope
|
|
58
|
+
|
|
59
|
+
**In:**
|
|
60
|
+
- Refresh respx mock bodies in `tests/test_scaffold.py` so endpoint paths match the current scaffold flow and response bodies match `Actor.model_dump()` / `SpaceInfo.model_dump()` shape.
|
|
61
|
+
- Refresh respx mock bodies in `tests/test_provision.py` and `tests/test_auth.py` so response bodies match the backend's actual model_dump() shape (currently passing but author-imagined minimal dicts).
|
|
62
|
+
- Delete `tests/test_example.py` in full (6 tests against non-existent `examples/markdraft/`).
|
|
63
|
+
- Delete `tests/test_templates.py::test_publish_doc_exists` (single test against non-existent `docs/publish.md`).
|
|
64
|
+
|
|
65
|
+
**Out:**
|
|
66
|
+
- Generating a typed Python client from the OAS (separate task, not this run).
|
|
67
|
+
- Backend (convo) changes. OAS is the emitted contract; it is read-only here.
|
|
68
|
+
- Adding new test coverage beyond refresh + deletion.
|
|
69
|
+
- Fixing pre-existing pyright errors in `mcp_adapter.py` / `test_launch.py`.
|
|
70
|
+
- Deleting dead CLI commands (`cmd_provision`, cosmetic fallback in scaffold.py) — see § 11 findings.
|
|
71
|
+
- Restoring `examples/markdraft/` or `docs/publish.md` (deferred; if either ships later, add new tests against the real tree then).
|
|
72
|
+
|
|
73
|
+
## 4. Decisions (resolved in-draft)
|
|
74
|
+
|
|
75
|
+
### D1 — Response-body anchor when OAS does not constrain shape
|
|
76
|
+
|
|
77
|
+
The convo OAS declares almost every successful response body as `additionalProperties: true, type: object` (or `list[...dict]`). FastAPI routes emit `dict = model.model_dump()` without a `response_model=` declaration, so the OAS reflects "any object." Validating mock bodies against the OAS therefore admits any dict — useless as a drift gate on shape.
|
|
78
|
+
|
|
79
|
+
**Resolution:** OAS is authoritative for **endpoint path, method, path params, query params, and request body** (where `$ref` schemas exist). **Response body shape** is anchored to the backend Pydantic model that each route handler's `model_dump()` call exposes (`Actor`, `SpaceInfo`, etc.) — Spec enumerates the required fields per mock in § 6.1. This matches `feedback_cross_repo_http_mocks_via_oas` ("grep the route handler for its return type and copy fields exactly"). Revisit once convo routes gain `response_model=` declarations (out of scope here).
|
|
80
|
+
|
|
81
|
+
### D2 — `test_example.py` triage: delete the whole file
|
|
82
|
+
|
|
83
|
+
Every test in `test_example.py` reads from `EXAMPLE_DIR = Path(__file__).parent.parent.parent / "examples" / "markdraft"`. The `examples/` directory does not exist at `feat/oas-mock-refresh` tip and is not tracked in git (`git ls-files examples/` empty). Five tests fail with `FileNotFoundError`; one (`test_no_convo_specific_paths`) passes vacuously because `rglob("*")` over a missing directory yields nothing.
|
|
84
|
+
|
|
85
|
+
**Options:**
|
|
86
|
+
- (a) Delete the whole file.
|
|
87
|
+
- (b) Skip all tests with `@pytest.mark.skip(reason="examples/markdraft/ not yet restored")`.
|
|
88
|
+
- (c) Recreate `examples/markdraft/`. Out of scope (Product-direction, not test hygiene).
|
|
89
|
+
|
|
90
|
+
**Resolution: (a) delete the file.** The tests describe a fixture tree that has been absent for the entire alpha-stabilization window. If examples are ever restored, their shape will differ and the tests would need to be rewritten anyway — skip-markers would be dead scaffolding.
|
|
91
|
+
|
|
92
|
+
### D3 — `test_publish_doc_exists` triage: delete the single test
|
|
93
|
+
|
|
94
|
+
`tests/test_templates.py::test_publish_doc_exists` asserts `docs/publish.md` exists in the moot repo. Neither the file nor the `docs/` directory exists (`git ls-files docs/` empty). The publish procedure the test was guarding has not landed in-tree.
|
|
95
|
+
|
|
96
|
+
**Resolution:** delete the single test body from `test_templates.py`. Keep the other 17 tests in that file (all passing). If publish docs land later, add a fresh test pinned to whatever is actually shipped.
|
|
97
|
+
|
|
98
|
+
## 5. Endpoint catalogue (what each mock currently asserts vs what the backend emits)
|
|
99
|
+
|
|
100
|
+
Authoritative refs: `backend/core/models/models.py` (Actor, SpaceInfo), `backend/api/routes/actors.py`, `backend/api/routes/spaces.py`, `/workspaces/convo/docs/api/openapi.yaml`.
|
|
101
|
+
|
|
102
|
+
### 5.1 `GET /api/actors/me` — returns `Actor.model_dump()`
|
|
103
|
+
|
|
104
|
+
**Fields** (all present in the dump):
|
|
105
|
+
|
|
106
|
+
| field | type | notes |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `actor_id` | `str` | encoded `agt_*` / `usr_*` |
|
|
109
|
+
| `display_name` | `str` | |
|
|
110
|
+
| `actor_type` | `str` | `"human"` \| `"agent"` |
|
|
111
|
+
| `sponsor_id` | `str \| None` | |
|
|
112
|
+
| `tenant_id` | `str \| None` | FK |
|
|
113
|
+
| `is_admin` | `bool` | default False |
|
|
114
|
+
| `email` | `str \| None` | |
|
|
115
|
+
| `agent_profile` | `str \| None` | |
|
|
116
|
+
| `api_key_prefix` | `str \| None` | first 8 chars of live key, None if keyless |
|
|
117
|
+
| `default_space_id` | `str \| None` | encoded `spc_*` |
|
|
118
|
+
| `is_connected` | `bool \| None` | agent-only; None for humans |
|
|
119
|
+
| `focus_space_id` | `str \| None` | |
|
|
120
|
+
| `metadata` | `dict \| None` | |
|
|
121
|
+
| `last_seen_at` | `str \| None` | ISO-8601 |
|
|
122
|
+
| `created_at` | `str` | ISO-8601, required |
|
|
123
|
+
| `updated_at` | `str` | ISO-8601, required |
|
|
124
|
+
|
|
125
|
+
**Consumer reads in moot-cli:**
|
|
126
|
+
- `scaffold.py:176` — `actor.get("default_space_id")` (uses)
|
|
127
|
+
- `provision.py:49` — `me.get("tenant_id")` (uses)
|
|
128
|
+
- `auth.py` — cmd_login uses only the presence of 200 (no field reads)
|
|
129
|
+
|
|
130
|
+
### 5.2 `GET /api/spaces/{space_id}` — NOT IMPLEMENTED
|
|
131
|
+
|
|
132
|
+
OAS has `patch:` but NO `get:` on this path (`api/routes/spaces.py` line 155 is the patch; no GET). `scaffold.py:181` issues a GET and silently swallows the 404/405 via `space_resp.status_code == 200` guard, falling back to `space_name = space_id`. See § 11.F1 for disposition.
|
|
133
|
+
|
|
134
|
+
**Mock disposition:** stub a **404** response (matches production behavior); remove the bogus `{"name": "Test Space"}` body. This keeps the fallback path exercised by tests.
|
|
135
|
+
|
|
136
|
+
### 5.3 `GET /api/spaces/{space_id}/participants` — NO LONGER CALLED BY SCAFFOLD
|
|
137
|
+
|
|
138
|
+
`scaffold.py` replaced this endpoint with `/api/actors/me/agents` (see docstring of `_fetch_keyless_agents` at scaffold.py:190–214, which narrates the switch). The endpoint is still live on the backend (`api/routes/spaces.py:...`) but moot-cli no longer calls it in the init flow.
|
|
139
|
+
|
|
140
|
+
**Mock disposition:** DELETE the `/api/spaces/{space_id}/participants` mock stub from `_stub_backend` (and from every per-test inline stub). Because `@respx.mock` defaults to `assert_all_called=True`, leaving an unused mock would flip every test from `AllMockedAssertionError` to `AllCalledAssertionError`.
|
|
141
|
+
|
|
142
|
+
### 5.4 `GET /api/actors/me/agents` — NEW (returns `list[Actor.model_dump()]`)
|
|
143
|
+
|
|
144
|
+
Route: `api/routes/actors.py:217–225`.
|
|
145
|
+
```python
|
|
146
|
+
@router.get("/api/actors/me/agents")
|
|
147
|
+
async def get_my_agents(actor: Actor = Depends(require_actor)) -> list[dict]:
|
|
148
|
+
if actor.actor_type != "human":
|
|
149
|
+
raise HTTPException(status_code=403, ...)
|
|
150
|
+
...
|
|
151
|
+
return [a.model_dump() for a in agents]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Returns **all agents sponsored by the caller**, regardless of space. Each dict carries the full Actor shape (§ 5.1).
|
|
155
|
+
|
|
156
|
+
**Consumer reads in moot-cli** (`scaffold.py:215–225`):
|
|
157
|
+
- `a.get("actor_type") == "agent"` — filters to agents
|
|
158
|
+
- `a.get("api_key_prefix")` — filters to keyless when `force=False`
|
|
159
|
+
- `a["actor_id"]` — used in rotate-key URL
|
|
160
|
+
- `a["display_name"]` — used for role key
|
|
161
|
+
|
|
162
|
+
**Mock disposition:** ADD a `/api/actors/me/agents` stub to `_stub_backend` returning 4 Actor dicts (one per role), each with `actor_type="agent"` and `api_key_prefix=None` (so the keyless filter admits all 4). Other Actor fields set to representative values.
|
|
163
|
+
|
|
164
|
+
### 5.5 `POST /api/actors/{actor_id}/rotate-key` — returns `Actor.model_dump()` + `{"api_key": ...}`
|
|
165
|
+
|
|
166
|
+
Route: `api/routes/actors.py:397–401`.
|
|
167
|
+
```python
|
|
168
|
+
refreshed = await actor_store.get_actor(pool, actor_id)
|
|
169
|
+
result = refreshed.model_dump() if refreshed else target.model_dump()
|
|
170
|
+
result["api_key"] = new_key
|
|
171
|
+
return result
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Consumer reads in moot-cli** (`scaffold.py:250–255`): `data.get("api_key", "")` only. The other Actor fields are emitted but unused by moot-cli.
|
|
175
|
+
|
|
176
|
+
**Mock disposition:** response body must include `api_key` + full Actor shape. Current mocks emit `{"api_key": "convo_key_live_{role}"}` only — thin but functionally sufficient. Per D1, expand to full Actor + api_key so the test documents the real shape.
|
|
177
|
+
|
|
178
|
+
### 5.6 `POST /api/tenants/{tenant_id}/agents` — PHANTOM (not in OAS; not in routes/)
|
|
179
|
+
|
|
180
|
+
Called by `provision.py:56–62`. Does not exist on the backend (OAS grep for `/api/tenants/` shows only `POST /api/tenants`, `GET /api/tenants/{tenant_id}`, and admin variants — no `/{tenant_id}/agents`). In production this request would 404.
|
|
181
|
+
|
|
182
|
+
`test_provision.py::test_provision_fresh_writes_moot_agents_fresh_json` mocks the phantom endpoint with `{"actor_id": "agt_p", "api_key": "convo_key_fresh"}` and self-confirms. See § 11.F2 for disposition.
|
|
183
|
+
|
|
184
|
+
**Mock disposition:** keep the existing phantom mock as-is (Product-direction call; see § 11.F2). Add a code comment in the test file citing this spec so the phantom is visible.
|
|
185
|
+
|
|
186
|
+
## 6. Files to modify
|
|
187
|
+
|
|
188
|
+
| # | File | Action | Scope |
|
|
189
|
+
|---|------|--------|-------|
|
|
190
|
+
| 1 | `tests/test_example.py` | **delete** | removes 6 tests (5 failing + 1 vacuous) |
|
|
191
|
+
| 2 | `tests/test_templates.py` | edit | delete the 22-line `test_publish_doc_exists` body (1 test) |
|
|
192
|
+
| 3 | `tests/test_scaffold.py` | edit | rewrite `_stub_backend` helper, update 2 inline stubs in `test_init_rotate_key_failure_does_not_persist`, adjust one assertion that indexes `respx.mock.routes[3]` |
|
|
193
|
+
| 4 | `tests/test_provision.py` | edit | expand the `/api/actors/me` mock body to match `Actor` shape; add comment annotating the phantom `/api/tenants/.../agents` endpoint |
|
|
194
|
+
| 5 | `tests/test_auth.py` | edit | expand the 2 `/api/actors/me` mocks to match `Actor` shape |
|
|
195
|
+
|
|
196
|
+
No source code (`src/moot/*.py`) changes. No new files. No pyproject/lockfile changes.
|
|
197
|
+
|
|
198
|
+
## 6.1 Canonical Actor mock dict (paste-ready)
|
|
199
|
+
|
|
200
|
+
Use this dict (fill `actor_id`, `display_name`, `actor_type`, `sponsor_id`, `api_key_prefix`, `default_space_id` per call site) as the common body shape for every Actor-returning mock in this run. All other fields use these defaults:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
def _actor_dict(
|
|
204
|
+
*,
|
|
205
|
+
actor_id: str,
|
|
206
|
+
display_name: str,
|
|
207
|
+
actor_type: str = "agent",
|
|
208
|
+
sponsor_id: str | None = "usr_test_1",
|
|
209
|
+
api_key_prefix: str | None = None,
|
|
210
|
+
default_space_id: str | None = None,
|
|
211
|
+
is_connected: bool | None = False,
|
|
212
|
+
) -> dict:
|
|
213
|
+
"""Return a full Actor.model_dump() shape for respx mocks.
|
|
214
|
+
|
|
215
|
+
Field order and defaults match backend/core/models/models.py::Actor.
|
|
216
|
+
Only the fields callers vary are parameters; the rest are fixed
|
|
217
|
+
canonical defaults so every mock emits a shape indistinguishable
|
|
218
|
+
from a real backend response.
|
|
219
|
+
"""
|
|
220
|
+
return {
|
|
221
|
+
"actor_id": actor_id,
|
|
222
|
+
"display_name": display_name,
|
|
223
|
+
"actor_type": actor_type,
|
|
224
|
+
"sponsor_id": sponsor_id,
|
|
225
|
+
"tenant_id": "ten_test_1",
|
|
226
|
+
"is_admin": False,
|
|
227
|
+
"email": None,
|
|
228
|
+
"agent_profile": None,
|
|
229
|
+
"api_key_prefix": api_key_prefix,
|
|
230
|
+
"default_space_id": default_space_id,
|
|
231
|
+
"is_connected": is_connected,
|
|
232
|
+
"focus_space_id": None,
|
|
233
|
+
"metadata": None,
|
|
234
|
+
"last_seen_at": None,
|
|
235
|
+
"created_at": "2026-04-18T00:00:00+00:00",
|
|
236
|
+
"updated_at": "2026-04-18T00:00:00+00:00",
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Place at the top of `tests/test_scaffold.py` after imports (before `_stub_backend`). **Do NOT also place it in `test_provision.py` or `test_auth.py`** — their Actor shapes are simple enough to inline; duplicating the helper adds no value.
|
|
241
|
+
|
|
242
|
+
## 7. § 6.1 — test_scaffold.py canonical rewrite
|
|
243
|
+
|
|
244
|
+
### 7.1 Rewrite `_stub_backend`
|
|
245
|
+
|
|
246
|
+
Replace lines 17–56 of `tests/test_scaffold.py` with:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
def _stub_backend(respx_mock: respx.Router, api_url: str) -> None:
|
|
250
|
+
"""Stub the 3-call happy-path flow (post-Run-W scaffold).
|
|
251
|
+
|
|
252
|
+
Endpoints (all anchored to convo OAS b6f6d13):
|
|
253
|
+
GET /api/actors/me — Actor.model_dump()
|
|
254
|
+
GET /api/spaces/{space_id} — 404 (no GET handler; scaffold falls back to space_id)
|
|
255
|
+
GET /api/actors/me/agents — list[Actor.model_dump()]
|
|
256
|
+
POST /api/actors/{actor_id}/rotate-key — Actor.model_dump() + api_key
|
|
257
|
+
"""
|
|
258
|
+
respx_mock.get(f"{api_url}/api/actors/me").mock(
|
|
259
|
+
return_value=Response(
|
|
260
|
+
200,
|
|
261
|
+
json=_actor_dict(
|
|
262
|
+
actor_id="usr_user_1",
|
|
263
|
+
display_name="Test User",
|
|
264
|
+
actor_type="human",
|
|
265
|
+
sponsor_id=None,
|
|
266
|
+
default_space_id="spc_test_1",
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
# GET /api/spaces/{id} is not implemented; scaffold swallows the 404.
|
|
271
|
+
respx_mock.get(f"{api_url}/api/spaces/spc_test_1").mock(
|
|
272
|
+
return_value=Response(404, json={"detail": "Not found"})
|
|
273
|
+
)
|
|
274
|
+
respx_mock.get(f"{api_url}/api/actors/me/agents").mock(
|
|
275
|
+
return_value=Response(
|
|
276
|
+
200,
|
|
277
|
+
json=[
|
|
278
|
+
_actor_dict(
|
|
279
|
+
actor_id=f"agt_{role.lower()}_1",
|
|
280
|
+
display_name=role,
|
|
281
|
+
)
|
|
282
|
+
for role in ("Product", "Spec", "Implementation", "QA")
|
|
283
|
+
],
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
for role in ("product", "spec", "implementation", "qa"):
|
|
287
|
+
respx_mock.post(
|
|
288
|
+
f"{api_url}/api/actors/agt_{role}_1/rotate-key"
|
|
289
|
+
).mock(
|
|
290
|
+
return_value=Response(
|
|
291
|
+
200,
|
|
292
|
+
json={
|
|
293
|
+
**_actor_dict(
|
|
294
|
+
actor_id=f"agt_{role}_1",
|
|
295
|
+
display_name=role.capitalize(),
|
|
296
|
+
api_key_prefix="convo_ke",
|
|
297
|
+
),
|
|
298
|
+
"api_key": f"convo_key_live_{role}",
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 7.2 `respx.mock.routes[3]` assertion in `test_init_force_rotates_keys`
|
|
305
|
+
|
|
306
|
+
Current line 263:
|
|
307
|
+
```python
|
|
308
|
+
rotate_call = respx.mock.routes[3].calls.last
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The new `_stub_backend` registers routes in a different order: `/api/actors/me` (0), `/api/spaces/spc_test_1` (1), `/api/actors/me/agents` (2), `/api/actors/agt_product_1/rotate-key` (3), `.../agt_spec_1/rotate-key` (4), `.../agt_implementation_1/rotate-key` (5), `.../agt_qa_1/rotate-key` (6). Index 3 is still the first rotate-key — assertion semantics are preserved. **No change needed**.
|
|
312
|
+
|
|
313
|
+
### 7.3 `test_init_rotate_key_failure_does_not_persist` inline stubs
|
|
314
|
+
|
|
315
|
+
Lines 361–391 (inline stubs duplicating `_stub_backend`'s first 3 calls + a 500 on rotate-key for product only). Replace the 3 inline non-failure stubs with:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
respx.mock.get(f"{api_url}/api/actors/me").mock(
|
|
319
|
+
return_value=Response(
|
|
320
|
+
200,
|
|
321
|
+
json=_actor_dict(
|
|
322
|
+
actor_id="usr_user_1",
|
|
323
|
+
display_name="Test User",
|
|
324
|
+
actor_type="human",
|
|
325
|
+
sponsor_id=None,
|
|
326
|
+
default_space_id="spc_test_1",
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
respx.mock.get(f"{api_url}/api/spaces/spc_test_1").mock(
|
|
331
|
+
return_value=Response(404, json={"detail": "Not found"})
|
|
332
|
+
)
|
|
333
|
+
respx.mock.get(f"{api_url}/api/actors/me/agents").mock(
|
|
334
|
+
return_value=Response(
|
|
335
|
+
200,
|
|
336
|
+
json=[
|
|
337
|
+
_actor_dict(
|
|
338
|
+
actor_id="agt_product_1",
|
|
339
|
+
display_name="Product",
|
|
340
|
+
)
|
|
341
|
+
],
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Keep the existing `respx.mock.post(f"{api_url}/api/actors/agt_product_1/rotate-key").mock(return_value=Response(500, json={"error": "boom"}))` line unchanged.
|
|
347
|
+
|
|
348
|
+
### 7.4 Import line
|
|
349
|
+
|
|
350
|
+
Top of `tests/test_scaffold.py` is already:
|
|
351
|
+
```python
|
|
352
|
+
import respx
|
|
353
|
+
from httpx import Response
|
|
354
|
+
```
|
|
355
|
+
No new imports needed.
|
|
356
|
+
|
|
357
|
+
## 8. § 6.2 — test_provision.py mock expansion
|
|
358
|
+
|
|
359
|
+
Update lines 54–63 to emit a full Actor shape for `/api/actors/me` (so the test documents the real shape) and add a comment pointing at the phantom endpoint:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
respx.mock.get("https://mootup.io/api/actors/me").mock(
|
|
363
|
+
return_value=Response(
|
|
364
|
+
200,
|
|
365
|
+
json={
|
|
366
|
+
"actor_id": "agt_u",
|
|
367
|
+
"display_name": "Test User",
|
|
368
|
+
"actor_type": "human",
|
|
369
|
+
"sponsor_id": None,
|
|
370
|
+
"tenant_id": "ten_1",
|
|
371
|
+
"is_admin": False,
|
|
372
|
+
"email": None,
|
|
373
|
+
"agent_profile": None,
|
|
374
|
+
"api_key_prefix": None,
|
|
375
|
+
"default_space_id": None,
|
|
376
|
+
"is_connected": None,
|
|
377
|
+
"focus_space_id": None,
|
|
378
|
+
"metadata": None,
|
|
379
|
+
"last_seen_at": None,
|
|
380
|
+
"created_at": "2026-04-18T00:00:00+00:00",
|
|
381
|
+
"updated_at": "2026-04-18T00:00:00+00:00",
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
# NOTE: POST /api/tenants/{tenant_id}/agents is not implemented on the
|
|
386
|
+
# convo backend at b6f6d13 (no matching route; not in openapi.yaml).
|
|
387
|
+
# See docs/specs/oas-mock-refresh.md § 11.F2 — disposition deferred to
|
|
388
|
+
# Product (task #53 follow-up).
|
|
389
|
+
respx.mock.post("https://mootup.io/api/tenants/ten_1/agents").mock(
|
|
390
|
+
return_value=Response(
|
|
391
|
+
201, json={"actor_id": "agt_p", "api_key": "convo_key_fresh"}
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## 9. § 6.3 — test_auth.py mock expansion
|
|
397
|
+
|
|
398
|
+
Update lines 105–109 and 137–141 (two near-identical blocks). Current shape:
|
|
399
|
+
```python
|
|
400
|
+
mock.get("/api/actors/me").respond(
|
|
401
|
+
200,
|
|
402
|
+
json={"actor_id": "usr_test", "display_name": "Test User"},
|
|
403
|
+
)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Replace each with:
|
|
407
|
+
```python
|
|
408
|
+
mock.get("/api/actors/me").respond(
|
|
409
|
+
200,
|
|
410
|
+
json={
|
|
411
|
+
"actor_id": "usr_test", # or "usr_bypass" in the second block
|
|
412
|
+
"display_name": "Test User", # or "Bypass User"
|
|
413
|
+
"actor_type": "human",
|
|
414
|
+
"sponsor_id": None,
|
|
415
|
+
"tenant_id": "ten_test_1",
|
|
416
|
+
"is_admin": False,
|
|
417
|
+
"email": None,
|
|
418
|
+
"agent_profile": None,
|
|
419
|
+
"api_key_prefix": None,
|
|
420
|
+
"default_space_id": None,
|
|
421
|
+
"is_connected": None,
|
|
422
|
+
"focus_space_id": None,
|
|
423
|
+
"metadata": None,
|
|
424
|
+
"last_seen_at": None,
|
|
425
|
+
"created_at": "2026-04-18T00:00:00+00:00",
|
|
426
|
+
"updated_at": "2026-04-18T00:00:00+00:00",
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`cmd_login` only reads status 200 (see `auth.py:81–end`), so the additional fields are documentation, not behavior — but they keep the mocks honest to the real response shape.
|
|
432
|
+
|
|
433
|
+
## 10. § 7 — test_example.py deletion
|
|
434
|
+
|
|
435
|
+
`git rm tests/test_example.py`. No other references to this file in the tree (grep confirmed: the only occurrences are inside the file itself).
|
|
436
|
+
|
|
437
|
+
## 11. § 8 — test_templates.py single-test deletion
|
|
438
|
+
|
|
439
|
+
In `tests/test_templates.py`, delete the test function `test_publish_doc_exists` (one `def test_publish_doc_exists` block, 22 lines including its docstring referring to "Product scope item 4 (Run V)"). Leave every other test in the file untouched.
|
|
440
|
+
|
|
441
|
+
## 12. Pytest count formula
|
|
442
|
+
|
|
443
|
+
Baseline: 124 collected, 15 failed, 109 passed.
|
|
444
|
+
|
|
445
|
+
| Change | count |
|
|
446
|
+
|---|---|
|
|
447
|
+
| drops | −6 (test_example.py) − 1 (test_publish_doc_exists) = **−7** |
|
|
448
|
+
| rewrites | 0 (test_scaffold bodies rewritten in-place; no new `def test_*` lines) |
|
|
449
|
+
| adds | 0 |
|
|
450
|
+
| net | **−7** |
|
|
451
|
+
|
|
452
|
+
**Projected final:** 117 collected, 0 failed, 117 passed.
|
|
453
|
+
|
|
454
|
+
Breakdown of failures-to-green:
|
|
455
|
+
- 5 test_example failures → deleted
|
|
456
|
+
- 1 test_publish_doc_exists failure → deleted
|
|
457
|
+
- 9 test_scaffold failures → pass (mock refresh)
|
|
458
|
+
- Remainder: 109 pre-existing pass + 9 scaffold back in green = 117 passing.
|
|
459
|
+
|
|
460
|
+
## 13. Incremental plan for Impl (three stages, each independently green)
|
|
461
|
+
|
|
462
|
+
### Stage 1 — delete dead tests
|
|
463
|
+
|
|
464
|
+
1. `git rm tests/test_example.py`.
|
|
465
|
+
2. Delete `test_publish_doc_exists` from `tests/test_templates.py`.
|
|
466
|
+
3. `.venv/bin/python -m pytest` → **117 collected, 9 failed, 108 passed.** (Only scaffold failures remain; example+templates failures gone.)
|
|
467
|
+
|
|
468
|
+
### Stage 2 — test_scaffold.py mock refresh
|
|
469
|
+
|
|
470
|
+
1. Add `_actor_dict` helper at the top of `tests/test_scaffold.py` (§ 6.1).
|
|
471
|
+
2. Replace `_stub_backend` body (§ 7.1).
|
|
472
|
+
3. Update inline stubs in `test_init_rotate_key_failure_does_not_persist` (§ 7.3).
|
|
473
|
+
4. No change to `respx.mock.routes[3]` assertion (verify § 7.2 analysis).
|
|
474
|
+
5. `.venv/bin/python -m pytest tests/test_scaffold.py` → **13 passed** (9 refreshed + 4 pre-existing passers: `test_init_refuses_without_force_when_actors_exist`, `test_init_update_suggestions_no_network`, `test_infer_team_template`, `test_launch_includes_channel_flag`).
|
|
475
|
+
|
|
476
|
+
### Stage 3 — test_provision.py + test_auth.py shape expansion
|
|
477
|
+
|
|
478
|
+
1. Expand the `/api/actors/me` mock in test_provision.py (§ 8) and add the phantom-endpoint comment.
|
|
479
|
+
2. Expand the two `/api/actors/me` mocks in test_auth.py (§ 9).
|
|
480
|
+
3. `.venv/bin/python -m pytest` → **117 collected, 0 failed, 117 passed.**
|
|
481
|
+
4. `.venv/bin/python -m pyright` → **≤ 12 errors** (same files as baseline: `mcp_adapter.py` + `test_launch.py`; no new errors in the test files touched).
|
|
482
|
+
|
|
483
|
+
Each stage is a single commit so Leader can bisect if anything regresses.
|
|
484
|
+
|
|
485
|
+
## 14. Surprises for Impl
|
|
486
|
+
|
|
487
|
+
### Missing-imports audit
|
|
488
|
+
|
|
489
|
+
Every symbol referenced in § 6/§ 7/§ 8/§ 9 code blocks is already imported in the target files. Confirmed:
|
|
490
|
+
- `test_scaffold.py` imports: `respx`, `Response`, `pytest`, `json`, `os`, `stat`, `Path`, `cmd_init`, `ACTORS_JSON`. New `_actor_dict` uses only `dict` (builtin). No new imports needed.
|
|
491
|
+
- `test_provision.py` imports: `respx`, `Response`, `pytest`, `asyncio`, `json`, `Path`. No new imports needed.
|
|
492
|
+
- `test_auth.py` imports: already has `respx` (verified at line 105, 137). No new imports needed.
|
|
493
|
+
|
|
494
|
+
### Findings (out of scope; report to Product after ship)
|
|
495
|
+
|
|
496
|
+
**F1. `GET /api/spaces/{space_id}` is not implemented on the backend.** `scaffold.py:181` calls it and swallows the 404; `space_name` falls back to `space_id`. In production the user sees the space ID as the space name. Disposition options: (a) add the GET handler in backend (convo repo change); (b) remove the cosmetic call from moot-cli; (c) ship as-is. Not in this run's scope. Flag for Product at retro.
|
|
497
|
+
|
|
498
|
+
**F2. `POST /api/tenants/{tenant_id}/agents` is a phantom endpoint.** `provision.py:56` calls it; the OAS does not list it (grep `/api/tenants/` in openapi.yaml returns only `POST /api/tenants`, `GET /api/tenants/{tenant_id}`, and admin variants). `cmd_provision` would 404 in production. Disposition options: (a) delete `cmd_provision` and `test_provision.py` entirely; (b) add the route in backend; (c) ship as-is. The current run ships the code comment annotation (see § 8) so the phantom is visible to future readers. Design decision deferred to Product.
|
|
499
|
+
|
|
500
|
+
**F3. `@respx.mock` defaults to `assert_all_called=True` AND `assert_all_mocked=True`.** The § 7.1 `_stub_backend` rewrite DELETES the old `/api/spaces/{id}/participants` mock (no longer called) and ADDS the `/api/actors/me/agents` mock (now called). Leaving either one wrong flips every test. If Impl sees a new `AllCalledAssertionError` after stage 2, an old mock was retained; if `AllMockedAssertionError`, a new endpoint is missing. Both surface in a single green test run once § 7.1 is applied verbatim.
|
|
501
|
+
|
|
502
|
+
### Pyright on the rewritten test bodies
|
|
503
|
+
|
|
504
|
+
Dry-ran the embedded helper + mock snippets against `basic` mode pythonVersion 3.11. No `object`-typed captures, no `list[dict[str, object]]` subscript reads, no `monkeypatch.setattr` on paths that do not resolve. Clean.
|
|
505
|
+
|
|
506
|
+
### No xdist
|
|
507
|
+
|
|
508
|
+
`pyproject.toml` does not depend on `pytest-xdist`. Use plain `pytest` / `.venv/bin/python -m pytest`. Do not add `-n auto`.
|
|
509
|
+
|
|
510
|
+
### Cross-repo source of truth
|
|
511
|
+
|
|
512
|
+
OAS path at `/workspaces/convo/docs/api/openapi.yaml` is read-only from the moot worktree. Impl should `cat` or `grep` it for reference during implementation; do NOT copy it into the moot repo. The Actor / SpaceInfo models at `/workspaces/convo/backend/core/models/models.py` (lines 28–44 for Actor, 101–107 for SpaceInfo) are the canonical response-body reference.
|
|
513
|
+
|
|
514
|
+
## 15. QA spot-checks (§ 12)
|
|
515
|
+
|
|
516
|
+
**Q-1** — `git log -1 --stat` shows three commits, one per stage; no unrelated files touched.
|
|
517
|
+
|
|
518
|
+
**Q-2** — `git diff main -- src/` is empty (source code untouched).
|
|
519
|
+
|
|
520
|
+
**Q-3** — `git ls-files tests/test_example.py` returns empty; the file is gone.
|
|
521
|
+
|
|
522
|
+
**Q-4** — `grep -n '/api/spaces/{.*}/participants' tests/` is empty (the stale stub path is gone from all test files).
|
|
523
|
+
|
|
524
|
+
**Q-5** — `grep -n '/api/actors/me/agents' tests/test_scaffold.py` returns ≥ 1 hit (the new endpoint is present).
|
|
525
|
+
|
|
526
|
+
**Q-6** — `.venv/bin/python -m pytest` shows exactly `117 passed` (or whatever number matches § 12 net after the deletions; projected 117).
|
|
527
|
+
|
|
528
|
+
**Q-7** — `.venv/bin/python -m pyright` shows ≤ 12 errors, all in `mcp_adapter.py` (11) + `test_launch.py` (1). Zero new errors in the 4 touched test files.
|
|
529
|
+
|
|
530
|
+
**Q-8** — `test_init_force_rotates_keys` passes (validates § 7.2 index-3 assertion still points to the first rotate-key call).
|
|
531
|
+
|
|
532
|
+
**Q-9** — `grep -n "phantom" tests/test_provision.py` returns ≥ 1 hit (the F2-annotated comment landed in the file).
|
|
533
|
+
|
|
534
|
+
**Q-10** — Spot-read `tests/test_scaffold.py` and confirm `_actor_dict` appears exactly once (helper is not duplicated in per-test bodies).
|
|
535
|
+
|
|
536
|
+
## 16. § 13 — Verbatim grounding commands and output
|
|
537
|
+
|
|
538
|
+
Run in `/workspaces/convo/mootup-io/moot/.worktrees/spec/` at `1546cf9`.
|
|
539
|
+
|
|
540
|
+
### Phase A — confirm missing directories
|
|
541
|
+
|
|
542
|
+
```
|
|
543
|
+
$ git ls-files examples/ → (empty)
|
|
544
|
+
$ git ls-files docs/ → (empty)
|
|
545
|
+
$ ls examples/ → ls: cannot access 'examples/': No such file or directory
|
|
546
|
+
$ ls docs/ → ls: cannot access 'docs/': No such file or directory
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Phase B — enumerate respx mocks in moot-cli tests
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
$ grep -rln 'respx\.' tests/
|
|
553
|
+
tests/test_auth.py
|
|
554
|
+
tests/test_provision.py
|
|
555
|
+
tests/test_scaffold.py
|
|
556
|
+
```
|
|
557
|
+
Three files, exactly the five edit-targets in § 6 (file #1 is a deletion; files #2/#3/#4/#5 match).
|
|
558
|
+
|
|
559
|
+
### Phase C — endpoint catalogue in src/moot/
|
|
560
|
+
|
|
561
|
+
```
|
|
562
|
+
$ grep -rn '"/api/[^"]*"\|f"/api/[^"]*"' src/moot/ | grep -v adapters/
|
|
563
|
+
src/moot/auth.py:81: resp = await client.get("/api/actors/me")
|
|
564
|
+
src/moot/provision.py:44: me_resp = await client.get("/api/actors/me")
|
|
565
|
+
src/moot/provision.py:57: f"/api/tenants/{tenant_id}/agents",
|
|
566
|
+
src/moot/scaffold.py:168: resp = await client.get("/api/actors/me")
|
|
567
|
+
src/moot/scaffold.py:181: space_resp = await client.get(f"/api/spaces/{space_id}")
|
|
568
|
+
src/moot/scaffold.py:215: resp = await client.get("/api/actors/me/agents")
|
|
569
|
+
src/moot/scaffold.py:241: f"/api/actors/{actor_id}/rotate-key",
|
|
570
|
+
```
|
|
571
|
+
7 call sites across scaffold.py / provision.py / auth.py — matches § 5.1–§ 5.6 cataloguing.
|
|
572
|
+
|
|
573
|
+
### Phase D — confirm phantom endpoints
|
|
574
|
+
|
|
575
|
+
```
|
|
576
|
+
$ grep -n '/api/spaces/{space_id}:' /workspaces/convo/docs/api/openapi.yaml
|
|
577
|
+
125: /api/spaces/{space_id}:
|
|
578
|
+
$ sed -n '125,130p' /workspaces/convo/docs/api/openapi.yaml
|
|
579
|
+
/api/spaces/{space_id}:
|
|
580
|
+
patch:
|
|
581
|
+
summary: Update Space
|
|
582
|
+
...
|
|
583
|
+
$ grep -n '/api/tenants/{tenant_id}/agents' /workspaces/convo/docs/api/openapi.yaml
|
|
584
|
+
(empty)
|
|
585
|
+
```
|
|
586
|
+
Confirms F1 (only PATCH on `/api/spaces/{id}`) and F2 (`/api/tenants/{id}/agents` not in OAS).
|
|
587
|
+
|
|
588
|
+
### Phase E — confirm route handlers for called endpoints
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
$ grep -n '@router.get("/api/actors/me"\|@router.get("/api/actors/me/agents"\|@router.post("/api/actors/{actor_id}/rotate-key"' \
|
|
592
|
+
/workspaces/convo/backend/api/routes/actors.py
|
|
593
|
+
169:@router.get("/api/actors/me")
|
|
594
|
+
217:@router.get("/api/actors/me/agents")
|
|
595
|
+
358:@router.post("/api/actors/{actor_id}/rotate-key")
|
|
596
|
+
```
|
|
597
|
+
All three canonical endpoints exist on the backend.
|
|
598
|
+
|
|
599
|
+
### Phase F — Actor model field enumeration
|
|
600
|
+
|
|
601
|
+
`/workspaces/convo/backend/core/models/models.py:28–44`:
|
|
602
|
+
```
|
|
603
|
+
class Actor(BaseModel):
|
|
604
|
+
actor_id: str
|
|
605
|
+
display_name: str
|
|
606
|
+
actor_type: str
|
|
607
|
+
sponsor_id: str | None = None
|
|
608
|
+
tenant_id: str | None = None
|
|
609
|
+
is_admin: bool = False
|
|
610
|
+
email: str | None = None
|
|
611
|
+
agent_profile: str | None = None
|
|
612
|
+
api_key_prefix: str | None = None
|
|
613
|
+
default_space_id: str | None = None
|
|
614
|
+
is_connected: bool | None = None
|
|
615
|
+
focus_space_id: str | None = None
|
|
616
|
+
metadata: dict[str, Any] | None = None
|
|
617
|
+
last_seen_at: str | None = None
|
|
618
|
+
created_at: str
|
|
619
|
+
updated_at: str
|
|
620
|
+
```
|
|
621
|
+
16 fields. Matches § 5.1 exactly.
|
|
622
|
+
|
|
623
|
+
### Phase G — baseline pytest + pyright (reproduced for freeze)
|
|
624
|
+
|
|
625
|
+
```
|
|
626
|
+
$ .venv/bin/python -m pytest
|
|
627
|
+
124 tests collected
|
|
628
|
+
15 failed, 109 passed in 3.17s
|
|
629
|
+
|
|
630
|
+
$ .venv/bin/python -m pyright
|
|
631
|
+
12 errors, 0 warnings, 0 informations
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## 17. Ship gates
|
|
635
|
+
|
|
636
|
+
- `.venv/bin/python -m pytest` → 117 collected, 0 failed, 117 passed.
|
|
637
|
+
- `.venv/bin/python -m pyright` → ≤ 12 errors, all in `mcp_adapter.py` + `test_launch.py` (no new errors in `tests/test_scaffold.py`, `tests/test_provision.py`, `tests/test_auth.py`, `tests/test_templates.py`).
|
|
638
|
+
- `git diff main -- src/` empty (no source changes).
|
|
639
|
+
- `git ls-files tests/test_example.py` empty (deleted).
|
|
640
|
+
- No respx mock stubs paths containing `/api/spaces/{...}/participants` remain in any test file.
|
|
641
|
+
- `/api/actors/me/agents` is the new endpoint stubbed in `_stub_backend` and in the inline stub block of `test_init_rotate_key_failure_does_not_persist`.
|
|
642
|
+
|
|
643
|
+
## 18. Security considerations
|
|
644
|
+
|
|
645
|
+
- **No auth boundary changes.** All mocked endpoints are authenticated behind `Depends(require_actor)` on the backend; the tests already emit a PAT-bearing `store_credential` fixture.
|
|
646
|
+
- **No user-input sanitization surfaces touched.** The `_actor_dict` helper embeds literal ASCII strings only; no template interpolation against user data.
|
|
647
|
+
- **No secrets in mocks.** `api_key_prefix="convo_ke"` and `api_key="convo_key_live_{role}"` are clearly test-only patterns (do not match the production key prefix scheme — production uses 8-char prefixes of live keys, which start with `convo_`). Consider `convo_key_live_*` fixture values flagged as test-mock tokens by any CI secret scanner; no change needed here.
|
|
648
|
+
- **Phantom endpoint finding (F2)** is a security-adjacent finding: `cmd_provision` silently fails in production. Users will not successfully provision agents via this path and may fall through to other flows. Not in scope this run; flag to Product.
|
|
649
|
+
|
|
650
|
+
## 19. Open questions
|
|
651
|
+
|
|
652
|
+
**None are blocking.** All design calls are resolved in-draft (D1/D2/D3).
|
|
653
|
+
|
|
654
|
+
F1, F2 are findings for Product's follow-up, not decisions blocking this run. Spec will raise them in the retro.
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
*Spec frozen @ `1546cf9`. Ready for Impl. Three-stage incremental plan (§ 13); every stage is independently testable; no folding.*
|