Commit Graph

6 Commits

Author SHA1 Message Date
xarmian 9e6791d4f1 fix(scripts): install-refresh asks git in the caller's checkout, not the server's cwd (BUG-3403) (#1790)
* fix(scripts): install-refresh asks git in the caller's checkout, not the server's cwd (BUG-3403)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(scripts): anchor the web/ pathspec at the repo root (codex r1 on BUG-3403)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 05:52:30 -04:00
xarmian d4ec587de9 fix(cli): a CLI call during an install-refresh bounce waits for the refresh instead of auto-starting a server over it (BUG-3215) (#1544)
install-refresh.sh stops the server, installs, and restarts it with the
captured argv. Any pad CLI call in between found nothing answering and
auto-started a server of its own: the caller's binary, cwd and default
flags. That server held the port against the refresh's restart (day 79:
127.0.0.1:7777 went dark and the script exited 1).

- The script writes <DataDir>/refresh.lock {pid, port, deadline} by rename
  before the stop and removes it in its EXIT trap, only while it is still
  its own. The deadline is 10s + PAD_PROBE_TIMEOUT (30s by default).
- EnsureServer defers to a live marker for its own port (writer pid alive
  AND deadline not passed): it polls health and never spawns. If the
  writer is still alive past the deadline it errors naming the pid. A dead
  writer, a passed deadline, another port or an unparseable file means
  the marker is ignored.
- The script names a holder that took the port before the restart spawns
  (and does not spawn it), and refuses success when the listener after
  the probe is not the restart it spawned. A health probe answers for
  whoever holds the address.
- The script-test dependency guard now reads every install_refresh*_test.go.

Claude-Session: https://claude.ai/code/session_01A3rXV7GDLAhBe9mL4V6z2m
2026-09-25 09:55:48 -04:00
xarmian b2bee3242f fix(scripts): install-refresh restores the server's own cwd and matches the running server by identity (BUG-3196) (#1512)
* fix(scripts): install-refresh restores the server's own cwd with its argv, refuses a deleted one, and matches the running server by identity rather than the built binary's name (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): one existence check refuses a deleted server cwd, naming the path without the kernel's suffix (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* test(scripts): the no-/proc refresh test declares lsof, which now reads the server's cwd there (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* test(scripts): the lsof dependency sits on the requireScriptDeps line the guard reads (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* docs(make): the install target's guarantees include the restored cwd (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): the server's cwd is re-checked immediately before the stop, and the capture's limits are stated (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): a failed restart cd says what state it leaves, not that no server runs (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): an unreadable server cwd is refused naming what was read and its error, and the case is tested (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N
2026-09-24 10:38:16 -04:00
xarmian c3d64451ed test(buildtools): the install-refresh pre-server proves it is the listener the script probes (TASK-3146) (#1443)
* test(buildtools): the install-refresh pre-server proves it is the listener the script probes (TASK-3146)

Seven tests pre-start a "running server" on a port chosen by freePort, which
releases the port before the stub binds it, and then waited only until
SOMETHING answered on it. A stub that lost the port in that window exits on
the bind error and the dial succeeds against the foreign listener, so the
script's outcome was judged against someone else's process.

The port cannot be 0 there, as it is in BUG-3144's two-server test, because
the script restarts that server and reads the port from PAD_PORT or the
captured argv. startPreServer therefore proves two things instead: the
stub's own STUB_ANNOUNCE line, with the port it bound equal to the chosen
one, and its pid at the address the script will probe (a new /stub/pid
route), which covers a wildcard bind beside a held specific address on a
stack that permits it. A collision is now a named fixture failure.

Tests force each case: a foreign listener answering health ok on the
chosen port, a probe address that reaches someone else, and a stub that
bound a different port, plus a positive control.

Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX

* test(buildtools): hold the chosen port in the port-proof case so the stub's port 0 cannot land on it (TASK-3146, codex round 1)

Claude-Session: https://claude.ai/code/session_01Vf8uZmYdsrQqJtbAmjUFSX
2026-09-22 19:55:00 -04:00
xarmian 94381d09a5 test(buildtools): start the two-server fixture on port 0 and wait on each stub's own announcement (BUG-3144) (#1434)
* test(buildtools): start the two-server fixture on port 0 and wait on each stub's own announcement (BUG-3144)

TestInstallRefresh_RefusesWhenSeveralServersAreRunning failed once on
CI's Go (PostgreSQL) job (run 35738655417): "script proceeded with two
servers running". The script was right. The fixture had one server.

The script counts server PROCESSES (pgrep -x plus the `server start`
argv). The test picked two "free" ports with freePort, which releases the
port before the stub binds it, and proved readiness by dialing each
port. A stub whose port was taken in between (by the other stub, or by
anything else on a busy runner) exits on the bind error, the dial
succeeds against the other listener, and only ONE stub process exists.
The script correctly proceeds, and the test blames it.

The stub now binds via net.Listen and, with STUB_ANNOUNCE=1, prints the
port it actually got. Otherwise it behaves as before (a failed bind
exits). The test starts both stubs on port 0 and waits on each
process's own "LISTENING <port>" line, so readiness is tied to that
process and two stubs cannot share a port. A stub that exits is now a
fixture failure naming itself, not an assertion about the script.

Evidence, pre-committed on BUG-3144 before running:
- unfixed, forced portB = portA: 3/3 fail with the CI message
- unfixed, a foreign listener pre-bound on portB: 3/3 fail, same message
- unfixed, unmutated, -count=30, all CPUs busy + a port-churning load:
  2/30 fail (one exactly the CI message; one the same mechanism with a
  failed restart)
- fixed, the second stub forced onto the first's port: 3/3 fail as
  "stub exited without announcing a port" (a fixture failure, not a
  wrong verdict about the script)
- fixed, same contention + churn, -count=50: 50/50 pass

Not changed: eight other tests pre-start a server on a chosen port the
script must restart on, so they cannot use port 0. They share the
pick-then-bind window.

Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd

* test(buildtools): reap announced stubs in t.Cleanup and keep draining their stdout (BUG-3144)

Codex round 1, both valid: startAnnouncedStub started each stub without
ever calling Wait, and its reader stopped after the first line, so a
stub writing more could block on a full pipe.

Each stub is now killed and reaped in t.Cleanup, which runs even when an
assertion fails. Wait closes the pipe, ending the reader. The reader
drains stdout after the announcement.

Hygiene, not measured: with the old code the defer's pkill plus the test
binary's exit leave nothing observable afterwards, so no counterfactual
discriminates this change. Stated rather than claimed.

Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
2026-09-22 13:23:50 -04:00
xarmian 110045578c fix(build): make install proves what it installed and what it restarted (BUG-2897, TASK-2787) (#1272)
`make install` made three claims it did not check.

1. It brought the server back BY SIDE EFFECT -- `pad auth whoami` triggers
   an auto-start, which does not know the killed process's argv. A server
   running `--host 0.0.0.0` came back bound to the default host alone:
   curl 127.0.0.1:7777 -> 000 while the LAN address -> 200, with the
   process count and the version both reading correct (BUG-2897).

2. `cp` copied whatever was at the repo path, not what the invocation
   built. Two sessions sharing the checkout interleave and the loser's
   build is installed by the winner, every exit code green (TASK-2787).

3. "Server restarted." was printed after a command ending in `|| true`,
   with no probe of any kind. Not "the wrong address went unverified" --
   nothing was verified. Found reading the recipe; neither filing names it,
   and it is what made the other two invisible.

The logic moves to scripts/install-refresh.sh for one reason above
readability: a script can be TESTED. internal/buildtools drives it against
a compiled stub `pad`, including the branches where a check must FAIL.
Recipe-inline logic is only exercisable by running `make install`, which
stops the developer's server -- a test nobody runs twice, which is how this
target accumulated three unverified claims.

The script checks OUTCOMES rather than steps: what got installed, and what
is answering afterwards. Two commit checks, deliberately, answering
different questions -- the ARTIFACT before the kill, so a wrong build costs
an error instead of an outage, and the DESTINATION after the copy, which is
the shared-path race TASK-2787 names. The restart uses the argv read from
/proc before the kill, and nothing is printed about a restart until the
server answers on BOTH 127.0.0.1 and the configured host (`--host 0.0.0.0`
resolving to loopback plus the primary LAN address, since 0.0.0.0 is a bind
spec, not something to curl).

Three defects were found in the fix itself, by its own tests and by the
first real run:

- The restart redirected to $HOME/.pad/server.log with nothing creating
  that directory. On a fresh HOME the redirect fails, the server never
  starts, and the probe reports "did not answer" -- true, and three steps
  downstream. Invisible to every hand-run this script could have had,
  because a developer's box has ~/.pad by luck of history.
- The post-copy check SURVIVED its mutant: both fixtures reported the same
  version from source and destination, so the guard and its absence were
  indistinguishable. Fixed by making the stub's version depend on its path.
- Comparing commits by equality rejected a healthy build. `git rev-parse
  --short` returns the shortest UNAMBIGUOUS prefix, so its width grows with
  the object database: the binary embedded `a3a1d58` and the Makefile
  produced `a3a1d586` minutes later. Now a prefix comparison in either
  direction, with a negative leg pinning that a genuinely different commit
  of the same width is still refused.

Five mutants, each verified to compile, each detected by its own leg.
Verified end to end on the real box: captured `--host 0.0.0.0`, installed
a3a1d586, restarted with that argv, both addresses answering, /proc/exe
matching the installed binary.

CONVE-2687's manual sibling-safe recipe is unchanged and remains the path
to use when another session's worktree is live; this makes the plain target
honest, it does not replace the convention.

NINE adversarial rounds ran before one returned nothing new. They found
twelve more defects, each verified in the code before being accepted:

- The probe used a fixed port while the restart preserved `--port N`.
- Only `--flag value` was parsed; Cobra accepts `--flag=value` too.
- The artifact check and the copy were separated by a window another
  session could write into, so a swap was caught only AFTER the kill. The
  copy is now STAGED beside the destination, verified there, and moved into
  place with a rename.
- `setsid` is Linux-only and would have failed on macOS after the stop.
- A host or port configured only in ~/.pad/config.toml was ignored, so such
  a user would have seen every install fail.
- A TOML inline comment (`port = 8080 # dev`) was swallowed into the value.
- $INSTALLED could be replaced between the outcome check and the exec.
- Comparing abbreviations by PREFIX accepts two different commits sharing
  seven characters -- the same mechanism that makes abbreviations grow. Ids
  are now resolved with `git rev-parse` and compared in full; an id that
  cannot be resolved is REFUSED rather than prefix-matched.
- The argv capture read /proc only, so on macOS it captured nothing and the
  restart fell back to defaults -- the fix silently absent on the platform
  the setsid fallback had just been added for.
- A wildcard bind whose LAN address could not be determined was reported as
  a success having verified only loopback. It now FAILS CLOSED.
- `localhost` was mapped to 127.0.0.1 unconditionally, failing a healthy
  IPv6 bind.
- `local` was used at top level, so bash printed an error to stderr on every
  normal refresh while the script kept working.
- More than one running server is now REFUSED rather than guessed: the stop
  is a system-wide pkill and the restart can only restore one argv.

Twenty mutants, each verified to compile, each detected by its own leg.
SEVEN survived first -- the destination check, the `--flag=value` parse, the
setsid guard, the /proc fallback, the wildcard loopback probe, the
prefix-collision refusal, and the multi-server refusal. Every one survived
for the same reason: the fixtures were drawn from my own model of the input
(equal-width commits, one flag spelling, one bind shape, a machine with
/proc), so they tested the model rather than the input. Two of them are
unreachable on this platform by construction and are reached through
documented PAD_NO_SETSID / PAD_NO_PROC knobs, because an untestable branch
is how the macOS half would have shipped broken twice.

One residual is named rather than closed: a concurrent `make install` can
still swap the binary between the final verification and the exec. The
complete answer is an exclusive lock across the whole sequence; flock is
Linux-only and a lock that silently does nothing on macOS is worse than a
named gap. Filed as IDEA-2925.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR
2026-09-07 11:48:58 -04:00