MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3692

(motir-core) `main` cannot deploy — the `billing-cloud` at-scale leg has been red since 07:18Z on the transient double-subtree strict-mode class at `/settings/project/ai-planning`, and no production deploy has gone out since 01:50Z

Done
Description

Found while running MOTIR-3682, whose acceptance criterion 2 cannot be met until main deploys.

The consequence first, because it is the reason this is highest

deploy is a job inside ci.yml gated behind the at-scale legs on push-to-main (.github/workflows/ci.yml, "Ship it — the deploy runs AFTER the gates"). That leg is red, so nothing has deployed since release v166, 2026-08-27T01:50Z.

  • fly releases -a motir-core shows v167–v172, all complete, all carrying the same ImageRef registry.fly.io/motir-core:deployment-01M10E9MC4EWT8YPBSMT4EKVZ8. They are secrets set restarts, not deploys. FLY_IMAGE_REF read from inside all four machines agrees.
  • #2344 (MOTIR-1103, the public changelog + follow/digest) merged at 07:07Z and is not live. grep -rl 'system.public-follow-digest-tick' /app/worker inside the running machine returns nothing; system.attachment-gc (control) hits.
  • Two ids routed into MOTIR_POSTGRES_JOB_IDS today therefore point at jobs the running process does not know about, and have no timer on either lane — see MOTIR-3688 and #2351.

Nothing alerts on this. It was found by reading fly releases while chasing an empty job_run.

The failure

Playwright E2E at-scale (billing-cloud), run 33061991669 on 555932db9. Two attempts, four different tests, one surface:

attemptspeclocatoroutcome
1cloud-cadence.spec.ts:131getByTestId('ai-planning-settings')failed (attempt + retry)
2cloud-cadence.spec.ts:131 and :432samefailed
2cloud-lesson-recording.spec.ts:165getByTestId('ai-planning-record-mistakes-explanation')failed
2cloud-lesson-library.spec.ts:125flaky (passed on retry)

Every one is the same error:

Error: strict mode violation: getByTestId('ai-planning-settings') resolved to 2 elements:
  1) <div class="flex flex-col gap-5" data-testid="ai-planning-settings">…
       aka locator('#main').getByTestId('ai-planning-settings')
  2) <div class="flex flex-col gap-5" data-testid="ai-planning-settings">…
       aka getByTestId('ai-planning-settings').nth(1)

Root cause — verified, not assumed

The test id is rendered in exactly ONE place: app/(authed)/settings/project/ai-planning/_components/AiPlanningSettingsEditor.tsx:295. git grep 'ai-planning-settings' -- app components returns that one line. So there is no duplicated component and no second render path — the whole route SEGMENT is mounted twice, transiently, during the client-side navigation into it. Note the two locators: one resolves under #main, the other does not, which is what a second segment subtree outside the committed one looks like.

All four failures go through the same three lines, duplicated verbatim in two spec files (cloud-cadence.spec.ts:127, cloud-lesson-recording.spec.ts:112):

await page.goto('/settings/project');
await page.getByRole('link', { name: 'AI planning' }).click();
await page.waitForURL('**/settings/project/ai-planning');
await expect(page.getByTestId('ai-planning-settings')).toBeVisible();   // ← strict, no settling

waitForURL resolves on the URL, which the router writes before the old subtree unmounts, so the assertion can run while both are attached. toBeVisible() on a strict locator refuses rather than waiting for the count to settle — auto-retry cannot save it, because the expectation throws on the violation instead of retrying it. That is why the leg fails on attempt AND retry, and why a re-run only rotates which of the four specs loses the race.

This is the transient double-subtree class the flake log already tracks (board-scrum.spec.ts:114, project-square-flow.spec.ts:196, comments.spec.ts:261), at its predicted escalation: "if this recurs, the duplicate shape changed — the whole subtree is doubling, and the fix is a settling toHaveCount, not more scoping." It is NOT MOTIR-2506 / MOTIR-2600 / MOTIR-2621, which are /planning first-paint TIMEOUTS on a different route with a different signature; all three are done.

Fix direction

Scoping to #main is not enough on its own — the duplicate can land inside it — so settle the count before asserting, in ONE shared helper rather than in each spec:

const panel = page.getByTestId('ai-planning-settings');
await expect(panel).toHaveCount(1);      // waits for the old subtree to unmount
await expect(panel).toBeVisible();

toHaveCount retries on the COUNT and never trips strict mode, which is exactly the property missing here. Then de-duplicate openAiPlanningSettings — it exists twice, byte-for-byte, and cloud-lesson-library.spec.ts has a third variant (railEntry) — so the fix lands once for all four specs.

Do NOT re-run past this. It has now failed on two consecutive runs of the same commit; the second re-run is what produced the four-spec picture above.

Acceptance criteria

  • openAiPlanningSettings lives in ONE place, is used by cloud-cadence.spec.ts, cloud-lesson-recording.spec.ts and cloud-lesson-library.spec.ts, and settles the panel's count before any strict assertion.
  • The same settling is applied to ai-planning-record-mistakes-explanation at cloud-lesson-recording.spec.ts:165, which failed on the identical shape.
  • The assertions still FAIL when the panel genuinely does not render — a toHaveCount(1) that passes on zero would be worse than the flake, so the negative direction is exercised and its output quoted in the pull-request body.
  • Playwright E2E at-scale (billing-cloud) is green on this card's own pull request, run with the e2e-at-scale label so the leg actually executes (it is push-only otherwise), and the run id is quoted.

CLOSE-OUT, not a criterion — the reason run.md forbids a criterion that reads on post-merge state, and the reason this card exists at all: after the merge, confirm fly releases -a motir-core shows a NEW ImageRef, not merely a new version number. A green check is what everybody already had; a deploy is the thing that was missing. Do not flip this card done on the check alone.

Context refs

  • .github/workflows/ci.yml — the deploy job and the at-scale gate above it
  • tests/e2e/cloud-cadence.spec.ts:127 · tests/e2e/cloud-lesson-recording.spec.ts:112 — the duplicated helper
  • app/(authed)/settings/project/ai-planning/_components/AiPlanningSettingsEditor.tsx:295 — the single render site, which is what rules out a component-level duplicate
  • CI run 33061991669, both attempts