Agent credential bootstrap - Manual Test Plan โ
| Field | Value |
|---|---|
| Branch / PR | feat/admin-and-merchant-portals (no PR opened yet) |
| Environment | dev VM, main checkout, non-interactive Bash tool shell (PATH has no Cloud SDK entry) |
| Build flags / config | n/a |
| Build (commit) | cb4a10a8 |
| Tester | Claude (agent session), live-executed before this project was reported done |
| Date | 2026-08-24 |
Summary โ
| # | Scenario | Result | Notes |
|---|---|---|---|
| 1 | Fresh creation of both credential files | [x] pass [ ] fail [ ] blocked | 24/24 secrets fetched, 0 failed |
| 2 | gcloud PATH resolution, with a falsifying control | [x] pass [ ] fail [ ] blocked | control genuinely fails; fix genuinely works |
| 3 | Idempotent re-run skips already-set keys | [x] pass [ ] fail [ ] blocked | 0 fetched, N skipped on run 2 |
| 4 | Self-healing permissions | [x] pass [ ] fail [ ] blocked | run against a synthetic file, not the real secrets |
| 5 | Concurrent writers never corrupt the target | [x] pass [ ] fail [ ] blocked | 6 real concurrent processes, synthetic target |
| 6 | lantern doctor reports all four file states | [x] pass [ ] fail [ ] blocked | happy path live; other three against synthetic files |
| 7 | e2e script can parse the real probe file | [x] pass [ ] fail [ ] blocked | key presence + length only, value never printed |
| 8 | No secret value ever appears in bootstrap output | [x] pass [ ] fail [ ] blocked | dry-run masks with bullets; only key names print |
| 9 | Both files stay outside git | [x] pass [ ] fail [ ] blocked | invisible to git status; gitignored defensively too |
Setup / preconditions โ
gcloudauthenticated ascattreedev@gmail.com(operator's existing ADC), SDK installed at/home/mechelle/google-cloud-sdk, deliberately NOT on PATH in this shell (the exact condition under test).- The 24 secrets already existed in GCP Secret Manager, project
lantern-app-dev, before this session started. - Neither
~/.lantern-agent-probe.envnor~/.lantern-test-accounts.envexisted at session start (confirmed vials). - Scenarios 4 and 5 deliberately run against synthetic scratch files, never the real credential files, so there is no window where a live secret sits at the wrong permission or races a concurrent write.
Scenarios โ
1. Fresh creation of both credential files โ
- Goal:
npm run env:bootstrap(vianode tooling/scripts/bootstrap-env.mjs) creates both files from nothing. - Preconditions: neither file exists.
- Steps:
- Run
node tooling/scripts/bootstrap-env.mjs --dry-runfirst, confirm the preview. - Run
node tooling/scripts/bootstrap-env.mjsfor real. ls -lboth files.
- Run
- Expected: both files created, all keys fetched, mode 0600.
- Verify (data/console):
ls -loutput; script's ownN fetched, N skipped, N failedsummary line.
Result: [x] pass [ ] fail [ ] blocked
Actual: Dry run previewed all 24 keys masked. Real run: agent-probe 4 fetched/0 skipped/0 failed, test-accounts 20 fetched/0 skipped/0 failed.
Evidence: `-rw------- 1 mechelle mechelle 190 ... .lantern-agent-probe.env` and `-rw------- 1 mechelle mechelle 624 ... .lantern-test-accounts.env`
Follow-up: none2. gcloud PATH resolution, with a falsifying control โ
- Goal: prove the resolver is load-bearing, not decorative: the OLD behavior (bare
gcloud) must actually fail here, and the NEW behavior (resolved path) must actually succeed, in the identical shell. - Preconditions: none.
- Steps:
- Run bare
gcloud versionin this shell (the control: the code path being replaced). - Run
node -e "import('./tooling/shared/gcloud.mjs').then(m => console.log(m.resolveGcloudBin()))". - Run
./lantern doctorand read thegcloud CLI/gcloud auth (ADC)rows.
- Run bare
- Expected: step 1 fails (falsifying arm: if it succeeded, the whole premise of this project would be wrong and PATH resolution was never the problem). Steps 2 and 3 succeed.
- Verify (data/console): exit code of step 1; doctor's printed row text.
Result: [x] pass [ ] fail [ ] blocked
Actual: Step 1: `/bin/bash: line 4: gcloud: command not found`, exit 127 (the control genuinely fails, confirming this was a real bug, not a hypothetical one). Step 2: resolved to `/home/mechelle/google-cloud-sdk/bin/gcloud`. Step 3: doctor printed "gcloud CLI: installed" and "gcloud auth (ADC): authenticated", both green.
Evidence: raw command output captured in this session's transcript.
Follow-up: none3. Idempotent re-run skips already-set keys โ
- Goal: a second
env:bootstraprun (no--force) must not re-fetch anything it already wrote. - Preconditions: scenario 1 already ran, both files populated.
- Steps:
- Run
node tooling/scripts/bootstrap-env.mjsagain, no flags.
- Run
- Expected:
0 fetched, 4 skippedfor the probe file,0 fetched, 20 skippedfor the test-accounts file (opposite of run 1's4 fetched, 0 skipped/20 fetched, 0 skipped). - Verify (data/console): the script's own summary line, both files.
Result: [x] pass [ ] fail [ ] blocked
Actual: `0 fetched, 4 skipped (already set), 0 failed` and `0 fetched, 20 skipped (already set), 0 failed`, exactly the opposite counts from run 1.
Evidence: command output.
Follow-up: none4. Self-healing permissions โ
- Goal: if a credential file's mode ever drifts from 0600 (a stray
chmod, an editor save), the next bootstrap run must silently correct it, becausewriteSecretFilechmods unconditionally on every write, even a run where every key was skipped. - Preconditions: a synthetic scratch file at the wrong mode. Never run this against the real credential files (no reason to open even a brief window on a live secret when a synthetic file proves the same code path).
- Steps:
- Create a scratch file,
chmod 644it. - Run the exact tmp-suffix-plus-rename-plus-chmod sequence from
writeSecretFileagainst it.
- Create a scratch file,
- Expected: mode is 0600 afterward.
- Verify (data/console):
ls -lbefore and after.
Result: [x] pass [ ] fail [ ] blocked
Actual: before `-rw-r--r--` (0644), after `-rw-------` (0600).
Evidence: `ls -l` output captured in this session's transcript.
Follow-up: none5. Concurrent writers never corrupt the target โ
- Goal: several agent sessions on this shared VM can run
env:bootstraparound the same time; the pid-suffixed tmp path plus atomic rename must mean the target file is always either the old complete content or one writer's new complete content, never a mix of two. - Preconditions: synthetic scratch target, six standalone Node processes each writing a distinguishable payload.
- Steps:
- Launch 6 processes in the background, all targeting the same synthetic path, all racing.
- Wait for all 6 to finish.
- Read the final file back; count distinct lines (
sort -u | wc -l); check for leftover.tmp.*files.
- Expected: exactly one distinct line value (proves the file is one writer's complete, uncorrupted output, not an interleaved mix); mode 0600; zero leftover tmp files.
- Verify (data/console):
sort -u <file> | wc -lmust equal 1;lson the tmp glob must find nothing.
Result: [x] pass [ ] fail [ ] blocked
Actual: `sort -u | wc -l` = 1 (proc1 won the race, and every one of its 50 repeated lines survived intact). Mode was `-rw-------`. No `.tmp.*` files remained.
Evidence: command output captured in this session's transcript.
Follow-up: the pre-existing `.env.local` writer (`mergeIntoEnvFile`) uses a non-pid-suffixed tmp path and was NOT changed by this project (out of scope; see the project README's "found but not fixed" section).6. lantern doctor reports all four file states โ
- Goal: doctor must correctly distinguish missing / wrong-permissions / present-but-incomplete / present-and-complete, and name the exact fix command for the first three.
- Preconditions: the happy-path case runs against the real files (post scenario 1); the other three run against synthetic files with the exact row-computation logic doctor uses, so the real files are never deliberately broken to watch doctor react.
- Steps:
- Run
./lantern doctorfor real, read the two credential-file rows. - Separately, exercise the same row logic against: a nonexistent path, a present-but-0644 path, and a present-but-missing-one-key path.
- Run
- Expected: real run shows both rows green, "present, mode 0600". Synthetic runs show, respectively: "missing (label)" + the bootstrap hint; "wrong permissions, want 0600" + a chmod hint; "present, missing keys: B_KEY" + the bootstrap hint.
- Verify (data/console): doctor's printed rows; the synthetic harness's printed objects.
Result: [x] pass [ ] fail [ ] blocked
Actual: Real doctor run: both rows green, "present, mode 0600". Synthetic harness: all three failure branches produced exactly the expected detail + hint text, and the fourth (happy) branch matched too.
Evidence: doctor's colored terminal output; synthetic harness console output, both in this session's transcript.
Follow-up: none7. e2e script can parse the real probe file โ
- Goal:
tooling/e2e/admin-signin-tier.e2e.mjs's ownparseEnv(unmodified by this project) must readAGENT_PROBE_EMAILout of the file this project now writes. - Preconditions: the real probe file exists (scenario 1).
- Steps:
- Reproduce that script's exact
parseEnvfunction in a one-offnode -e, point it at the real file. - Check
typeof value === 'string' && value.length > 0, and the parsed key count, without ever printing the value itself.
- Reproduce that script's exact
- Expected:
AGENT_PROBE_EMAILparses as a non-empty string; key count is 4. - Verify (data/console): the boolean and the count, never the value.
Result: [x] pass [ ] fail [ ] blocked
Actual: "AGENT_PROBE_EMAIL parsed as non-empty string: true", "key count in file: 4".
Evidence: command output captured in this session's transcript.
Follow-up: none8. No secret value ever appears in bootstrap output โ
- Goal:
--dry-runand the real run's console output must never print a fetched value, matching the existing.env.localflow's SM-1 masking rule. - Preconditions: none.
- Steps:
- Read the full
--dry-runoutput for both new targets. - Read the full real-run output for both new targets.
- Read the full
- Expected: every printed line for a secret is either a key name with a checkmark, or the literal
โขโขโขโขโขโขโขโขmask; no line contains an actual credential. - Verify (data/console): manual read of the captured console output.
Result: [x] pass [ ] fail [ ] blocked
Actual: confirmed by direct read of both outputs; every value-bearing line used the bullet mask, real-run lines showed only "Fetching KEY_NAME... [checkmark]".
Evidence: command output captured in this session's transcript.
Follow-up: none9. Both files stay outside git โ
- Goal: the two files must never be trackable by this repo's git, and the
.gitignorepattern must match them defensively even though they live at$HOME. - Preconditions: both files exist at
$HOME. - Steps:
git status --short | grepfor either filename.git check-ignore -vboth bare filenames.realpathboth files, confirm they resolve outsidegit rev-parse --show-toplevel.
- Expected: zero
git statusmatches;check-ignorereports a match against the new.gitignorelines; realpath is outside the repo root. - Verify (data/console): command output.
Result: [x] pass [ ] fail [ ] blocked
Actual: 0 matches in git status for both names. `git check-ignore -v` matched both against `.gitignore:51` and `.gitignore:52`. `/home/mechelle/.lantern-agent-probe.env` resolves outside `/home/mechelle/repos/lantern_app`.
Evidence: command output captured in this session's transcript.
Follow-up: noneNotes โ
- This plan was written after the three pieces were built, because the task's own "Done when" checklist already specified the exact verifications up front; each scenario above reproduces one of those checklist items (or a falsifying control this session added, scenarios 2's control and scenario 5) rather than being drafted blind. Every result is a real, freshly-run command from this session, not a projection.
- No PR exists yet for
feat/admin-and-merchant-portals(checked viagh pr list), so this plan is not yet linked from a PR; link it when one is opened.