feat: enforce hard test-tier requirement across all DevPlace workflow agents and feature-builder docs
DevPlace CI / test (push) Failing after 21m53s
DevPlace CI / test (push) Failing after 21m53s
Update the feature-builder agent prompt, test-maintainer agent, and all four workflow JS files (devii-tool, endpoint, feature, job-service) to codify the DevPlace test standard as a non-optional project requirement: one test file per endpoint, directory tree mirroring the URL/source path, split into three tiers (unit, api, e2e). Add explicit Test phases to devii-tool, endpoint, feature, and job-service workflows, and embed tier-specific test instructions (path mapping, fixture choice, coverage scope) directly in each workflow's meta description and TESTS constant.
This commit is contained in:
@@ -35,7 +35,7 @@ A DevPlace feature is one data source fanning out into several consumers, all fr
|
||||
6. **Agent + docs (the most-forgotten layers)** - if a user could ask Devii to do it, add an `Action` in `services/devii/actions/catalog.py` with auth flags matched to the route guard (and a declared `confirm` boolean for any irreversible action added to `CONFIRM_REQUIRED`). Add a `docs_api.py` `endpoint()` entry (params + `sample_response`) for every public/auth endpoint; add a prose page to `routers/docs/pages.py` `DOCS_PAGES` when warranted. State-changing actions need an audit event (`events.md` key, `category_for`, recorder call at the mutation point).
|
||||
7. **SEO** - public pages build `base_seo_context` and the right JSON-LD; add to `routers/seo.py` sitemap when indexable.
|
||||
8. **Docs of record** - update `README.md` (product-facing) and `AGENTS.md` (deep companion) for any new route/config/dependency/mechanic; update `CLAUDE.md` only when a NEW architectural rule or convention is introduced.
|
||||
9. **Tests** - add tests in the correct tier and path (`tests/{unit,api,e2e}/<endpoint>.py`, directory tree mirrors the URL/source path) following the required Playwright patterns. WRITE them; NEVER run them.
|
||||
9. **Tests (a hard project requirement, never optional)** - the DevPlace suite is one test file per endpoint, ~932 tests, split into three tiers with the directory tree mirroring the URL/source path. Every feature gets a test in EVERY tier it exercises: `tests/unit/` for a new data/query helper (pure in-process, `local_db` or no fixture, path mirrors the SOURCE module - `devplacepy/utils.py` -> `tests/unit/utils.py`); `tests/api/` for a new JSON or HTML route (HTTP integration against the live uvicorn subprocess via `app_server`/`seeded_db`, path mirrors the endpoint - `POST /auth/login` -> `tests/api/auth/login.py`); `tests/e2e/` for a new interactive UI flow (Playwright `page`/`alice`/`bob`, path mirrors the endpoint - `GET /admin/ai-usage` -> `tests/e2e/admin/aiusage.py`). A route or feature with no test in any tier is incomplete. Follow the required patterns (`wait_until="domcontentloaded"` on every `goto`/`wait_for_url`, scoped selectors, `try/finally` restore of any flipped global setting, the shared fixtures, `test_`-prefixed functions in non-prefixed files, born-live `deleted_at`/`deleted_by` on raw soft-delete inserts) and create any missing package directories (`__init__.py`). WRITE them; validate each by a clean import only; NEVER run them.
|
||||
|
||||
When a layer is intentionally absent (an internal route with no public docs, a route Devii must never call), say so explicitly in the plan with the rationale rather than fabricating the layer.
|
||||
|
||||
@@ -51,6 +51,12 @@ No comments or docstrings in source you author; full typing on every signature a
|
||||
## Validation (after implementing; never skip)
|
||||
Run `hawk .` and confirm zero errors. Confirm `python -c "from devplacepy.main import app"` imports clean. Grep your touched files for em-dashes and confirm none. Do NOT run the test suite. Then hand off: name which maintainer dimensions are most relevant to the change (e.g. fanout, security, dry, docs, seo, audit, frontend, style, test) so the fleet can verify it.
|
||||
|
||||
## Live verification of UI/API changes (mandatory for visual work)
|
||||
A structurally valid template can still render broken - `hawk` and the import check never open a browser. Per CLAUDE.md this project treats live verification as non-negotiable for any layout, styling, component, responsive, or backend change:
|
||||
- When your change touches `templates/` or `static/`, the rendered result MUST be visually verified: start the dev server (`make dev` in the background; confirm with `mole check http://localhost:10500`), capture each new/changed route with headless Playwright (`wait_until="domcontentloaded"`), run `falcon describe <png>`, and compare the description against the intended UI and the surrounding design system (tokens, spacing, responsiveness). Tear down any server you started.
|
||||
- When your change touches `routers/`, verify the endpoints over HTTP with a `hound` JSON spec against the live server.
|
||||
- The `/feature` workflow performs this live `Verify` phase for you automatically; when you are invoked standalone for UI/API work, perform it yourself before declaring the work complete, or explicitly state it is the caller's responsibility and name the routes to check.
|
||||
|
||||
## Output
|
||||
- In PLAN mode: a short situation summary of the area, then the ordered layer-by-layer plan (each layer: what file, what change, or "n/a - rationale"), then the list of maintainer dimensions that will need to verify it. End by asking for approval to implement.
|
||||
- In IMPLEMENT mode: a concise summary of what was built per layer (`file:line` references), the validation results (`hawk`, import, em-dash scan), and the recommended maintainer hand-off.
|
||||
|
||||
@@ -36,15 +36,23 @@ Default to **REPORT** mode: record coverage gaps and pattern violations, do NOT
|
||||
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.
|
||||
|
||||
## Your dimension
|
||||
Keep integration-test coverage in step with the routes and features, writing tests that follow the project's required patterns.
|
||||
Keep integration-test coverage in step with the routes and features. The DevPlace suite is a hard project standard, not a nicety: one test file per endpoint, ~932 tests, split into three tiers with the directory tree mirroring the URL/source path. A route or feature that exercises a tier with no test in it is a coverage gap.
|
||||
|
||||
DETECT: routes and features with no corresponding test under `tests/{api,e2e,unit}/<endpoint-path>.py` (per the directory-mirrors-path naming rule). The project prefers integration tests over unit tests and tests the interface and API.
|
||||
The three tiers and which one a change belongs to (decided by what it exercises, mirroring the existing files):
|
||||
- **`tests/unit/`** - pure in-process tests of library functions (`local_db` or no fixture); the path mirrors the SOURCE module (`devplacepy/utils.py` -> `tests/unit/utils.py`, `devplacepy/services/audit/store.py` -> `tests/unit/services/audit/store.py`). The right tier for a new data/query/serialization helper.
|
||||
- **`tests/api/`** - HTTP integration tests against the live uvicorn subprocess (`app_server`/`seeded_db`, `requests`/`httpx` vs `BASE_URL`, no browser); the path mirrors the endpoint (`POST /auth/login` -> `tests/api/auth/login.py`). The right tier for a JSON or HTML route, auth/role gating, and Devii actions.
|
||||
- **`tests/e2e/`** - Playwright browser tests (`page`/`alice`/`bob`); the path mirrors the endpoint (`GET /admin/ai-usage` -> `tests/e2e/admin/aiusage.py`). The right tier for an interactive UI flow. The project prefers the interface/API tiers over unit where either fits.
|
||||
|
||||
FIX: write the missing integration test following the required patterns: every `page.goto` and `page.wait_for_url` passes `wait_until="domcontentloaded"`; selectors are scoped; a test that flips a global `site_settings` value restores it in `try/finally`; the shared fixtures (`alice`, `bob`, `app_server`) are used; test functions are `test_`-prefixed though files are not.
|
||||
A feature that adds a data helper AND a JSON route AND a UI flow needs a test in all three tiers. Choose the tier(s) by what the change actually touches; never leave a new route or helper untested.
|
||||
|
||||
DETECT: routes, features, data helpers, and Devii actions with no corresponding test in the tier(s) they exercise under `tests/{unit,api,e2e}/<path>.py` (per the directory-mirrors-path naming rule). A collection path that also parents deeper paths uses `index.py` in its own directory.
|
||||
|
||||
FIX: write the missing test in the correct tier, creating any missing package directories (`__init__.py`), following the required patterns: every `page.goto` and `page.wait_for_url` passes `wait_until="domcontentloaded"`; selectors are scoped; a test that flips a global `site_settings` value restores it in `try/finally`; the shared fixtures (`alice`, `bob`, `app_server`, `seeded_db`) are used; test functions are `test_`-prefixed though files are not; a raw insert into a `SOFT_DELETE_TABLES` table sets `deleted_at`/`deleted_by`; a test that mutates a cross-process cached value (settings/roles) polls the endpoint rather than asserting immediately.
|
||||
|
||||
## Scope units
|
||||
- **coverage-gaps**: `routers/*.py` routes with no referencing test under `tests/{api,e2e,unit}/`.
|
||||
- **pattern-lint**: `tests/*.py` use `domcontentloaded`, scoped selectors, try/finally global restore, shared fixtures.
|
||||
- **coverage-gaps**: `routers/*.py` routes, `database.py`/service data helpers, and `services/devii/actions/catalog.py` actions with no referencing test in the tier(s) they exercise under `tests/{unit,api,e2e}/`.
|
||||
- **tier-fit**: a feature exercising a tier (a UI flow with only an api test, a data helper with no unit test) where that tier's test is missing.
|
||||
- **pattern-lint**: `tests/*.py` use `domcontentloaded`, scoped selectors, try/finally global restore, shared fixtures, born-live soft-delete inserts.
|
||||
|
||||
## Output
|
||||
Return a markdown report: a one-line summary line, then one bullet per finding with severity (`error`/`warning`/`info`), `file:line`, the rule name, the message, and (in fix mode) whether the test was written.
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
export const meta = {
|
||||
name: 'devii-tool',
|
||||
description: 'Add a Devii agent capability: an Action in the catalog with auth flags matched to the route guard, dispatcher wiring, API docs, and a test, then verify role-gating and confirmation',
|
||||
description: 'Add a Devii agent capability: an Action in the catalog with auth flags matched to the route guard, dispatcher wiring, API docs, then verify role-gating and confirmation and write the api-tier integration test for the action',
|
||||
phases: [
|
||||
{ title: 'Understand', detail: 'find the underlying route and a similar Action to mirror' },
|
||||
{ title: 'Implement', detail: 'add the Action, wire the handler, document it' },
|
||||
{ title: 'Verify', detail: 'role-gating, flag alignment, and confirm gating' },
|
||||
{ title: 'Fix', detail: 'close gaps and write the action test' },
|
||||
{ title: 'Fix', detail: 'close gaps from the review' },
|
||||
{ title: 'Test', detail: 'write the api-tier integration test for the action (visibility, auth gating, confirm)' },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -20,6 +21,13 @@ const RULES = [
|
||||
'- Validate with "hawk ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.',
|
||||
].join('\n')
|
||||
|
||||
const TESTS = [
|
||||
'DevPlace test standard (a hard project requirement - one test file per endpoint, the directory tree mirroring the URL path):',
|
||||
'A Devii tool is reached over the same HTTP surface a user hits, so its test lives in tests/api/ (often tests/api/devii/), against the live uvicorn subprocess (app_server/seeded_db, requests/httpx vs BASE_URL).',
|
||||
'Cover the role-gating that is the whole point of the tool: an unauthenticated/guest caller is refused, a member sees and can call a requires_auth tool but is refused a requires_admin one (and its schema is withheld), an admin can call it, and a destructive action is refused without confirm=true and proceeds with it.',
|
||||
'Required patterns: scoped assertions; try/finally restore of any flipped global setting; the shared fixtures (alice, bob, app_server); test FUNCTIONS are test_-prefixed though files are not. Validate by a clean import only. NEVER run the suite.',
|
||||
].join('\n')
|
||||
|
||||
function toolBrief() {
|
||||
if (!args) return ''
|
||||
if (typeof args === 'string') return args
|
||||
@@ -125,8 +133,8 @@ if (gaps.length) {
|
||||
}
|
||||
|
||||
const test = await agent(
|
||||
`Operate in FIX mode. Write the integration test for this Devii tool following the required patterns (tests/api/devii layout). Validate by a clean import only. NEVER run the suite.\n\nTool request: ${ask}\nAction: ${build && build.actionName}\nFiles changed:\n${changed.join('\n')}`,
|
||||
{ agentType: 'test-maintainer', label: 'test', phase: 'Fix' }
|
||||
`Operate in FIX mode. Write the integration test for this Devii tool following the required patterns (tests/api/devii layout), asserting the role-gating and confirm behavior described below. The tool is not complete until its gating is tested. Create any missing package directories the test path needs. Validate by a clean import only. NEVER run the suite.\n\n${TESTS}\n\nTool request: ${ask}\nAction: ${build && build.actionName}\nFiles changed:\n${changed.join('\n')}\n\nReturn the test file written and the gating cases it covers.`,
|
||||
{ agentType: 'test-maintainer', label: 'test', phase: 'Test' }
|
||||
)
|
||||
|
||||
return { ask, map, build, audit: gaps, gapFix, test }
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
export const meta = {
|
||||
name: 'endpoint',
|
||||
description: 'Scaffold ONE new DevPlace route across all of its touchpoints (Form model, Out schema, guarded handler with respond, main.py mount, template, Devii action, API docs, SEO, test) and verify it',
|
||||
description: 'Scaffold ONE new DevPlace route across all of its touchpoints (Form model, Out schema, guarded handler with respond, main.py mount, template, Devii action, API docs, SEO) and verify it, then write its integration test in the matching tier (api for JSON/HTML, e2e for an interactive UI flow)',
|
||||
phases: [
|
||||
{ title: 'Understand', detail: 'find the closest existing route to mirror' },
|
||||
{ title: 'Implement', detail: 'wire the route across every touchpoint' },
|
||||
{ title: 'Verify', detail: 'completeness and security review of the new route' },
|
||||
{ title: 'Fix', detail: 'close gaps and write the route test' },
|
||||
{ title: 'Fix', detail: 'close gaps from the review' },
|
||||
{ title: 'Test', detail: 'write the route test in the matching tier (api or e2e), mirroring the path' },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -31,6 +32,14 @@ const TOUCHPOINTS = [
|
||||
'8. seo.py - base_seo_context for a public page; sitemap entry if indexable.',
|
||||
].join('\n')
|
||||
|
||||
const TESTS = [
|
||||
'DevPlace test standard (a hard project requirement - one test file per endpoint, the directory tree mirroring the URL path):',
|
||||
'- tests/api/ - HTTP integration test against the live uvicorn subprocess (app_server/seeded_db, requests/httpx vs BASE_URL) - the right tier for a JSON or HTML route.',
|
||||
'- tests/e2e/ - Playwright browser test (page/alice/bob) - the right tier for an interactive UI flow.',
|
||||
'The route path maps to the test path by dropping {param} segments and lowercasing each segment (POST /auth/login -> tests/api/auth/login.py; GET /admin/ai-usage -> tests/e2e/admin/aiusage.py). A collection path that also parents deeper paths uses index.py in its own directory. Create any missing package directories with __init__.py.',
|
||||
'Required patterns: wait_until="domcontentloaded" on every goto/wait_for_url; scoped selectors; try/finally restore of any flipped global setting; the shared fixtures; test FUNCTIONS are test_-prefixed though files are not. Validate by a clean import only. NEVER run the suite.',
|
||||
].join('\n')
|
||||
|
||||
function endpointBrief() {
|
||||
if (!args) return ''
|
||||
if (typeof args === 'string') return args
|
||||
@@ -132,8 +141,8 @@ if (gaps.length) {
|
||||
}
|
||||
|
||||
const test = await agent(
|
||||
`Operate in FIX mode. Write the integration test for this new route following the required patterns and the directory-mirrors-path layout. Validate by a clean import only. NEVER run the suite.\n\nEndpoint: ${ask}\nFiles changed:\n${changed.join('\n')}`,
|
||||
{ agentType: 'test-maintainer', label: 'test', phase: 'Fix' }
|
||||
`Operate in FIX mode. Write the integration test for this new route in the matching tier (api for a JSON/HTML route, e2e for an interactive UI flow) following the required patterns and the directory-mirrors-path layout. The route is not complete until it has a test. Create any missing package directories the test path needs. Validate by a clean import only. NEVER run the suite.\n\n${TESTS}\n\nEndpoint: ${ask}\nFiles changed:\n${changed.join('\n')}\n\nReturn the test file written and its tier.`,
|
||||
{ agentType: 'test-maintainer', label: 'test', phase: 'Test' }
|
||||
)
|
||||
|
||||
return { ask, map, build, audit: gaps, gapFix, test }
|
||||
|
||||
+148
-26
@@ -1,13 +1,15 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
export const meta = {
|
||||
name: 'feature',
|
||||
description: 'Add a feature across the full DevPlace fan-out: understand the area, plan the layers, implement coherently in the repo, audit completeness and security, fix gaps and write tests, then verify',
|
||||
description: 'Add a feature across the full DevPlace fan-out: understand the area, plan the layers, build via the feature-builder agent, audit every quality dimension with adversarial verification, verify live in the browser and over HTTP, close gaps, then write the integration tests across every applicable tier (unit, api, e2e)',
|
||||
phases: [
|
||||
{ title: 'Understand', detail: 'map the target area and a similar existing feature' },
|
||||
{ title: 'Plan', detail: 'a per-layer implementation plan across the nine touchpoints' },
|
||||
{ title: 'Implement', detail: 'build all layers coherently in the repo' },
|
||||
{ title: 'Audit', detail: 'completeness and security review of the changed files' },
|
||||
{ title: 'Fix', detail: 'close audit gaps and write missing integration tests' },
|
||||
{ title: 'Implement', detail: 'build all layers coherently via the feature-builder agent' },
|
||||
{ title: 'Audit', detail: 'every relevant quality dimension, each finding adversarially verified against source' },
|
||||
{ title: 'Verify', detail: 'live dev-server visual (falcon) and API (hound) verification of the change' },
|
||||
{ title: 'Fix', detail: 'close confirmed gaps from the audit and live verification' },
|
||||
{ title: 'Test', detail: 'write integration tests across every applicable tier (unit, api, e2e), one file per endpoint mirroring the path' },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -17,10 +19,10 @@ const RULES = [
|
||||
'- First line of any NEW file is the header: Python "# retoor <retoor@molodetz.nl>", JS "// retoor <retoor@molodetz.nl>", CSS "/* retoor <retoor@molodetz.nl> */".',
|
||||
'- No em-dash characters; use a hyphen. Source is English only.',
|
||||
'- Full type hints on Python signatures and variables; pathlib over os; Pydantic Form input with explicit max lengths; sanitize and bound all user input.',
|
||||
'- Reuse shared helpers: templating.templates (never a per-router Jinja2Templates), database.py batch helpers (no inline N+1), the respond() negotiator, _avatar_link.html / _user_link.html, and on the frontend Http / Poller / JobPoller / OptimisticAction / FloatingWindow.',
|
||||
'- Reuse shared helpers: templating.templates (never a per-router Jinja2Templates), database.py batch helpers (no inline N+1), the respond() negotiator, _avatar_link.html / _user_link.html, and on the frontend Http / Poller / JobPoller / OptimisticAction / FloatingWindow and the dp-* components.',
|
||||
'- Auth guards: get_current_user (public read), require_user (member write), require_admin (admin). Every POST/PUT/DELETE is guarded; deletes are soft and owner-or-admin.',
|
||||
'- Never pass a respond() context key that collides with a Jinja global (use viewer_is_admin, not is_admin). Dates are DD/MM/YYYY via format_date.',
|
||||
'- Validate with "hawk ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.',
|
||||
'- Validate with "hawk ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the pytest suite. Never perform any git write.',
|
||||
].join('\n')
|
||||
|
||||
const FANOUT = [
|
||||
@@ -36,6 +38,16 @@ const FANOUT = [
|
||||
'9. README.md (product) + AGENTS.md (mechanics) + CLAUDE.md (only for a genuinely new architectural rule).',
|
||||
].join('\n')
|
||||
|
||||
const TESTS = [
|
||||
'DevPlace test standard (a hard project requirement, NOT optional - the suite is one test file per endpoint, ~932 tests, with the directory tree mirroring the URL/source path):',
|
||||
'- tests/unit/ - pure in-process tests of library functions (local_db or no fixture); the path mirrors the SOURCE module (devplacepy.utils -> tests/unit/utils.py).',
|
||||
'- tests/api/ - HTTP integration tests against the live uvicorn subprocess (app_server/seeded_db, requests/httpx vs BASE_URL, no browser); the path mirrors the endpoint (POST /auth/login -> tests/api/auth/login.py).',
|
||||
'- tests/e2e/ - Playwright browser tests (page/alice/bob); the path mirrors the endpoint (GET /admin/ai-usage -> tests/e2e/admin/aiusage.py).',
|
||||
'A feature MUST get every tier it exercises: a new data/query helper -> a unit test; a new JSON or HTML route -> an api test; a new interactive UI flow -> an e2e test. Pick tiers by what the change actually touches; never ship a route or feature with no test in any tier.',
|
||||
'Required patterns: every page.goto/page.wait_for_url passes wait_until="domcontentloaded"; selectors are scoped; a test that flips a global site_settings value restores it in try/finally; reuse the shared fixtures (alice, bob, app_server, seeded_db); test FUNCTIONS are test_-prefixed though files are not; raw inserts into a soft-delete table set deleted_at/deleted_by.',
|
||||
'Validate each new test module by a clean import only (python -c "import ..." or python -m py_compile). NEVER run the suite, not the full suite and not one file - that is the human-only /test path.',
|
||||
].join('\n')
|
||||
|
||||
function featureBrief() {
|
||||
if (!args) return ''
|
||||
if (typeof args === 'string') return args
|
||||
@@ -81,6 +93,7 @@ const PLAN_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
routes: { type: 'array', items: { type: 'string' } },
|
||||
outOfScope: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
}
|
||||
@@ -94,6 +107,7 @@ const BUILD_SCHEMA = {
|
||||
filesChanged: { type: 'array', items: { type: 'string' } },
|
||||
validatorPassed: { type: 'boolean' },
|
||||
importOk: { type: 'boolean' },
|
||||
routes: { type: 'array', items: { type: 'string' } },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
}
|
||||
@@ -122,6 +136,42 @@ const FINDINGS_SCHEMA = {
|
||||
},
|
||||
}
|
||||
|
||||
const VERDICT_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['isReal', 'reason'],
|
||||
properties: {
|
||||
isReal: { type: 'boolean' },
|
||||
reason: { type: 'string' },
|
||||
severity: { type: 'string', enum: ['error', 'warning', 'info'] },
|
||||
},
|
||||
}
|
||||
|
||||
const LIVE_SCHEMA = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['ran', 'summary'],
|
||||
properties: {
|
||||
ran: { type: 'boolean' },
|
||||
summary: { type: 'string' },
|
||||
pagesChecked: { type: 'array', items: { type: 'string' } },
|
||||
apiChecked: { type: 'array', items: { type: 'string' } },
|
||||
issues: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['severity', 'where', 'message'],
|
||||
properties: {
|
||||
severity: { type: 'string', enum: ['error', 'warning', 'info'] },
|
||||
where: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
log(`Feature: ${ask}`)
|
||||
|
||||
const map = await agent(
|
||||
@@ -130,16 +180,17 @@ const map = await agent(
|
||||
)
|
||||
|
||||
const plan = await agent(
|
||||
`Produce a precise, per-layer implementation plan for this DevPlace feature. One step per file with the exact touchpoint to add or change. Mark layers that are intentionally not needed as outOfScope with a reason. Do not write code.\n\nFeature request: ${ask}\n\nArea map:\n${JSON.stringify(map, null, 2)}\n\n${FANOUT}`,
|
||||
`Produce a precise, per-layer implementation plan for this DevPlace feature. One step per file with the exact touchpoint to add or change. List the user-facing routes (URL paths) the feature adds or changes in "routes". Mark layers that are intentionally not needed as outOfScope with a reason. Do not write code.\n\nFeature request: ${ask}\n\nArea map:\n${JSON.stringify(map, null, 2)}\n\n${FANOUT}`,
|
||||
{ agentType: 'Plan', label: 'plan', phase: 'Plan', schema: PLAN_SCHEMA }
|
||||
)
|
||||
|
||||
const build = await agent(
|
||||
`Implement this DevPlace feature coherently and completely, editing files directly in the repo. Follow the plan; keep every layer in agreement (the *Out schema must carry every JSON key the handler returns; the Devii action auth flags must match the route guard). When done, run "hawk ." and "python -c \\"from devplacepy.main import app\\"" and report whether each passed. Do not write tests in this step. Do not run the test suite. Do not commit.\n\nFeature request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${FANOUT}\n\n${RULES}\n\nReturn the list of files you changed or created, whether the validator and the import passed, and a short summary.`,
|
||||
{ label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
|
||||
`Implement directly - no plan, no approval needed, this is implement mode. Build this DevPlace feature coherently and completely, editing files in the repo, following the plan. Keep every layer in agreement (the *Out schema must carry every JSON key the handler returns; the Devii action auth flags must match the route guard; a respond() context key must never shadow a Jinja global). Do NOT write pytest tests in this step (a later phase owns that). When done, run "hawk ." and "python -c \\"from devplacepy.main import app\\"" and report whether each passed, and list the user-facing routes the feature exposes.\n\nFeature request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${FANOUT}\n\n${RULES}\n\nReturn the list of files you changed or created, whether the validator and the import passed, the routes, and a short summary.`,
|
||||
{ agentType: 'feature-builder', label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
|
||||
)
|
||||
|
||||
const changed = (build && build.filesChanged) || []
|
||||
const routes = (build && build.routes && build.routes.length ? build.routes : (plan && plan.routes) || [])
|
||||
const scopeNote = changed.length
|
||||
? `\n\nRestrict your findings to these changed files (read others only for cross-reference):\n${changed.join('\n')}`
|
||||
: ''
|
||||
@@ -147,36 +198,107 @@ const scopeNote = changed.length
|
||||
const AUDITORS = [
|
||||
{ key: 'fanout', agent: 'fanout-maintainer' },
|
||||
{ key: 'security', agent: 'security-maintainer' },
|
||||
{ key: 'style', agent: 'style-maintainer' },
|
||||
{ key: 'dry', agent: 'dry-maintainer' },
|
||||
{ key: 'frontend', agent: 'frontend-maintainer' },
|
||||
{ key: 'seo', agent: 'seo-maintainer' },
|
||||
{ key: 'audit', agent: 'audit-maintainer' },
|
||||
{ key: 'devii', agent: 'devii-maintainer' },
|
||||
{ key: 'docs', agent: 'docs-maintainer' },
|
||||
]
|
||||
|
||||
const audits = await parallel(
|
||||
AUDITORS.map((a) => () =>
|
||||
agent(
|
||||
`Operate in REPORT mode (read-only). Audit the just-implemented feature for your single dimension. Confirm each finding against the source.${scopeNote}\n\nFeature request: ${ask}`,
|
||||
{ agentType: a.agent, label: `audit:${a.key}`, phase: 'Audit', schema: FINDINGS_SCHEMA }
|
||||
).then((r) => ({ key: a.key, findings: (r && r.findings) || [] }))
|
||||
function verifyPrompt(dimension, finding) {
|
||||
return (
|
||||
`Adversarially verify a candidate "${dimension}" finding against the just-built feature. Your goal is to REFUTE it. ` +
|
||||
`Open the exact file and read enough surrounding context (the whole function, the caller, the contract) to judge intent. ` +
|
||||
`It is REAL only if it survives refutation as a genuine violation of the ${dimension} dimension introduced by this change. ` +
|
||||
`Rule it out (isReal=false) if it is a contract identifier, DATA rather than authored prose, generated/vendored/third-party, ` +
|
||||
`pre-existing and untouched by this feature, or already correct under a known exemption. When uncertain, default to isReal=false.\n\n` +
|
||||
`Candidate finding:\n- file: ${finding.file}\n- line: ${finding.line == null ? 'unspecified' : finding.line}\n` +
|
||||
`- severity: ${finding.severity}\n- rule: ${finding.rule}\n- message: ${finding.message}\n\nReturn isReal and a one-line reason.`
|
||||
)
|
||||
}
|
||||
|
||||
const reviewed = await pipeline(
|
||||
AUDITORS,
|
||||
(auditor) =>
|
||||
agent(
|
||||
`Operate in REPORT mode (read-only). Do not modify any file. Audit the just-implemented feature for your single quality dimension, following your mandate and accuracy doctrine. Confirm each candidate against the actual source before recording it.${scopeNote}\n\nFeature request: ${ask}`,
|
||||
{ agentType: auditor.agent, label: `audit:${auditor.key}`, phase: 'Audit', schema: FINDINGS_SCHEMA }
|
||||
),
|
||||
(review, auditor) =>
|
||||
parallel(
|
||||
((review && review.findings) || []).map((finding) => () =>
|
||||
agent(verifyPrompt(auditor.key, finding), {
|
||||
agentType: auditor.agent,
|
||||
label: `verify:${auditor.key}`,
|
||||
phase: 'Audit',
|
||||
schema: VERDICT_SCHEMA,
|
||||
}).then((verdict) => ({ ...finding, dimension: auditor.key, verdict }))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const gaps = audits
|
||||
.filter(Boolean)
|
||||
.flatMap((r) => r.findings.map((f) => ({ dimension: r.key, ...f })))
|
||||
.filter((f) => f.severity !== 'info')
|
||||
const auditCandidates = reviewed.flat().filter(Boolean)
|
||||
const auditConfirmed = auditCandidates.filter((f) => f.verdict && f.verdict.isReal)
|
||||
log(`Audit: ${auditConfirmed.length} confirmed of ${auditCandidates.length} candidate finding(s) across ${AUDITORS.length} dimensions`)
|
||||
|
||||
let gapFix = 'no actionable gaps from the audit'
|
||||
const touchedFrontend = changed.some((f) => f.includes('/templates/') || f.includes('/static/'))
|
||||
const touchedApi = changed.some((f) => f.includes('/routers/'))
|
||||
|
||||
let live = { ran: false, summary: 'no frontend or API files changed; live verification skipped', issues: [] }
|
||||
if (touchedFrontend || touchedApi) {
|
||||
const kinds = [touchedFrontend ? 'visual (falcon)' : null, touchedApi ? 'API (hound)' : null].filter(Boolean).join(' and ')
|
||||
live = await agent(
|
||||
`Operate the MANDATORY DevPlace live verification (${kinds}) for the just-built feature, exactly per CLAUDE.md.\n\n` +
|
||||
`Procedure:\n` +
|
||||
`1. Check if the dev server already answers: "mole check http://localhost:10500". If it does NOT, start it yourself with "make dev" as a BACKGROUND process, then poll "mole check http://localhost:10500" until healthy (give uvicorn a few seconds to boot). Remember whether YOU started it.\n` +
|
||||
(touchedFrontend
|
||||
? `2. VISUAL: for each user-facing route the feature adds or changes, capture a screenshot with the installed Playwright (chromium, headless) navigating to "http://localhost:10500<route>" with wait_until="domcontentloaded", saving a PNG under /tmp/, then run "falcon describe <png>". Compare each AI description against the intended UI and the surrounding design system (layout, spacing, design tokens, responsiveness). Record any mismatch, broken layout, missing element, or visual regression as an issue. Authenticated routes: log in via the /auth/login form first (seeded users may not exist on a fresh dev DB - if a route needs auth and you cannot reach it, record that as an info issue rather than failing).\n`
|
||||
: '') +
|
||||
(touchedApi
|
||||
? `3. API: write a hound JSON spec (tests: name/method/path/expect_status[/expect_body/expect_headers]) covering the feature's endpoints with realistic expected statuses, then run "hound <spec>.json --base-url http://localhost:10500". Record every failing assertion as an issue.\n`
|
||||
: '') +
|
||||
`4. TEARDOWN: if YOU started the server, kill it now (do not leave a stray uvicorn running). If it was already running, leave it.\n\n` +
|
||||
`Routes for this feature: ${routes.length ? routes.join(', ') : '(infer from the changed routers/templates below)'}\n` +
|
||||
`Changed files:\n${changed.join('\n')}\n\n` +
|
||||
`Return ran=true, the pages and api endpoints you checked, and one issue per real visual/functional defect (severity/where/message). Do not edit feature source in this phase; only report.`,
|
||||
{ label: 'live-verify', phase: 'Verify', schema: LIVE_SCHEMA }
|
||||
)
|
||||
log(`Live verify: ${(live && live.issues && live.issues.length) || 0} issue(s) over ${((live && live.pagesChecked) || []).length} page(s)`)
|
||||
}
|
||||
|
||||
const gaps = []
|
||||
for (const f of auditConfirmed) {
|
||||
if (f.severity !== 'info') gaps.push({ source: f.dimension, file: f.file, line: f.line, rule: f.rule, message: f.message })
|
||||
}
|
||||
for (const i of (live && live.issues) || []) {
|
||||
if (i.severity !== 'info') gaps.push({ source: 'live-verify', file: i.where, rule: 'live', message: i.message })
|
||||
}
|
||||
|
||||
let gapFix = 'no actionable gaps from the audit or live verification'
|
||||
if (gaps.length) {
|
||||
gapFix = await agent(
|
||||
`Close these completeness and security gaps found by the audit of the new feature. Apply minimal root-cause fixes directly in the repo, keeping all layers in agreement. Re-run "hawk ." afterward. Do not run the test suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
|
||||
{ label: 'fix-gaps', phase: 'Fix' }
|
||||
`Close these confirmed completeness, security, style, frontend, and live-rendering gaps found in the new feature. Apply minimal root-cause fixes directly in the repo, keeping all layers in agreement and the styling consistent with the design system. Re-run "hawk ." afterward. Do not run the pytest suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
|
||||
{ agentType: 'feature-builder', label: 'fix-gaps', phase: 'Fix' }
|
||||
)
|
||||
}
|
||||
|
||||
const tests = await agent(
|
||||
`Operate in FIX mode. Write the missing integration tests for this new feature following the required Playwright patterns and the directory-mirrors-path layout. Validate each new test module by a clean import only. NEVER run the suite, not the full suite and not one file.\n\nFeature request: ${ask}\nFiles changed:\n${changed.join('\n')}`,
|
||||
{ agentType: 'test-maintainer', label: 'tests', phase: 'Fix' }
|
||||
`Operate in FIX mode. Write the missing integration tests for this new feature across EVERY tier it exercises, per the DevPlace test standard below. This is mandatory, not a nicety: the feature is incomplete until each route and helper it adds has a test in the appropriate tier (unit for new data/query helpers, api for new JSON/HTML routes, e2e for new interactive UI flows), in the correct file under the directory-mirrors-path layout. Decide the tiers from the changed files and routes; create the package directories (with __init__.py) the new test paths require. Validate each new test module by a clean import only. NEVER run the suite, not the full suite and not one file.\n\n${TESTS}\n\nFeature request: ${ask}\nRoutes: ${routes.join(', ')}\nFiles changed:\n${changed.join('\n')}\n\nReturn the test files you wrote, the tier of each, and which routes/helpers remain uncovered (with the reason).`,
|
||||
{ agentType: 'test-maintainer', label: 'tests', phase: 'Test' }
|
||||
)
|
||||
|
||||
log(`Feature build complete: ${changed.length} file(s), ${gaps.length} audit gap(s) addressed`)
|
||||
log(`Feature build complete: ${changed.length} file(s), ${gaps.length} gap(s) addressed`)
|
||||
|
||||
return { ask, map, plan, build, audit: gaps, gapFix, tests }
|
||||
return {
|
||||
ask,
|
||||
map,
|
||||
plan,
|
||||
build,
|
||||
routes,
|
||||
audit: { candidates: auditCandidates.length, confirmed: auditConfirmed, gaps },
|
||||
liveVerify: live,
|
||||
gapFix,
|
||||
tests,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
export const meta = {
|
||||
name: 'job-service',
|
||||
description: 'Scaffold an async JobService (the zip/fork pattern): the JobService subclass, enqueue/status/download routes, the JobOut schema, main.py registration, Devii tools, JobPoller frontend, and docs, then verify',
|
||||
description: 'Scaffold an async JobService (the zip/fork pattern): the JobService subclass, enqueue/status/download routes, the JobOut schema, main.py registration, Devii tools, JobPoller frontend, and docs, then verify and write the integration tests (enqueue, status, download) in the api tier',
|
||||
phases: [
|
||||
{ title: 'Understand', detail: 'read ZipService and ForkService as the template' },
|
||||
{ title: 'Plan', detail: 'a per-touchpoint plan for the new job kind' },
|
||||
{ title: 'Implement', detail: 'build the service and all consumers in the repo' },
|
||||
{ title: 'Verify', detail: 'completeness, security, and audit-log review' },
|
||||
{ title: 'Fix', detail: 'close gaps and write the job tests' },
|
||||
{ title: 'Fix', detail: 'close gaps from the review' },
|
||||
{ title: 'Test', detail: 'write the api-tier integration tests for enqueue, status, and download' },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -34,6 +35,13 @@ const CHECKLIST = [
|
||||
'9. README.md + AGENTS.md - document the new job kind.',
|
||||
].join('\n')
|
||||
|
||||
const TESTS = [
|
||||
'DevPlace test standard (a hard project requirement - one test file per endpoint, the directory tree mirroring the URL path):',
|
||||
'A job kind is exercised over HTTP, so its tests live in tests/api/ against the live uvicorn subprocess (app_server/seeded_db, requests/httpx vs BASE_URL), one file per route path (POST /projects/{slug}/zip -> tests/api/projects/zip.py; GET /zips/{uid} -> tests/api/zips/index.py). Cover enqueue (authz + a job uid back), status (the *JobOut shape and lifecycle), and download/result (the capability URL) where applicable.',
|
||||
'Because the service loop only runs in the lock owner and tests set DEVPLACE_DISABLE_SERVICES=1, assert the enqueue contract and the pending/known status shape rather than waiting on real completion; if you need a finished job, drive process() directly in a unit test under tests/unit/services/jobs/.',
|
||||
'Required patterns: scoped assertions; try/finally restore of any flipped global setting; the shared fixtures; raw inserts into a soft-delete table set deleted_at/deleted_by. Validate by a clean import only. NEVER run the suite.',
|
||||
].join('\n')
|
||||
|
||||
function jobBrief() {
|
||||
if (!args) return ''
|
||||
if (typeof args === 'string') return args
|
||||
@@ -160,8 +168,8 @@ if (gaps.length) {
|
||||
}
|
||||
|
||||
const tests = await agent(
|
||||
`Operate in FIX mode. Write the integration tests for the new job kind (enqueue, status, download) following the required patterns and the directory-mirrors-path layout. Validate by a clean import only. NEVER run the suite.\n\nJob request: ${ask}\nKind: ${build && build.kind}\nFiles changed:\n${changed.join('\n')}`,
|
||||
{ agentType: 'test-maintainer', label: 'tests', phase: 'Fix' }
|
||||
`Operate in FIX mode. Write the integration tests for the new job kind (enqueue, status, download) following the required patterns and the directory-mirrors-path layout. The job kind is not complete until each of its routes has a test. Create any missing package directories the test paths need. Validate by a clean import only. NEVER run the suite.\n\n${TESTS}\n\nJob request: ${ask}\nKind: ${build && build.kind}\nFiles changed:\n${changed.join('\n')}\n\nReturn the test files written and the routes they cover.`,
|
||||
{ agentType: 'test-maintainer', label: 'tests', phase: 'Test' }
|
||||
)
|
||||
|
||||
return { ask, map, plan, build, audit: gaps, gapFix, tests }
|
||||
|
||||
Reference in New Issue
Block a user