Adapting Legacy Cases

Many TestRail cases still describe the legacy CPE Admin portal. The Adaptation tab rewrites them for the Angular Management Portal and checks every rewritten case. An administrator writes a passed case into its source case in TestRail project 15. See Writing In Place.

At a glance
  • Status: every case of the source suite has one status. The Status panel counts them. See Statuses.

  • Adapt: pick a Source section or type Case IDs, set a Limit, press Start. One adaptation job runs at a time.

  • Refresh: after a worker deploy, press Refresh to current version. No model call is made. See Refresh to the Current Version.

  • Review: open a case in Cases. Read the findings, edit the adapted JSON and press Re-gate.

  • Act in bulk: select cases and press an action of the bulk bar. See Bulk Actions.

  • Write: an administrator writes ready cases into TestRail. See Writing In Place.

  • Measure: Pilot report gives pass rates, tokens and hours per engine.

  • Then: import the written cases like any other. See After Writing.

Why

TestRail project Angular Management Portal (id 15) was filled by copying the cases of the CPE Admin project. The portal name in the title and in the login step was replaced, but most step text still describes the legacy UI — Go to "Device Update" tab, Select "LWM2M" from the left menu. The converter grounds a navigation step only when it follows the grammar in Writing TestRail Cases; every legacy phrasing it cannot match falls through to a slower, less reliable LLM interact step.

The adaptation changes UI wording only. Expected results, numbers, IP addresses, TR-181 paths and quoted values are carried over verbatim, a case keeps the template and the number of scenarios of its source. The result is written into the source case. While ADAPT_COPIES_RETIRED is set, the tab never creates, deletes, moves or copies a case in TestRail. A write into a source case never does it either.

Writing In Place

The adapted text goes into the project 15 case itself. The case keeps its id, section, history and run results. The adaptation job only reads TestRail. Only the in-place routes write.

  • Preview shows what a write would send. Every role can preview with POST /api/adapt/inplace/preview.

  • Apply and restore write and undo. Only an administrator can use /api/adapt/inplace/apply and /api/adapt/inplace/restore.

  • A request names at most 10 cases. INPLACE_MAX_PER_REQUEST sets the limit.

  • The worker saves the original text before the first write. It reads every write back. Restore returns the original text.

  • The write sends only the five text fields: title, preconditions, steps, expected results and separated steps.

  • A body field changes only when its words change.

  • A title never changes when only the letter case or the spacing differs.

  • Written fields are HTML.

  • Writes run only on the worker that has ADAPT_INPLACE_ENABLED on. That worker is live.

  • The row must have a clean verdict: ready or automation_blocked. A stored published or published_blocked row is clean too. The current adapter version must have made the row.

  • Rows with the status needs_attention, error or pending are never written.

  • A held row has a hold_reason. An administrator sets it with POST /api/adapt/holds.

  • Preview, apply and format upgrade refuse a held row, even with force.

  • A job returns a written row as a cache hit.

  • To adapt a written case again, an administrator presses Re-adapt from the original in the case detail. The route is POST /api/adapt/jobs/from-legacy, at most 200 cases.

  • Write to TestRail in the bulk bar sends 10 cases per request, one request after another. See Bulk Actions.

Flow

Copies in Project 20 (retired)

The tab no longer copies cases into project 20 and shows no project 20 control. While ADAPT_COPIES_RETIRED is set, the publish and target routes answer 409.

A stored error row with the text publish outcome unknown stays as it is. A job does not adapt it again, because a new adaptation could write the case twice. Re-gate or Re-run with engine clears it.

Start a Job

The job form sits under the header (write roles only):

Field Meaning

First pass

The engine that rewrites a case the rules could not finish. Starts at the Settings default (sonnet). See Engines and Cost.

Repair

The engine of a repair pass. Starts at the Settings default (opus).

Max repairs

Repair passes allowed per case, 0 to 5; starts at the Settings default (1). A value outside the range is refused.

Source section

A section of the source project: the job takes the cases TestRail lists for it. The job reads the cases only.

Case IDs

Case ids, comma or space separated, with or without the C prefix (10891, C90006). Used instead of the section.

Limit

Take only the first N cases of the selection; 0 takes all of them. The form starts at 50.

Press Start. A job takes at most 1000 cases, and a case listed twice runs once. The job is refused when another adaptation job holds the slot (see One Job at a Time), when an engine is unknown, or when an ollama: engine is picked while no Ollama URL is set.

Refresh to the Current Version

A change of a rule or of the vocabulary creates a new adapter version. Rows of an older version are stale. A refresh brings them to the current version with the rules only.

  1. Wait until no run and no adaptation job is in progress.

  2. In Run adaptation, press Refresh to current version.

  3. Press Start refresh in the dialog.

    The worker picks the rows, at most 1000 per job. A toast names the rows left for the next refresh. When no row is stale, a toast says Nothing to refresh.

The refresh never calls a model:

  • A case the rules finish gets the current version.

  • The refresh marks a case the rules cannot finish. The mark carries the current version. The case keeps its row and shows Waiting for model.

  • The refresh also takes old-version cases in the statuses Error and No Angular equivalent.

  • A mark from an older version shows Refresh pending. The next refresh checks the case again.

  • The refresh never takes a Written, Restored, No change needed or Held case.

  • The refresh never takes a case with a written text in TestRail. For such a case, use Re-adapt from the original in the case detail.

  • The refresh does not adapt a case again after you edited it with Re-gate. It runs the gate again on the edited text. Then it stamps the current version. Your edit stays.

The owner decides on the model spend for Waiting for model cases. Then select them and press Re-adapt with model. The route of the refresh is POST /api/adapt/jobs/refresh.

What Happens to Each Case

  1. Cache. When the case has not changed in TestRail since its last adaptation (title, preconditions, steps, expected results and template), the stored row was made by the current adapter version and it is not error — or is the error of a publish whose outcome is unknown (see Copies in Project 20 (retired)), whatever its version — the stored row is reused: the job counts it under its stored status, adds no tokens and raises its cache hits. The adapter version changes with a release that changes the rules, the gate or the prompts, and whenever the curated vocabulary or the vendored portal phrases change; a row of another version — every row stored before versions existed included — is adapted again by the next job that selects it, never deleted. The Refresh pending and Waiting for model tiles count the old-version cases still to do. Re-gate stamps the current version on the row it stores, so an edited case is reused rather than adapted over. A refresh gates an edited case again and never adapts it again.

  2. Rules. Deterministic rewrites: the title prefix (CPE Admin > becomes Angular Management Portal >), the login line (Log into the Angular Management Portal), the portal name everywhere else, a step that equals a known legacy menu step or opens with one, a quoted label that has a known Angular name, and — on a step, for the navigation target itself, never for a button, a value, a precondition or an expected result — a label of two or more words that differs from exactly one Angular label only in case, punctuation or a plural s: Navigate to the "Filter (optional)" section becomes … "Filters (optional)" …, keeping the case the step wrote. A curated rename wins over it, an ambiguous label (two Angular labels differ from it the same way) is left alone, and a label of one word is never renamed — "File" is not the Files page when the section means Download file, so such a step goes to the model.

    A step that only opens with a legacy menu step has that clause rewritten and the rest carried as written: Go to "Settings" tab and make sure that any alert is configured becomes Select "Settings" from the left menu and make sure that any alert is configured. The clause must end at and, a comma or a semicolon, and its Angular side must be one step with a quoted label — never a Settings > … menu path (the rest of the sentence would read as the path’s last segment), never a step Angular drops, never one the screen makes unmappable. A legacy tab later in the sentence (Open the mentioned device from the "Device Update" tab) is no menu step and is left for the model. The gate judges such a step by its whole ruled text, so a page the rest of the sentence names still has to survive a model pass.

    A portal case that logs in nowhere gets Log into the Angular Management Portal as the first step of its first scenario (rule R-login in the row’s rule changes). The step carries no source line, the published numbering moves by one and a step N reference moves with it; a model pass that drops the step gets it back, and a case that should carry it and does not is blocked (login-missing). The rule applies only when all of these hold:

    • no field — title, preconditions, steps, expected results — mentions a login (log in, logged in, logging in, log on, login, sign in, signed in, sign-in, and logs in after someone who logs in — "the user logs in", not "the logs in the tab"): C89092 checks that the login screen is shown, and C89037 / C89039 what a user who is not logged in sees;

    • the first action — the first step that is not a Make sure … setup line — opens something on the portal: a page, a tab, a menu item, a form, Go to "X";

    • no step sends a REST / SOAP request or an upper-case GET / POST / PUT / DELETE request, resets or restarts IIS or a service, or opens a URL in any browser: such a case works on the server, not in the portal ("let the device rest" is no REST call);

    • a case of several scenarios does not refer to step numbers — the renumbering covers one scenario, so such a case is left alone and gets the MAJOR login-not-added.

    A case that logs in later (a Make sure … setup line first, then the login) is left alone, and so is a case with no step at all. Over the 3738 source cases this adds the step to 46 cases.

    The rules also add the steps an Angular form needs and the legacy case never wrote (UF-<fact>) — see Missing Intermediate Steps.

    When no legacy wording is left and every navigation target is a known Angular label, the case goes straight to the gate. The case detail then shows engine rules, and no model is called.

    The model does not change an expected result. A case can have legacy labels left only in an expected result. If every navigation target is also a known Angular label, this case goes straight to the gate. The gate blocks the case. The case then shows Needs attention.

  3. First pass. Otherwise the First pass engine rewrites the case. It receives the grammar, the vocabulary entries that occur in the case and portal UI-map snippets for up to three of its navigation targets.

  4. Gate. The result is checked, see The Gate.

  5. Repair. While the gate fails on something in the text and fewer than Max repairs repair passes have run, the Repair engine gets the adapted case, the source and the findings, and the gate runs again. When every blocking finding is a dry-conversion finding (see The Gate) no repair runs: a model cannot change what the converter covers or what the stand provides.

  6. Store. The copy statuses in this step apply only to old rows. The row is saved as ready, automation_blocked, needs_attention or error — see Statuses. A case that already has a TestRail copy keeps showing it: an automation_blocked verdict is stored as published_blocked, and when the text would publish exactly what the copy holds (the same payload as the last publish sent), a ready verdict is stored as published — there is nothing to publish again. A ready verdict with a changed text stays ready, and the copy keeps its old text.

  7. Write. A clean row waits for an administrator to write it in place. See Writing In Place.

A model call gets 240 seconds and is not retried by the job. A call that fails, an answer that is not the expected JSON, and a model refusal each make the case an error row without a repair pass.

Missing Intermediate Steps

An Angular form shows some tabs and enables some buttons only after another action: the Automation events and Automation parameters tabs appear once the chosen model has a template, Add tasks stays disabled until its row has a condition, the Device profile rule tabs need a name and a model, and Send on Custom RPC or on a file upload asks a confirmation pop-up. A legacy case never says so — it was written for a portal that needed fewer actions — so the adapted case looked complete in TestRail and failed on the stand.

scripts/uiflow/registry.yaml lists those gates and the facts that open them. A fact carries the one sentence to perform it, for example Fill in the "Name" field with any value. It also carries the wordings already stating the fact and the converter action for the sentence. For seven of the eight facts, an inserted step runs and does not fall to the agent. The eighth fact, confirm_answered, becomes a noop on purpose. The runner already answers the confirmation pop-up as part of the click. The sentence stays for the reader of the report.

The devices_picked fact becomes a pick_devices step with the placeholder $serial. The runner replaces the placeholder with the first device from this list:

  • the serial of the case

  • the device from the MOVE_DEVICE_TO_DOMAIN preflight of the case

  • the stand’s own device

If the case has a serial, the converter already writes the serial into the step. The model on the form comes from the same device.

The stand’s own device uses the TR-069 protocol. A case can name another protocol, such as the User Services Platform (USP) or Message Queuing Telemetry Transport (MQTT). If such a case has no device of its own, the step fails. The step does not take the stand’s TR-069 device instead.

The step also fails if the dialog does not list the device. For example, a domain user sees only the devices of the domain. If no preflight moves a device into that domain, the dialog has no device for the step. The step never picks the first device in the dialog.

A gate names its screens, its mode (add, edit or any), the control, the facts it requires before that control, the ones that follow it (then — the two confirmations) and the Angular line that proves it. The file is data, not code: a malformed entry is a load error, and the worker then mounts no /api/adapt route at all rather than adapt cases with half a registry.

The rules walk each adapted scenario with state — where the tester stands, whether a form is open, which facts the case has already performed — and insert the missing sentence immediately before the step that reaches the gated control. An inserted step carries no source line and a UF-<fact> change record, which is what exempts it from the invented step check of the gate; the published numbering moves and every step N reference moves with it (R8). A step that hides the work behind an umbrella (Perform all required actions to …) keeps its own line and gets the spelled-out sentences in front of it — the umbrella often names where it goes next, which a replacement would drop. A step that opens a tab and uses that tab’s button in one breath is split in two (UF-split), with the facts between the halves. Over the 3738 source cases this adds 744 sentences to 447 cases and splits 45 steps. A case that already states a fact gets nothing for it, and a second pass over an adapted case adds nothing more.

What the walk cannot settle by itself is a BLOCKER, so the case waits in needs_attention:

Finding What a human does

uiflow-order-<fact>

The case performs the enabling action after the step that needs it, so inserting would do it twice. Move that action before that step, or delete the later one.

uiflow-compound-<fact>

One step opens the tab and uses its control together, and its first clause is not a clean navigation, so nothing can be split off. Split the step by hand: the navigation, then the enabling action, then the control.

uiflow-unverified-<gate id>

The gate’s Angular evidence no longer matches the census, so what the portal does there is no longer known. Check the control on the portal and correct the registry entry (below).

uiflow-renumber

The case has several scenarios and refers to step numbers, so no step may be inserted at all — R8 renumbers one scenario only. Insert the step by hand and renumber the references.

Adding a gate. Find the condition in the oneiot_angular checkout. Add the fact — its sentence, the recognisers that spot a case which already does it, and the converter: action the sentence maps to — or reuse an existing one; then add the gate with its screens (keys of generated/portal_screens.json), control, requires / then and angular.evidence as path:line. Then run python scripts/sync_ui_gates.py: it rewrites generated/ui_gates.json, the census of every form-state condition in the checkout, and the new entry’s fingerprint goes into the gate. A condition held in TypeScript is not censused and keeps fingerprint: null — nothing checks such a gate automatically, so it is verified by hand on the portal. python scripts/sync_ui_gates.py --check exits 1 when the vendored census is stale and is due on every frontend bump: a gate whose fingerprint the census no longer carries loads unverified, and the cases that reach it are reported instead of adapted.

The registry, its loader and the census belong to the adapter version, exactly as the curated vocabulary does. Editing any of them makes every stored row stale and the next job over them adapts them again — so batch the registry work and run once. Only a case whose adapted text changes becomes Ready to write, and an administrator writes it.

The Gate

A case is ready only when no check below finds a problem. The gate never runs the case on a stand.

Check Blocks the case when

Legacy wording

The title, a precondition, a step or an expected result still names the legacy portal (CPE Admin, "old portal", "legacy portal").

Unmappable

The adapted text names a legacy screen — or a legacy control or location — that has no Angular equivalent (Hazelcast Statistics, Unclosed connection, SQL query, Reactivated Reports, Save encryption, Next step), or the model listed an element it could not map. An element the model listed does not block when every label it quotes is a label of the Angular Management Portal — as written, or by the same two-word near-match the rules use — and an adapted step still names that label: the finding stays as a MAJOR naming the label that exists, so the case can still be written. Only the steps count, because only a step performs an element: a label that survives in the title, a precondition or an expected result while the step naming it was folded away blocks, as does an entry that quotes no label, one whose label is known only from the legacy portal or exists nowhere, and one that names a removed screen.

Navigation

A navigation target is not a label of the Angular Management Portal. A label known only from the legacy portal does not count. A target the step quotes as a menu path (Select "Settings > Protocols > TR-069" from the left menu) is read segment by segment, the way the same path written unquoted already is: it is grounded when every segment is, and a quoted value that merely carries a > ("Value must be > 0", "<serial>") is no path and stays ungrounded.

Values

A value of the source — a number, IP address, TR-181 path (Device.…) or quoted value — is not where it belongs in the adapted case. Values are compared whole: 100 does not keep 10, and 70 does not keep -70. Every number is a value: a decimal (11.0), each number of a range or a ratio (70-100, 11.0/1000.0), a number that ends a sentence (in step 6.), follows a hyphen (TR-069) or carries a unit (20MHz is the value 20, 2.4GHz the value 2.4). The one exception is a step or list number that opens a line (1., 2), 3 -, 2.1.), so renumbering a list is not a finding. Digits that continue a word or a dotted name (WiFi6, X.509) are not a number of their own. Each field is checked on its own: a scenario’s expected result must carry exactly the values of its source expected result, none missing and none added; the values of a step stay in the steps of the same scenario or in the preconditions; those of a precondition in the preconditions or any step; those of the title in the title, the preconditions or the steps. Two quoted strings may be renamed, and only on a precondition or step line: the portal name, and the label a navigation word attaches to (Go to the "Device Update" tab). In a title or an expected result both are values.

Source coverage

A source line is claimed by no adapted step and does not appear in a precondition, or the number of scenarios differs from the source.

Dry conversion

The adapted case, converted with the LLM off, fails to convert, or its coverage map is not audited by design: a sentence nothing performs, a sentence that needs infrastructure the stand lacks, or an expected block that is neither measured nor judged.

Every finding lowers the Score — 100, minus 40 per BLOCKER, 10 per MAJOR and 3 per MINOR — and a problem found by a check above is a BLOCKER. The gate also reports findings that do not block, for example a number that the adapted title, preconditions or steps carry and the source does not (in an expected result it blocks), a share of catch-all interact steps in the dry conversion above Fallback warn, a button caption the vocabulary does not know, or a scenario that lost its expected result.

When every BLOCKER comes from the coverage map of the dry conversion — a sentence nothing performs (coverage-uncovered-…), a sentence that needs infrastructure the stand lacks (coverage-blocked-…, infra-blocked-<category>), a map that is not audited (coverage-not-audited) or a step the conversion lost (step-lost-…) — the adapted text itself is clean: the case is automation_blocked, no repair runs, and an administrator can write it. Any other BLOCKER — wording, navigation, values, source coverage, an unmappable element, a conversion that crashed, and a coverage map the gate could not build at all (coverage-unavailable, a failure of the gate itself) — keeps the case in needs_attention.

Some infrastructure is deferred rather than blocking. The converter stamps one category, usp_device, for a USP, MQTT or LwM2M device case, and it is the evidence that names the protocol (USP device, MQTT device, LWM2M device). The IoT emulator every worker carries (mcp-iot-emul) serves USP and MQTT, so for those the only thing missing is the converter’s route to it — and not LwM2M, which the stand does not run at all, so an LwM2M case keeps every BLOCKER and waits in automation_blocked exactly as before. A marker whose evidence names no protocol at all is treated the same way LwM2M is — as hard.

For a USP or MQTT device the infra finding becomes a MAJOR with the recommendation Automation coverage deferred: needs the IoT emulator route in the converter (mcp-iot-emul is on the stand), and so does every coverage finding that the same device is what blocks — the coverage-blocked-… of each sentence it stops and the coverage-uncovered-… that follow. The ids and the refs are unchanged, so the row panel still points at the same sentences; only the severity moves, and a case blocked by nothing else is therefore ready. The decision is per sentence wherever the coverage map records which reason blocked it, and per case otherwise: one reason the stand really lacks (file_server, license, oracle_db, an LwM2M device, …) keeps that sentence’s BLOCKER and leaves the case in automation_blocked. A wording, navigation, value or source-coverage BLOCKER is never deferred, so a case carrying one is still needs_attention.

A deferred case can be written, and a run still cannot execute it. The runner keeps its own list of waived infra categories and usp_device is not on it, so a batch skips such a case with needs infra: usp_device until the converter routes it to the IoT emulator. That is accepted for now — those runs are deprioritised — and the gap is counted rather than hidden: the Cases header carries an <n> deferred pill, GET /api/adapt/status returns counts.deferred, and every row carries deferred: true. Open the row to see which sentences — the Findings panel lists each deferred MAJOR with that recommendation.

When the dry conversion cannot reach a service it needs, there is no verdict: the row is stored as error with that reason, no repair runs, and the next job tries the case again.

A legacy control the gate reports as unknown wording is cleared by curating it in scripts/adapter/vocab/legacy_management.yaml, in one of two shapes. When the Angular portal has the control under another name, a labels: entry gives the name — always scoped to the element word the cases actually write (button, link, section, drop-down, checkbox, field, tab, left menu), because the value check trusts a curated rename only when its entry names an element kind: a rename of kind any on a step, a precondition or an expected result merely turns the wording finding into a Values one. A rename never contradicts the element word the source wrote either: where the only kind that could fire would publish a picker as a link or a screen as a page, the label is left uncurated and its cases wait for a human, because nothing in the table can rewrite that word. When the control is genuinely gone, a removed: entry with a one-sentence note naming the Angular source that proves it sends the case to a human with the evidence instead of a bare "no Angular name for it" — but the note’s phrase is matched over the whole source text, prose included, so a phrase also found in a case with a clean verdict today would block that case too and belongs in a report to QA rather than in the file. Either entry changes the vocabulary digest, hence the adapter version, so every stored row is adapted again by the next job: batch the vocabulary work and run once.

Following a Job

The last job line of the Status panel counts each case a refresh left for the model as needs model. The progress row of the job shows the same count as a needs model pill.

Under the form the tab shows the job’s status pill, <done> / <total> · now C<id>, a pill per row status, tokens <in> / <out> · billed <n> · cache hits <n>, a progress bar and the job’s error lines. The Cases list refreshes as cases finish, and the case in progress carries a blue running pill. When the job ends, a toast gives its status and counts.

Pill Job status Meaning

running

running

Cases are being adapted.

pausing

running

The model refused a call. No further case starts; the cases already in flight finish, and the job slot stays taken until they have.

paused: usage limit

paused_usage_limit

The job ended after such a refusal — the Claude usage limit, or a Claude authentication failure (its error lines say which). The refused case is stored as error.

done

done

Every case of the selection was processed.

stopped

stopped

Stop was pressed, or the job lost the shared job slot and stopped itself (an error line says so). The cases in flight finished; no other case started.

error

error

The job failed outside any case; its error line says why.

not visible here

—

The worker that serves your session does not know the job, so this page cannot follow it. The job may still be running on another worker.

Stop is shown to write roles while the job runs. It answers The job is not running on this worker when the job belongs to another worker.

To carry on after a pause or a stop, start a job over the same selection. Unchanged cases that already have a row are reused at no token cost; error rows and the cases the earlier job never reached are adapted. A case TestRail could not return leaves no row at all: the job counts it as an error and names it in its error lines.

One Job at a Time

Only one adaptation job runs at a time across all workers, and Re-run with engine counts as a job. The slot is taken in the database the workers share, so a second Start, on any worker, is refused with a message that names the holder: an adaptation job is already running: job <id> on worker <host>:<pid>.

A job’s live progress is known only to the worker that runs it. When a job holds the slot and the worker serving your session is not running it, the tab shows the slot instead: an amber job slot held pill and Held by <worker> (job <id>) since <time>, last heartbeat <time>. A running job refreshes its heartbeat every minute. The tab reads the slot when it opens, on Reload and after its own actions — not continuously.

A worker that crashed or was redeployed in the middle of a job leaves the slot taken; the slot frees itself 30 minutes after the last heartbeat. To free it sooner, a write role presses Clear stuck claim and confirms.

Clear the slot only when its holder is gone — a heartbeat that has stopped moving is the sign. If that job is still running, a second job can start and pay for the same cases again; the cleared job stops itself once it notices. The clear is refused when the slot changed hands after the tab read it: press Reload and look again.

Statuses

Every case of the source suite has exactly one status. The first matching rule wins. The counts add up to the cases of the source suite plus the orphans.

Status Rule Next action

Not adapted

No adaptation row for the source case.

Adapt

In progress

A job has the case, no verdict yet.

Wait

Written

Adapted text live in TestRail, whatever the latest verdict or error.

Done

Error

The last adaptation had no verdict.

Adapt again

Restored

Original text restored by an administrator.

Done

No change needed

Nothing to write; gate not failed.

Done

Held

Held by an administrator, with a reason.

QA or owner decides

No Angular equivalent

A legacy screen or control without an Angular equivalent.

QA rewrites or retires

Waiting for model

Marked by the current refresh; the rules alone cannot finish it.

Owner decides

Refresh pending

Made by an older adapter version; not refreshed yet.

Runs after a deploy

Needs attention

The current adapter version still fails the gate.

Edit and re-gate

Ready to write

Clean verdict; the writer has something to send.

Write to TestRail

The To do row of the panel holds every status except Written, No change needed and Restored. These three form the Done row.

Notes on the rules:

  • In progress comes first. Written, Error, Restored and No change needed come next. A hold or a version does not change these cases. Error is a to-do status.

  • A live write wins over the latest verdict and over an error. A later re-adaptation can fail. The case stays Written. Its reason line then says a later re-adaptation needs attention; TestRail holds the earlier written version. After an error it says a later re-adaptation failed: <error>; TestRail holds the earlier written version.

  • A newer adaptation can wait for a write. The case stays Written. Its reason line then says a newer adaptation waits for a write in the Project 15 panel.

  • The Title column of a Written case shows the title that TestRail holds. A newer adaptation does not change it.

  • The refresh and the bulk action Adapt / Re-adapt never take a Written case. An administrator can re-adapt it from the original.

  • Held comes before the version rules. A released case moves to the next matching rule.

  • Refresh pending also covers a row with an unmeasured change.

  • A stored error row with the text publish outcome unknown is not adapted again. See Copies in Project 20 (retired).

  • A formatting-only fix is a change to write. Ready to write covers it too.

The row keeps its stored status and the gate verdict. Verdict and Change are columns of Cases and filters under More filters. A stored published row reads as verdict ready. A stored published_blocked row reads as automation_blocked. The stored statuses published and published_blocked belong to the retired copies in project 20. No new row gets them.

Status Panel

The header line names the source project, its case count and the time of the last read. The Reload button of the header reads the tab again. The Status panel sits under the header.

  • Each status has a tile with its count and its next action.

  • The To do row comes first, the Done row second.

  • A tile with no case stays in place, dimmed.

  • Click a tile number. Cases then shows exactly that status.

  • The bar splits all cases into the statuses. The legend gives each count and share.

  • The last line names the last job of this worker.

The worker reads the source suite once an hour. Before the first read, Not adapted shows —. The header then says case list not read yet.

An orphan is an adaptation row without a case in the source suite. The summary names the orphans when there are any.

Sections

The Sections table lists every source section in the TestRail tree order. Each line shows the total, one column per to-do status and the Done count. The to-do columns include In progress. The All sections line gives the totals. Click a section to filter Cases. Click it again to show every section.

Cases

Cases lists every case of the source suite, one row per case. The worker filters and sorts the list. Every filter covers the whole suite.

Column Content

Case

The case id, linked to TestRail.

Section

The source section path.

Title

The adapted title, as written or as the writer will write it. Hover for the original title.

Status

The status pill and one reason line: the hold reason, the error or the first BLOCKER finding.

Verdict

The gate verdict. Empty for a case without an adaptation.

Change

none, formatting, words or unknown.

Score

The gate score.

Updated

The time of the last adaptation.

Filters:

  • The Status pills. Each pill shows its count under the other filters. A click adds or removes the status.

  • The section list and the search box. The search matches a case id prefix or a title substring.

  • This job only keeps the rows of the job this page follows.

  • More filters opens Verdict, Change and Deferred. These filters skip cases without an adaptation.

  • Clear filters resets all filters.

Case, Section, Status, Verdict, Score and Updated sort on a click. A second click reverses the order. A page holds 50, 100 or 200 rows. The browser keeps the filters, the sort and the page.

Click a row to open the case detail. A Not adapted case has no detail. Its id links to TestRail. The links use the TestRail URL set on the worker of your session.

Export CSV downloads every row of the current filter. The columns are id, section, title, status, reason, verdict, change, project 15 state, hold reason, updated and stored status.

Bulk Actions

A checkbox selects a row. The header checkbox selects the page. Select all N cases matching the filter selects every page. A selection survives a page change and a filter change.

The bulk bar shows only the actions allowed for the selected statuses. Each button counts the cases it takes. Every action reads the selection again first.

Action Statuses Role Rules

Adapt / Re-adapt

Not adapted, Refresh pending, Error

editor

One job, at most 1000 cases. A case with a written text in TestRail is left out. A case with a hold reason is left out too.

Re-adapt with model

Waiting for model

editor

The owner’s decision. The dialog states the model calls.

Write to TestRail

Ready to write

admin

10 cases per request, one request after another. Stops at the first failed request. Never writes a held case.

Hold…

Any to-do status except Not adapted and Held

admin

A reason is required.

Release hold

Held, or any case with a hold reason

admin

Clears the hold.

Before a job starts, a dialog states the case count, the engines and the upper bound of model calls. The upper bound is the case count multiplied by one more than Max repairs. The dialog also counts the cases with a hand edit. A new adaptation replaces that edit.

Adapt / Re-adapt, Re-adapt with model and Write to TestRail wait while an adaptation job runs. Write to TestRail also waits while in-place writes are off. The button tooltip says why. Hold… and Release hold work during a job.

After a bulk write, the bar lists each case with its outcome. When a request fails, the toast names the number of cases sent before it.

Reviewing a Case

The detail header shows the status pill and its reason line. Small text gives the verdict, the change, the engines and the tokens.

The detail shows the source case as TestRail has it now (Source (TestRail)), the adaptation as JSON (Adapted (editable JSON), read-only for viewers), Findings (N) with severity, id, message and recommendation, a line Dry conversion: <n> steps, <m> fallback (<p>%), step types: …, and the number of rule changes. Closing it with an unsaved edit asks Discard the edited adaptation?.

The JSON has this shape:

Key Holds

title

The adapted title.

preconditions

A list of lines. A source line moved here counts as covered when its text (or its rule-rewritten text) appears in one of them.

scenarios

One entry per source scenario: source_scenario (0-based index), expected, and steps — each {source_lines, text}, where source_lines are the 1-based source lines the step covers.

unmappable

Legacy elements with no Angular equivalent. An entry blocks the case unless every label it quotes is a label of the Angular portal and an adapted step still names it (see The Gate), and then it is only a MAJOR.

notes

Free text.

Re-gate (write roles) stores the edit and runs the gate again without a model: the row becomes ready, automation_blocked, needs_attention or error, and a toast shows the status and the score. The edit must be valid JSON in the shape above, checked the way a model’s answer is: a source_scenario outside the source’s scenarios, source_lines that are not a list of line numbers, a step without text, or preconditions / unmappable / notes that are not lists is refused with the reason, and nothing is stored.

Re-gate marks the case as edited by hand. A job that adapts the case again clears the mark. A new adaptation replaces your edit. Re-run with engine asks you to confirm before it replaces an edited case.

Re-run with engine (write roles) adapts the case again as a one-case job, with the engine picked beside the button as its first pass; its repairs use the Repair engine and Max repairs the worker has from Settings. The stored row changes only when the re-run finishes, and it is refused while an adaptation job holds the slot. A case the rules adapt completely never reaches a model, so its re-run applies the rules and the gate again, whatever engine is picked.

Hold and Release hold sit in the case detail for an administrator. Hold needs a reason and applies to cases still to do. Release hold shows for any held case. The writer refuses a held case.

A Written case stays Written after a later re-adaptation. When the new text is clean, the detail says a newer adaptation waits. In the Project 15 (in place) panel of the detail, press Preview in project 15, then Write to TestRail. When the re-adaptation fails, the detail shows the reason line and a note. A plain job would adapt the text now in TestRail. That text is the written text. The detail hides Re-run with engine for a Written case, and the server refuses it. An administrator presses Re-adapt from the original instead. The job adapts the saved original text. The button waits while a job runs. A restored case can still use Re-run with engine.

An administrator can name at most 200 cases in one Re-adapt from the original request. The bulk bar has no such action.

Engines and Cost

Engine Notes

sonnet

Claude Sonnet through the Claude CLI. Default First pass.

opus

Claude Opus through the Claude CLI. Default Repair.

ollama:<model>

A local model over the Ollama API at Ollama URL. The engine selects offer ollama:qwen3.5:9b whenever an Ollama URL is set — the default http://ollama:11434 is — whether or not the worker can reach it; another model is named in Settings. When Ollama cannot be reached, every case that needs a model becomes an error row and the job carries on.

Only cases the rules cannot finish spend tokens.

The case detail and the job line show input and output tokens followed by billed <n>: input, output and prompt-cache tokens (cache creation and cache reads) together. The Claude engines report cache tokens and Ollama does not, so compare engines by the billed figure. A case reused from the cache adds nothing to the job: it counts under cache hits, while its row still shows what it cost when it was adapted.

Pilot report (every role) opens a table with one line per first-pass engine — rules for cases no model touched:

Column Meaning

Cases

Rows whose current adaptation used that first-pass engine.

Ready 1st pass / Ready after repair

ready or published rows without and with a repair pass.

Automation blocked

automation_blocked rows, and published rows that were published as automation_blocked (their last gate still had blocking findings).

Needs attention / Errors

Rows in those statuses.

Mean tokens in / out, Mean billed, Mean s

Per row.

Project tokens in / out, Project billed, Hours

The means multiplied by the number of cases in the source project.

With This job only ticked, the report covers the rows last adapted by the job this page follows; otherwise every row. It reads the rows as they are now, hand edits included. When TestRail cannot be reached, the extrapolation uses the number of stored rows instead of the source project’s case count.

Comparing engines on the same cases

A second job over the same unchanged cases reuses the stored rows and calls no model, so it measures nothing new. Re-run with engine ignores the cache, but it does not add a row: each case has exactly one, and a re-run replaces it — engine, tokens, repairs, time, findings and status — whatever its outcome. The Pilot report then counts the case under the new engine in place of the old one; it never shows two engines for the same case. Read and note the first engine’s Pilot report before re-running its cases with another engine.

A re-run also gives a stored published row a fresh verdict.

Settings

Settings → Adaptation (legacy cases) (admin only, see Settings):

Field Default Environment variable Meaning

Target TestRail project name

Angular Management Portal new

ADAPT_TARGET_PROJECT_NAME

Retired. The Adaptation tab does not use it.

Source project id

15

ADAPT_SOURCE_PROJECT_ID

The project cases are read from, and the project the in-place write changes.

First-pass engine

sonnet

ADAPT_ENGINE_FIRST

Default of First pass: sonnet, opus or ollama:<model>.

Repair engine

opus

ADAPT_ENGINE_REPAIR

Default of Repair, and the repair engine of Re-run with engine.

Max repairs

1

ADAPT_MAX_REPAIRS

Default of Max repairs, and the value Re-run with engine uses. A value outside 0 to 5 is clamped.

Workers

2

ADAPT_WORKERS

How many cases one job adapts at the same time — not the worker stacks. A re-run uses one. A value outside 1 to 4 is clamped.

Ollama URL

http://ollama:11434

ADAPT_OLLAMA_URL

Where ollama:<model> engines are called; it must be reachable from the worker. Empty: no Ollama engine is offered, and one is refused.

Fallback warn

0.5

ADAPT_FALLBACK_WARN

The share of catch-all interact steps in a dry conversion above which the gate adds a MAJOR finding. It lowers the score and does not block.

First-pass thinking

false

ADAPT_FIRST_PASS_THINKING

Whether a Claude first pass uses extended thinking. Off, a first pass cost about a seventh of the output tokens and time in the measurement it was chosen on, and still answered in the expected form.

Repair thinking

true

ADAPT_REPAIR_THINKING

Whether a Claude repair uses extended thinking. When it differs from First-pass thinking and both passes use the same model, the repair gets a Claude process of its own instead of continuing in the first pass’s.

Short system prompt

true

ADAPT_SHORT_SYSTEM_PROMPT

On, every Claude call gets a one-line system prompt (rewrite the case, answer with the JSON object only) in place of Claude Code’s own, which roughly halves a first pass’s output again. Off keeps Claude Code’s system prompt.

Whatever these three say, a Claude call never gets tools and never reads Claude Code’s auto memory: the case is answered from its prompt alone. Ollama engines ignore all three.

When a change takes effect

Saving writes the environment profile; it changes no worker by itself. The profile’s values are written when a run of that env is dispatched, into the settings file the pooled workers share: a dispatch to any worker rewrites it for all of them, each worker picks the values up the next time it reloads its settings, and a run of another env replaces them the same way.

The Adaptation tab talks to the worker that serves your session (see Which worker answers), so the values that worker uses are the ones the tab shows: the source project in the header, and the First pass, Repair and Max repairs defaults the job form starts from — read once per page load, so reload the page to pick up new ones. Workers, Ollama URL, Fallback warn and the three thinking and system prompt settings are not shown in the tab; GET /api/adapt/status reports the last three under defaults.

Who Can Do What

Action admin editor viewer

Open the tab; read the header, the Status panel, the job slot, a job’s progress, Cases and the Pilot report; open a case; follow the TestRail links

✓

✓

✓

Start, Stop and Refresh to current version; Clear stuck claim

✓

✓

—

Adapt / Re-adapt and Re-adapt with model in the bulk bar

✓

✓

—

In-place preview

✓

✓

✓

In-place apply and restore; Write to TestRail; Hold… and Release hold; Re-adapt from the original

✓

—

—

Edit the adapted JSON, Re-gate, Re-run with engine

✓

✓

—

Settings → Adaptation (legacy cases)

✓

—

—

The write actions are enforced by the server as well; a viewer’s page simply does not show them. See Roles & Capabilities.

After Writing

Import the written cases through the TestRail modal. See Importing Tests. Run a small batch before you import everything. The gate converted each case with the LLM off. An import with the LLM on adds its enrichment. A green run is not proof. Audit the steps against the source case, as for any other import.

A deferred case is worth importing but not worth queueing yet. A batch skips it with needs infra: usp_device. The skip lasts until the converter routes its USP or MQTT device to the IoT emulator. See The Gate.

All four worker stacks must run the same image

Worker stacks 3, 25, 34 and 40 and the gateway must run the same image version. Only then is the Adaptation tab consistent. The adapter store is on the live worker only. On another stand the tab lists every source case as Not adapted. Select the live stand to see the adaptations.