Organizing cases
The tree of suites follows one rule, and it is the rule your repository already follows: a folder holds folders and suites, a suite holds test cases. Nothing holds both. The rule is enforced on every write, not suggested by a screen — a case cannot be created in a folder, a suite cannot be nested in a suite, and a node cannot change kind while it holds what the other kind cannot.
Folders and suites
Section titled “Folders and suites”Every node in GET /v1/suites carries a kind, a position among its siblings, an optional
description, and caseCount — the number of cases in it, or, for a folder, beneath it. The tree
comes back in tree order: siblings by position, the order a person set with ↑ / ↓ on the
Suites screen, not the alphabet.
{ "id": "6f1c…f1", "name": "checkout", "kind": "folder", "position": 0, "description": "Everything past the cart", "caseCount": 41, "children": [ { "id": "…", "name": "payment.spec.ts", "kind": "suite", "position": 0, "caseCount": 41, "children": [] } ]}A case that belongs to no suite sits at the project root. That is a normal state — a manual case written before any suite existed, or a case you unfiled on purpose — and the dashboard shows those cases as their own group, last, headed “Project root”.
Each of the six ways to put something somewhere refuses the wrong place with the same words:
| You try | Answer |
|---|---|
| create or move a case into a folder | 400 · suite '<id>' is a folder — cases go in suites |
| create or move a suite under a suite | 400 · parent '<id>' is a suite — suites go in folders |
| turn a suite that still holds cases into a folder | 409 · suite '<id>' still holds test cases — POST /v1/suites/<id>/convert-to-folder moves them into a child suite |
| turn a folder that still holds suites into a suite | 409 · folder '<id>' still holds child suites — move or delete them first |
The 409 answers name the way out, which for a suite with cases is the next section.
Converting a suite into a folder
Section titled “Converting a suite into a folder”A suite that has grown a second level cannot simply become a folder — its cases have to go somewhere.
POST /v1/suites/{id}/convert-to-folder does the whole move in one transaction:
- the node keeps its
idand its place and becomes a folder; - a child suite of the same name appears inside it at
position: 0, and every case moves there — and so does the suite’sfile, if the suite was made by filing (below), so later imports keep landing in it; - both steps are written to the audit log under one session, so the restore of one is the restore of both.
The answer is both nodes:
{ "folder": { "id": "6f1c…f1", "kind": "folder", "…": "…" }, "child": { "id": "…", "name": "checkout", "kind": "suite", "position": 0, "…": "…" } }Converting a folder answers 409 · suite '<id>' is already a folder. On the dashboard the same
action sits in the node’s ⋯ menu and asks first, showing what the node is and what it becomes.
Where an automated case lands
Section titled “Where an automated case lands”An automated case knows where its test lives: its specRef is a path such as
apps/web/e2e/catalog/search.spec.ts:9. Plune reads that path and files the case by it, on every
road a case can arrive by — POST /v1/test-cases, an approval in the review queue, a trusted source:
- each directory becomes a folder —
apps›web›e2e›catalog; - the file becomes a suite, named by its basename with the extension —
search.spec.ts; - the
:lineor#anchortail is not part of the place.
Folders and suites are found or created as needed, and a suite made this way remembers its file, so
renaming the suite on the dashboard does not stop the next import from finding it. Send a suiteId
yourself and the case goes where you said instead; a manual case with no suiteId stays at the root.
Filing what is already there
Section titled “Filing what is already there”Cases that existed before this rule did — imported earlier, approved earlier — may still sit at the
root with a specRef that would place them. POST /v1/admin/file-unfiled walks every project and
files each such case through the same seam, reporting what it did per project:
{ "projects": [ { "projectId": "…", "filed": 118, "skipped": [{ "caseId": "…", "reason": "filing skipped: 'e2e' exists as a suite, expected a folder" }] } ]}It is idempotent — a second run reports filed: 0 everywhere and the same skipped — and it is an
operator’s call: the route answers only the owner of the deployment, and 404 to everyone else.