Skip to content

T-E3S6-e2e-server-detects-stale-binary

State: open/ready · Impact: medium · Complexity: small

AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-09-27. Review the Goal, Approach, Today, Areas, and Acceptance-criteria carefully before trusting it.

A cargo build from a second worktree sharing the same target/ dir can leave a stale platform-server binary that a later cargo run does not relink, so the e2e suite silently runs against old server code instead of the current worktree’s changes. Found while working T-50QX-polish-rule-options-with-no-default on sksizer/dev.

Every Playwright fixture server that exercises determined-platform’s real /api boots the same way: solutions/determined/tests/ui/serve-smoke.ts, serve-notes.ts, serve-vaults.ts, serve-fitness.ts, serve-polish.ts, and serve-polish-lint.ts each spawn cargo run -p determined-platform --bin platform-server -- --port <N> --dist <built SPA> --vault <tmp> --state-dir <tmp> with stdio: 'inherit', and trust whatever cargo run hands back. None of them independently confirms that the binary cargo starts (target/debug/platform-server, linked from determined-platform and its full local dependency closure) actually reflects the invoking worktree’s current sources. That closure is bigger than solutions/determined/crates/* (which holds most of it, including determined-polish, determined-fitness, and determined-core): determined-platform’s own Cargo.toml also names local path dependencies outside that directory — packages/rust/vault-fs, packages/rust/wikilink, packages/rust/fleet, packages/rust/fleet-tauri, packages/rust/dir-aspect, packages/rust/markdown-util, packages/rust/yaml-splice, and packages/rust/atomic-write — and, deeper in the graph, packages/rust/agent-adapt, packages/rust/easy-git, packages/rust/fluent-git, packages/rust/prettier-fmt, packages/rust/slug, packages/rust/vault-transform, and even solutions/agent-pants/crates/agent-adapt-core (verified against the root workspace Cargo.toml and cargo metadata’s resolved graph for the determined-platform package). A staleness check scoped only to solutions/determined/crates/* would miss an edit to any of those and silently reproduce the exact bug this task exists to catch.

This repo does not itself configure a shared Cargo target directory: no CARGO_TARGET_DIR or [build] target-dir override exists anywhere in the tree, and target/debug/platform-server lives at each worktree’s own root by default. But cargo run’s freshness check operates on whatever target directory it is pointed at, without knowing which worktree’s absolute source tree produced the cached fingerprints already there. A developer who points CARGO_TARGET_DIR at one directory shared across several git worktrees of this 86-crate Rust workspace — a common optimization, since a from-scratch cargo build of the whole workspace is expensive to repeat per worktree — can hit exactly what T-50QX-polish-rule-options-with-no-default’s post-mortem reported: a cargo build from a second worktree against that shared target left platform-server unrelinked from the first worktree’s perspective. cargo run reported the binary fresh and executed it as-is, so the Playwright suite booted, ran, and reported green against yesterday’s server binary while solutions/determined/crates/determined-polish/src/lint/rule_options.rs (in the first worktree) had already changed.

Add one shared freshness check that every serve-*.ts script spawning platform-server runs immediately before handing control to Playwright, so a stale binary fails the e2e boot loudly — the process exits non-zero before the port ever opens — instead of silently serving old server code for the whole suite run. The check is independent of why the binary went stale (a shared CARGO_TARGET_DIR across worktrees, a failed background build, clock skew); it only asserts the observable property the Goal names: the binary on disk is not older than the sources it is supposed to contain.

  1. Add a new shared helper module, solutions/determined/tests/ui/platform-server-freshness.ts, exporting a single function (e.g. assertPlatformServerFresh(determinedRoot: string): void) that:
    • Resolves the workspace root two levels above determinedRoot (the same relative layout every serve-*.ts script already uses to compute its own determined constant) and the expected binary path, <workspaceRoot>/target/debug/platform-server (cargo run’s default, unreleased profile — none of these scripts pass --release).
    • If the binary does not exist yet, return immediately: a first-ever cargo run for this worktree is a cold build, not a staleness case.
    • Otherwise, statSync the binary’s mtimeMs and compare it against the newest mtimeMs among every .rs file, and every Cargo.toml, across determined-platform’s full local (path) dependency closure — not just solutions/determined/crates/*/src/, which misses real local deps that live outside it (see Today) — plus the workspace’s root Cargo.lock. Derive that closure by running cargo metadata --format-version 1 once, through the repo’s command seam (spawnRunner/runChecked in solutions/ontological/lib/util/command.ts — this is a one-shot exec, not the live-streaming cargo run spawn the scripts already carry, so it does not qualify for that exception): walk the resolve graph from the determined-platform node over edges whose dep_kinds include null (normal) or build, and keep every reachable package whose source is null (a workspace-local path dependency). Cache the resolved list for the process’s lifetime rather than re-running cargo metadata per serve-*.ts invocation, and walk each kept package’s directory (readdirSync(path, { recursive: true }) — no new dependency needed).
    • When a source file’s mtimeMs is newer than the binary’s, print a message naming the binary path, its mtime, and the newest offending source file, and call process.exit(1) before any cargo run or spawn call for the server itself runs.
  2. Call assertPlatformServerFresh(determined) at the top of each of solutions/determined/tests/ui/serve-smoke.ts, serve-notes.ts, serve-vaults.ts, serve-fitness.ts, serve-polish.ts, and serve-polish-lint.ts, right after each script computes its own determined constant and before it spawns cargo run -p determined-platform --bin platform-server.
  3. Leave the existing cargo run invocation in every script untouched — it still makes the real build-or-skip decision and boots the server; the new check only adds a guard in front of trusting its output blindly. A genuinely fresh binary (cargo just relinked it, or nothing changed and the existing binary already postdates every source) passes the check with no behavior change.
  4. Add solutions/determined/tests/ui/platform-server-freshness.test.ts, a unit test over the mtime-comparison logic alone (temp directories/files with controlled mtimes via utimesSync), asserting: no binary → passes; a binary newer than every source → passes; a source file newer than the binary → reports the offending path and would exit non-zero. No real cargo invocation needed for this test.
AreaNote
solutions/determined/tests/ui/platform-server-freshness.ts (new)Shared mtime-based staleness check and its exported assertion function
solutions/determined/tests/ui/serve-smoke.ts, serve-notes.ts, serve-vaults.ts, serve-fitness.ts, serve-polish.ts, serve-polish-lint.tsEach calls the new check before its existing cargo run -p determined-platform --bin platform-server spawn
solutions/determined/crates/determined-platform/src/bin/platform-server.rsThe binary the check protects; no change expected here
determined-platform’s full local dependency closure, derived via cargo metadata (not a hardcoded glob)The local crate source trees the check walks for mtimes — spans solutions/determined/crates/* (determined-polish, determined-fitness, determined-core, and others), packages/rust/* (e.g. vault-fs, wikilink, fleet), and solutions/agent-pants/crates/agent-adapt-core
  • AC-1: A shared helper (solutions/determined/tests/ui/platform-server-freshness.ts) exists that compares target/debug/platform-server’s mtime against the newest mtime among every .rs file and Cargo.toml across determined-platform’s full local dependency closure — derived via cargo metadata, not a hardcoded solutions/determined/crates/* glob; the closure also includes packages/rust/* crates such as vault-fs, wikilink, and fleet, plus solutions/agent-pants/crates/agent-adapt-core — and the root Cargo.lock.
  • AC-2: Each of serve-smoke.ts, serve-notes.ts, serve-vaults.ts, serve-fitness.ts, serve-polish.ts, and serve-polish-lint.ts calls that helper before spawning cargo run -p determined-platform --bin platform-server.
  • AC-3: With a crate source file’s mtime forced newer than the on-disk platform-server binary — simulating the shared-target-dir race from T-50QX-polish-rule-options-with-no-default’s post-mortem — running any of the six scripts named in AC-2 exits non-zero, names the stale binary and the offending source file, and never opens the server’s port.
  • AC-4: With platform-server genuinely fresh (freshly linked, or already newer than every crate source), each of the six scripts named in AC-2 boots the server exactly as it does today — no added failure on the happy path.
  • AC-5: A unit test for the mtime-comparison logic (solutions/determined/tests/ui/platform-server-freshness.test.ts) passes without invoking real cargo.
  • none
  • none

Spawned by /sdlc:spawn-task-pr on 2026-09-27 UTC from T-50QX-polish-rule-options-with-no-default in sksizer/dev.


← Back to Tasks