Settle #216 by keeping renderer decisions in modules, and extract the first one - #229
Merged
juanmaguitar merged 3 commits intoAug 10, 2026
Conversation
#216 asks for a deliberate choice between building a DOM harness for index.jsx and continuing to push decisions out into pure modules. This is the second, made explicit rather than left to drift one PR at a time. The rule goes in the review standard, where every agent and Copilot already read it, and says out loud what it does not buy: no-unused-vars catches a module whose last call site is deleted, but nothing catches a second code path answering the same question inline, which is what #180 was. site-folder.cjs is the first extraction under it — the Create site modal's path arithmetic, which was pure, untested, and chose a path separator by looking at the string. Both platforms are now exercised from one machine. directoryFromFileEntry came out as it stood, dead branch and all, and its tests pin what it actually answers today rather than the shapes it was written for. That turned out to be #228. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comments called the C:\fakepath outcome "the bug" and said what the function answers today is wrong. Dropping a folder on the location control is a route the app does not support — the click and keyboard handlers both go to the native dialog — so what these record is where an unsupported route currently ends, and the failing tests to write against if #228 is ever taken up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closed as not planned: the modal's route is clear as it stands and drop support is a feature rather than a gap. The comments said the question was still open, which would have read as a loose end to whoever got here next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Extracts Create site path decisions into a testable renderer module and establishes this pattern as a review invariant.
Changes:
- Adds tested site-folder sanitization and path-resolution helpers.
- Replaces inline renderer logic with module calls.
- Documents the module-extraction review standard.
No findings across the five review dimensions.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/renderer/site-folder.cjs |
Adds pure site-folder and path helpers. |
src/renderer/index.jsx |
Uses the extracted helpers. |
test/site-folder.test.cjs |
Covers sanitization and cross-platform paths. |
.github/instructions/code-review.instructions.md |
Establishes renderer-module review guidance. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
juanmaguitar
added a commit
that referenced
this pull request
Aug 10, 2026
## Why
While a new site is still cloning, the "Trac ticket" card sits below the
"Initial setup checklist", inviting the contributor to link a ticket to
a site that cannot do anything with one yet — the ticket's pull requests
and attachments panels only matter once the environment can apply and
run them. The neighbouring "Apply a patch or PR" card already waits for
the wizard; this one didn't.
## What changes
The Trac ticket card now renders under the same `skipInit` gate as the
"Apply a patch or PR" card. `skipInit` is set both by the wizard's final
step ("Start dev server & finish wizard") and by the "Skip
initialization wizard" link, so the card appears exactly when the setup
checklist disappears, on either path. No logic moved: the ticket-loading
effects already key off the linked ticket, not off the card being
mounted.
## How to test this
Platforms: any.
**Starting state:** the app open, no site selected.
1. Create a new site. While the "Initial setup checklist" is showing
(even mid-clone), scroll down.
- **Expected:** there is no "Trac ticket" card between the checklist and
the Terminal.
2. Click **Skip initialization wizard**.
- **Expected:** the checklist is replaced by the dev-server controls and
the "Trac ticket" card appears, with the "Apply a patch or PR" card
below it.
3. On another new site, complete the wizard instead: run the install and
build steps, then click **Start dev server and finish the wizard**.
- **Expected:** the "Trac ticket" card appears at that moment.
4. Open a site that was initialized before this change and has a ticket
linked.
- **Expected:** the card is still there, showing its ticket, with "Open
in Trac" and "Unlink" working.
**What must not have happened:** an already-linked ticket must not have
been forgotten — hiding the card is presentation only, the stored
`tracTicket` survives untouched. The "Apply a patch or PR" card must not
have changed when it appears.
## Risks and limitations
One pre-existing edge case, deferred (see review outcome): a site whose
ticket was linked *before* its wizard was finished — possible until this
change — now shows no ticket UI until the wizard is completed or
skipped. The state is recoverable through the "Skip initialization
wizard" link, which is always visible in the checklist.
## Related
Follow-up to #109 (the ticket card) and #229 (which settled where
renderer decisions live).
---
<details>
<summary>Design decisions and alternatives considered</summary>
- Gate on `skipInit` rather than `initialized`: `skipInit` is what
already splits the page between "wizard showing" and "wizard behind us"
(checklist at one end, dev-server controls and the patch card at the
other), and it covers the skipped-wizard path, which `initialized` does
not.
- No `src/renderer/*.cjs` decision module (the #216 invariant): the
condition is a bare existing boolean with no derivation — a
`shouldShowTracCard(skipInit)` module would wrap an identity function.
The invariant targets inline logic with branches, not a visibility
toggle that makes two cards consistent.
</details>
<details>
<summary>Review outcome (required — see AGENTS.md)</summary>
0 [fix here] · 1 [follow-up] — the follow-up deferred.
- architecture · 🔵 low · [follow-up] — a site can already hold
`tracTicket` with `skipInitWizard` false (linked pre-wizard, which this
change makes impossible going forward); such a site shows no ticket UI
until the wizard is done or skipped, and its loader effect still spends
one unauthenticated GitHub request per activation on a list nothing
renders. Recoverable via the always-visible skip link. Cheap close if
wanted: treat `tracTicket && !skipInitWizard` as skipped when reading
site status.
- §1 renderer-modules invariant and §5 test rules checked and not
tripped: no decision to extract, so no module and no reachable test
surface; `eslint .` and the full suite (621 tests) pass on both counts.
</details>
<details>
<summary>Screenshots or recording</summary>
Before: the state in the report — an uninitialized, still-cloning site
showing the checklist with the Trac ticket card beneath it. After: the
same page shows checklist → Terminal → Logs, with the ticket card absent
until the wizard finishes or is skipped, then identical to today's
post-wizard page.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
#216 asks for a decision, not a patch:
src/renderer/index.jsxis 4163 lines, mounts itself at module scope, and cannot be loaded without a DOM — so every decision made inside it is untestable by construction. Thirteen pure modules have been factored out of it, each with good tests, and nothing proves the component still reaches them.test/ipc-wiring.test.cjsexists because that gap became #180 on the main-process side.The issue weighs two directions: a DOM harness, or continuing to push decisions into the modules. It asks for that to be settled deliberately rather than one PR at a time. This settles it, and lands the first extraction under the new rule.
What changes
The decision is extraction, recorded as an invariant in §1 of the review standard with a pointer from §5, because that file is the only copy of the standard and the one Copilot reads natively.
The rule states its own limit rather than overselling it. Two things came out of looking at the current state:
src/renderer/*.cjshave a live call site, andno-unused-varsalready fails the build when the last one goes. The "wire cut by deletion" case is largely covered already.src/renderer/site-folder.cjsis the first extraction:sanitizeSiteFolder,resolveTargetDiranddirectoryFromFileEntry, the Create site modal's path arithmetic. Chosen because it was pure, untested, and picked a path separator by inspecting the string — the kind of thing only visible by creating a site by hand on Windows.Deliberately not in this PR:
executeTerminalCommand(index.jsx:1616), the largest decision left in the file and the only one with a security surface — it holds theTERMINAL_ALLOWED_SCRIPTSallow-list and has no tests. It wants the parse-to-a-plan shapedev-server-command.cjsalready uses, and its own PR.How to test this
Platforms: any for the suite; the Windows separator branch is covered from any machine, since nothing reads
process.platform.There is no behaviour change to observe, so the demonstration is the suite plus one path through the UI that must look identical to before.
Starting state: the app open, on any machine, with no site selected.
trac:45678/fixwill do.Expected: the site is created inside the folder chosen in step 3, in a directory named
trac-45678-fix— the illegal characters became dashes, and nothing else changed about the name.What must not have happened:
wordpress-sitewhen the name given was usable — that fallback is only for a name that sanitises down to nothing.Driven on macOS at
5cdf06a. Nametrac:45678/fixin~/CONTRIBUTIONSproduced/Users/…/CONTRIBUTIONS/trac-45678-fix— inside the chosen folder, both illegal charactersreplaced, no fallback — and the sidebar kept
trac:45678/fixas the label, sositeNameandsiteLabelstill reach the handler as the two different values they are. The clone completed andsetup moved on to
npm install. The Windows separator branch was not driven by hand; it is coveredby
test/site-folder.test.cjsfrom any machine, and a signed Windows artifact is available fromBuildkite for anyone who wants to.
Risks and limitations
Testing this by hand surfaced #228, closed as not planned and not addressed here.
directoryFromFileEntryonly runs when a folder is dropped on the location control — the click and keyboard handlers both go to the native dialog — and that route is not one the app supports. It is half-present rather than absent: the drop populates the input, and what comes back is''or the literalC:\fakepath. Supporting drops is a feature, not a gap, and #228 records the decision not to take it up.The function was extracted exactly as it stood, dead branch included, because changing what it answers was never this PR's to do. Its tests pin where the unsupported route currently ends rather than the shapes the function was written for — see the implementation notes.
The rule this PR adds is enforced by review only. That is stated in the rule itself, and it is the direct consequence of choosing extraction over a harness.
#216 is closed as decided, but not because the gap it names is gone. The proof that
index.jsxstill calls its modules does not exist and, under this decision, will not — what changes is how little is left in the component for a competing answer to hide in.Related
Settles #216 (closed with the reasoning). Surfaced #228, closed as not planned. Follow-up to #180, #211, #213.
Design decisions and alternatives considered
Why not the DOM harness. It needs
Appexported separately from thecreateRootbootstrap, a stub for the 77window.apicall sites, a jsdom devDependency whose behaviour under@wordpress/componentsandxtermis unproven, and CI wiring. Against a 4163-line component that buys coverage of the paths the harness happens to drive. The cost is real and the coverage is partial, which is what tipped it.Why not a static wiring test. The obvious cheap analogue of
ipc-wiring.test.cjs— parseindex.jsx, assert each module's exports are referenced — was measured and dropped. Every import already has a call site, andno-unused-varsfails the build when the last one goes, so such a test would duplicate the linter while catching nothing the linter misses. It would also not catch #180, which is the failure that matters.Why this module first.
site-folder.cjsis pure, has no IPC, was completely untested, and is cross-platform path handling — a dimension the review standard already treats separately. It makes the rule concrete without the PR turning into a rewrite.What was left in the component.
handleCreateDirInputChangekeeps the DOM work — readingfiles, clearingvalue, calling the setters — and one conditional state assignment. The rule as written does not carve that out; if it should, that clause is worth adding once there is a second example of it.Review outcome (required — see AGENTS.md)
/self-reviewrun against the working tree before this branch had any commits, with the judgement pass dispatched to a subagent given only the diff and the instructions file.1 [fix here] · 2 [follow-up] — the [fix here] is fixed; one follow-up filed and closed as not planned, one recorded.
test/site-folder.test.cjswere built on{ path: '…' }fixtures.File.pathdoes not exist in this app: Electron removed the augmentation in v32 in favour ofwebUtils.getPathForFile, this repo pins Electron 43, andsrc/preload.jsbridges nowebUtils. The wholeif (rawPath)branch is unreachable, so those tests were green while proving nothing — one of the shapes §5 names. Two of the fixtures also omitted the leading segmentwebkitRelativePathactually carries, which is the only reason they passed. Rewritten to cover what reaches the function today, with theC:\fakepathoutcome recorded as where an unsupported route ends and pointed at Dropping a folder on the Create site location button is half-supported #228.''(silently clearing an already-chosen folder) orC:\fakepath(handed towordpress:setup, wherefse.ensureDirwould create it). Weighed and declined: the modal's intended route — text field for the name, button for the location — is clear as it stands, and supporting drops is a feature rather than a gap. Note the drop behaviour is read from the code and Chromium's documented handling ofwebkitdirectory; it has not been reproduced by hand.sanitizeSiteFoldernow has a canonical home but its twin is still inline in thewordpress:setuphandler insrc/main.js— the same three replacements in the same order, differing only in fallback. It is literally the "second code path answering the same question inline" the new §1 text says nothing catches. Harmless today because main returnscreatedPathand the row adopts it; worth collapsing when either regex next changes.site-folder.cjsis dependency-free CJS andmain.jscould require it directly.The extraction itself was checked line by line and holds. The removed
finalizehelper was atypeofguard over a value that was already a string plus a trailing-separator strip the module now does;setCreateSiteDir(resolved)withif (resolved) setCreateSiteError('')is the old two-branch body exactly. TheuseCallbackdependency array was updated.Implementation notes
On testing
directoryFromFileEntryhonestly. Three options were on the table: keep thepathfixtures with a note that they cover the legacy Electron <32 shape, delete the dead branch outright, or leave the function untouched and test what actually reaches it. The first is the "green while proving nothing" §5 forbids. The second is a behaviour change inside a PR that advertises none. The third is what shipped: the module's header documents the dead branch and why it cannot run, and the tests assert''for a real Electron 43 entry andC:\fakepathfor the fallback, recorded rather than endorsed and pointing at #228. A file input'svalueis empty or that literal prefix by specification, on every platform, so there is no realistic input for which the old assertions were reachable. If the decision in #228 is ever revisited, these are the tests it has to rewrite.On
resolveTargetDirat a drive root.C:\normalises toC:, which has no backslash left to detect, so it takes the forward-slash branch and producesC:/my-site. Still absolute on Windows, so not a bug — but it was invisible before and now has a test saying what it does and why that is acceptable, rather than leaving the next reader to re-derive it.Files.
src/renderer/site-folder.cjs(new, 3 exports + the fallback constant),test/site-folder.test.cjs(new, 11 tests),src/renderer/index.jsx(44 lines out, 11 in),.github/instructions/code-review.instructions.md(the invariant in §1, one pointer in §5).Screenshots or recording
Nothing on screen changed. The Create site modal renders and behaves exactly as before; the extraction is invisible to the contributor, which is the point of the manual steps above.