fix(test): close the test-entry leak into real trip content
A ui-test entry had survived into the active trip's dailies. Three independent failures had to line up for that, and all three were real: 1. cleanupEntry() used host-side fs.rmSync. Grav's Apache workers run as root, so every entry the form creates is root-owned and recursive removal needs write permission on that directory — which the host user lacks. Cleanup had never worked for form-created entries; it just threw inside a path nothing checked. It now falls back to `docker exec … rm -rf` in the container that actually serves USER_DIR. 2. globalTeardown's dailies sweep keyed off a `parent:` in post-form.md — a key deliberately removed (the write target comes from site.yaml active_trip, and CLAUDE.md forbids re-adding a static parent). The regex could never match, so dailiesDir was always null and the sweep silently did nothing. It now reuses helpers' own resolution instead of keeping a divergent copy. 3. Nothing pinned the suite to this checkout's server. playwright.config.js defaults to :8081, so a worktree run hit the MAIN checkout — entries created in one content tree while the specs asserted and cleaned up in another. test-ui now passes GRAV_BASE_URL from GRAV_PORT, and globalSetup hard-fails when the server's bind mount disagrees with the tree the specs read. Also fixed, found on the way to a green run: - test-account interpolated the password into an `sh -c` string, so a password containing a shell metacharacter was re-parsed by the container's shell (`sh: 2: <fragment>: not found`, no account, every UI run dead). It now travels via `docker exec -e`, making the recipe indifferent to its contents. - `make start` in a worktree always failed: travel-memories declares `env_file: .env` and worktree-new creates none. It degrades to start-grav there — a worktree with no server is what sent runs to :8081 in the first place. - test-form-config asserted a hero_image field that 8cf1145 deliberately removed; it had been failing ever since. Verified: config 22/22, post 6/6, location-override 20/20, and a full UI run now leaves zero ui-test entries behind. The remaining UI failures are pre-existing on main — site.yaml pins owner_username to a real account while the suite logs in as testrunner, so owner-only controls never render for it. Only trip-publish.spec.js patches that; delete-flow, edit-mode and anon-view do not. Left for a separate branch.
This commit is contained in:
+59
-2
@@ -170,6 +170,60 @@ async function createPhotoEntry(page, tag, { content, publish = true, created }
|
||||
'Entry posted successfully!', { timeout: 15_000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Grav container that serves USER_DIR, so cleanup can delete as root.
|
||||
* Prefers GRAV_CONTAINER (set by .worktree-env / .env), else matches on the bind
|
||||
* mount so a worktree never picks the main checkout's container.
|
||||
*/
|
||||
function resolveGravContainer() {
|
||||
if (process.env.GRAV_CONTAINER) return process.env.GRAV_CONTAINER;
|
||||
try {
|
||||
const want = fs.realpathSync(USER_DIR);
|
||||
const names = execSync("docker ps --format '{{.Names}}'", { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] })
|
||||
.split('\n').filter(Boolean);
|
||||
return names.find((n) => {
|
||||
const src = execSync(
|
||||
`docker inspect ${n} --format '{{range .Mounts}}{{if eq .Destination "/var/www/html/user"}}{{.Source}}{{end}}{{end}}'`,
|
||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
|
||||
).trim();
|
||||
return src && fs.realpathSync(src) === want;
|
||||
}) || null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entry directory, falling back to the container when the host cannot.
|
||||
*
|
||||
* Grav's Apache workers run as root, so every entry the form creates is
|
||||
* root-owned. Removing one recursively needs write permission on that directory,
|
||||
* which the host user does not have — so a plain fs.rmSync throws EACCES and the
|
||||
* entry survives. That is how a ui-test entry ended up committed-adjacent in the
|
||||
* active trip's content on 2026-07-24: cleanup had never actually worked for
|
||||
* form-created entries, it just failed inside a path nothing checked.
|
||||
*
|
||||
* `docker exec … rm -rf` runs as root in the container, which can remove them.
|
||||
*/
|
||||
function removeEntryDir(dir) {
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true });
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code !== 'EACCES' && err.code !== 'EPERM') throw err;
|
||||
}
|
||||
const container = resolveGravContainer();
|
||||
if (!container) {
|
||||
throw new Error(
|
||||
`Cannot remove ${dir}: it is root-owned (written by Grav in the container) and no ` +
|
||||
`matching container was found to delete it as root. Set GRAV_CONTAINER or remove it manually.`
|
||||
);
|
||||
}
|
||||
execSync(`docker exec ${container} rm -rf '/var/www/html/user/${path.relative(USER_DIR, dir)}'`,
|
||||
{ stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a tracker entry folder by a unique slug fragment, then delete it.
|
||||
*/
|
||||
@@ -179,7 +233,7 @@ function cleanupEntry(slugFragment) {
|
||||
const entries = fs.readdirSync(TRACKER_DIR);
|
||||
const match = entries.find(e => e.includes(slugFragment));
|
||||
if (match) {
|
||||
fs.rmSync(path.join(TRACKER_DIR, match), { recursive: true });
|
||||
removeEntryDir(path.join(TRACKER_DIR, match));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,4 +256,7 @@ function readEntryMd(entryDir) {
|
||||
return fs.readFileSync(path.join(entryDir, name), 'utf-8');
|
||||
}
|
||||
|
||||
module.exports = { fillEditor, waitForPhotoUpload, postEntry, createPhotoEntry, cleanupEntry, findEntry, readEntryMd, TEST_PHOTO, TRACKER_DIR, ACTIVE_TRIP_URL };
|
||||
// USER_DIR is exported so global-setup/global-teardown resolve the same tree the
|
||||
// specs assert against, instead of keeping their own (previously divergent) copy
|
||||
// of this logic.
|
||||
module.exports = { fillEditor, waitForPhotoUpload, postEntry, createPhotoEntry, cleanupEntry, removeEntryDir, findEntry, readEntryMd, TEST_PHOTO, USER_DIR, TRACKER_DIR, ACTIVE_TRIP_URL };
|
||||
|
||||
Reference in New Issue
Block a user