Skip to content

Commit fc873b3

Browse files
committed
docs(envlite): plan for router DOCUMENT_ROOT fix
Implementation plan for the router path-resolution fix landed in 07f3550 and tested by 28c6332 / 271d7f6. Matches the convention of the two existing plan files in docs/superpowers/plans/.
1 parent 271d7f6 commit fc873b3

1 file changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Fix envlite router path resolution Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Make `tools/local-env/router.php` serve the repo where `php -S` was launched, not the repo that owns the router file.
6+
7+
**Architecture:** The PHP built-in server populates `$_SERVER['DOCUMENT_ROOT']` from its `-t` flag (resolved to an absolute path). `envlite_run_dev_server` already chdirs to the target repo and passes `-t src`, so `DOCUMENT_ROOT` always equals `<target-repo>/src`. Replace the router's two `dirname(__DIR__, 2) . '/src'` expressions with `$_SERVER['DOCUMENT_ROOT']` so the router stops assuming it lives inside the target repo.
8+
9+
**Tech Stack:** PHP 7.4+, PHP built-in server (`php -S`), envlite test harness (custom — `tools/local-env/tests/harness.php` + `run.php`).
10+
11+
---
12+
13+
## Bug recap (why this exists)
14+
15+
`router.php:11` and `router.php:25` use `dirname(__DIR__, 2) . '/src'` to locate the document root and front controller. `__DIR__` is the directory of the *router file itself*, so when envlite is invoked from a checkout other than the one that owns `router.php` (e.g. running `php /path/to/envlite-checkout/tools/local-env/envlite.php up` from a different worktree), the router loads the originating checkout's `src/index.php` — pulling in the *wrong* `wp-config.php`, which then triggers a WordPress canonical-URL 301 to whatever port that wp-config defines.
16+
17+
Reproduction observed: server bound at `127.0.0.1:8722` (target repo's port), but `GET /` returned `301 Location: http://127.0.0.1:8762/` (originating envlite checkout's wp-config port).
18+
19+
## File Structure
20+
21+
- **Modify:** `tools/local-env/router.php` — replace both `dirname(__DIR__, 2) . '/src'` expressions with `$_SERVER['DOCUMENT_ROOT']`. No structural change; same 25 lines.
22+
- **Create:** `tools/local-env/tests/test_router.php` — integration test that boots `php -S` against a fixture site whose path is unrelated to the router file's location, then asserts the request was served from the fixture (not from envlite's own tree).
23+
24+
The router file is intentionally small and a single responsibility; no extraction is needed.
25+
26+
---
27+
28+
### Task 1: Add the failing regression test
29+
30+
**Files:**
31+
- Create: `tools/local-env/tests/test_router.php`
32+
33+
This test boots a real `php -S` with the shipped `router.php`, pointing `-t` at a tmp fixture directory that contains a tiny `index.php` marker. It then `GET`s `/` and asserts the marker came back — proving the router served the fixture, not the envlite checkout's own `src/`.
34+
35+
Picking a free port: bind to `tcp://127.0.0.1:0`, read the assigned port from `stream_socket_get_name`, close the socket, then hand the port to `php -S`. There's a microscopic race window between close-and-rebind; if it bites in practice, retry once. Don't preemptively engineer for it.
36+
37+
- [ ] **Step 1: Write the failing test**
38+
39+
Create `tools/local-env/tests/test_router.php`:
40+
41+
```php
42+
<?php
43+
function envlite_test_router_pick_free_port(): int {
44+
$sock = @stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
45+
if ($sock === false) {
46+
throw new \RuntimeException("could not bind to find free port: $errstr");
47+
}
48+
$name = stream_socket_get_name($sock, false);
49+
$port = (int) substr($name, strrpos($name, ':') + 1);
50+
fclose($sock);
51+
return $port;
52+
}
53+
54+
function envlite_test_router_wait_for_bind(int $port, float $timeout_seconds = 3.0): bool {
55+
$deadline = microtime(true) + $timeout_seconds;
56+
while (microtime(true) < $deadline) {
57+
$check = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.1);
58+
if ($check) {
59+
fclose($check);
60+
return true;
61+
}
62+
usleep(100_000);
63+
}
64+
return false;
65+
}
66+
67+
function test_router_serves_from_document_root_not_router_directory() {
68+
// Build a fixture "site" that does NOT share a parent with router.php.
69+
$site = envlite_test_tmpdir('router-docroot');
70+
file_put_contents("$site/index.php", "<?php echo 'FIXTURE_OK ' . __DIR__;");
71+
72+
// Use the real shipped router so we exercise its path resolution.
73+
$router = realpath(__DIR__ . '/../router.php');
74+
envlite_assert(is_file($router), 'router.php must exist at ' . __DIR__ . '/../router.php');
75+
76+
$port = envlite_test_router_pick_free_port();
77+
78+
// Spawn `php -S 127.0.0.1:<port> -t <site> <router>` with cwd = site.
79+
// Matches envlite_run_dev_server: chdir into the target repo, then pass
80+
// -t <docroot>. The router file lives outside $site on purpose — that is
81+
// exactly the configuration that triggered the original bug.
82+
$argv = [PHP_BINARY, '-S', "127.0.0.1:$port", '-t', $site, $router];
83+
$descriptors = [
84+
0 => ['pipe', 'r'],
85+
1 => ['pipe', 'w'],
86+
2 => ['pipe', 'w'],
87+
];
88+
$proc = proc_open($argv, $descriptors, $pipes, $site);
89+
envlite_assert(is_resource($proc), 'failed to start php -S');
90+
91+
try {
92+
envlite_assert(
93+
envlite_test_router_wait_for_bind($port),
94+
"php -S did not bind on 127.0.0.1:$port within 3s"
95+
);
96+
97+
$body = @file_get_contents("http://127.0.0.1:$port/");
98+
envlite_assert($body !== false, "request to 127.0.0.1:$port failed");
99+
100+
envlite_assert(
101+
strpos($body, 'FIXTURE_OK ' . $site) !== false,
102+
'expected FIXTURE_OK marker from fixture index.php, got: ' . substr($body, 0, 400)
103+
);
104+
} finally {
105+
foreach ($pipes as $p) { if (is_resource($p)) { @fclose($p); } }
106+
$status = @proc_get_status($proc);
107+
if ($status && $status['running']) {
108+
@proc_terminate($proc, 15);
109+
}
110+
@proc_close($proc);
111+
}
112+
}
113+
```
114+
115+
- [ ] **Step 2: Run test to verify it fails**
116+
117+
Run: `php tools/local-env/tests/run.php`
118+
119+
Expected: `FAIL test_router_serves_from_document_root_not_router_directory` with a message about the FIXTURE_OK marker being absent. The body will either be empty (require failed — index.php from envlite's own `src/` does not exist in a fresh checkout running tests) or contain unrelated content from envlite's `src/index.php`. Either way the assert fires.
120+
121+
If instead the test passes here, STOP — the reproduction is wrong and the rest of the plan does nothing.
122+
123+
- [ ] **Step 3: Commit the failing test**
124+
125+
```bash
126+
git add tools/local-env/tests/test_router.php
127+
git commit -m "test(envlite): cover router DOCUMENT_ROOT resolution"
128+
```
129+
130+
It is fine — and intentional — to land a failing regression test in its own commit. The next commit makes it pass.
131+
132+
---
133+
134+
### Task 2: Fix the router to use DOCUMENT_ROOT
135+
136+
**Files:**
137+
- Modify: `tools/local-env/router.php:11` and `tools/local-env/router.php:25`
138+
139+
- [ ] **Step 1: Edit router.php**
140+
141+
Replace the current contents of `tools/local-env/router.php` with:
142+
143+
```php
144+
<?php
145+
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
146+
147+
// php -S does not honor Apache .ht* deny rules. Block any segment so the
148+
// SQLite DB at wp-content/database/.ht.sqlite is not downloadable.
149+
if (preg_match('#(^|/)\.ht#', $path)) {
150+
http_response_code(403);
151+
return true;
152+
}
153+
154+
// DOCUMENT_ROOT is the absolute resolution of php -S's -t flag. Using it
155+
// instead of a path computed from __DIR__ lets the router live outside the
156+
// target repo (e.g. envlite invoked from a different checkout).
157+
$docroot = $_SERVER['DOCUMENT_ROOT'];
158+
$file = $docroot . $path;
159+
160+
if ($path !== '/' && file_exists($file)) {
161+
if (!is_dir($file)) {
162+
return false;
163+
}
164+
// Existing directory: let the built-in server serve its index.php
165+
// (e.g. /wp-admin/ -> wp-admin/index.php). Without an index, fall
166+
// through to the front controller to avoid directory listings.
167+
if (file_exists(rtrim($file, '/') . '/index.php')) {
168+
return false;
169+
}
170+
}
171+
172+
require $docroot . '/index.php';
173+
```
174+
175+
- [ ] **Step 2: Run the new test to verify it passes**
176+
177+
Run: `php tools/local-env/tests/run.php`
178+
179+
Expected: `PASS test_router_serves_from_document_root_not_router_directory`, and the final tally line should show one more pass than before with zero failures.
180+
181+
- [ ] **Step 3: Run the full test suite to verify no regressions**
182+
183+
Run: `php tools/local-env/tests/run.php`
184+
185+
Expected: `0 failures` in the final summary line. In particular `test_dev_server_argv_targets_correct_port_root_router` must still pass — it asserts the argv shape, which this change does not touch.
186+
187+
- [ ] **Step 4: Manually verify the original reproduction is fixed**
188+
189+
This is a one-time smoke check, not a permanent test. Skip if you do not have two envlite-prepared worktrees handy.
190+
191+
Run:
192+
```bash
193+
# In one terminal, from a *different* envlite-prepared checkout B, start the server:
194+
cd /path/to/checkout-B
195+
php /path/to/checkout-A/tools/local-env/envlite.php up
196+
```
197+
198+
```bash
199+
# In another terminal:
200+
curl -sI http://127.0.0.1:<B's port>/
201+
```
202+
203+
Expected: a `200 OK` (or whatever the WordPress front page returns), NOT a `301` to checkout A's port.
204+
205+
- [ ] **Step 5: Commit the fix**
206+
207+
```bash
208+
git add tools/local-env/router.php
209+
git commit -m "fix(envlite): resolve router paths via DOCUMENT_ROOT
210+
211+
The router previously used dirname(__DIR__, 2) . '/src' to locate both
212+
the static-file root and the front controller. That resolves relative
213+
to the router file's own checkout, so invoking envlite from a different
214+
worktree loaded the wrong wp-config.php and triggered a canonical-URL
215+
301 to that wp-config's WP_HOME port.
216+
217+
Use \$_SERVER['DOCUMENT_ROOT'] instead — populated by php -S from its
218+
-t flag, which envlite_run_dev_server already points at the target
219+
repo's src/."
220+
```
221+
222+
---
223+
224+
## Self-Review
225+
226+
**1. Spec coverage:**
227+
- Root cause (router uses `__DIR__`-derived path): covered by Task 2 Step 1.
228+
- Fix uses `$_SERVER['DOCUMENT_ROOT']`: covered by Task 2 Step 1.
229+
- Regression test exists: covered by Task 1.
230+
- Manual verification of original reproduction: covered by Task 2 Step 4.
231+
- No structural changes to envlite.php: confirmed — `envlite_dev_server_argv` already passes `-t src`, no change needed.
232+
233+
**2. Placeholder scan:** No TBD/TODO/"fill in later". All code is concrete; all commands explicit.
234+
235+
**3. Type/identifier consistency:**
236+
- Helper names `envlite_test_router_pick_free_port` and `envlite_test_router_wait_for_bind` are referenced in the test exactly as defined.
237+
- `envlite_test_tmpdir` is defined in `tests/test_manifest.php:2` and reused here (matching the pattern in `test_smoke.php:3` and `test_atomic.php`).
238+
- `envlite_assert` is defined in `tests/harness.php:2`.
239+
- `proc_open` with `$descriptors` array form, then `proc_terminate`/`proc_close` — standard PHP API.

0 commit comments

Comments
 (0)