Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions cmd/relayfile-cli/relay_state_counter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"sync"
"testing"
)
Comment on lines +3 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '^\s*go\s+[0-9.]+' go.mod
rg -n -C2 't\.Context\(' cmd/relayfile-cli/relay_state_counter_test.go

Repository: AgentWorkforce/relayfile

Length of output: 263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test file outline ---'
ast-grep outline cmd/relayfile-cli/relay_state_counter_test.go

printf '%s\n' '--- test file ---'
cat -n cmd/relayfile-cli/relay_state_counter_test.go

printf '%s\n' '--- module and workspace settings ---'
cat -n go.mod
if [ -f go.work ]; then cat -n go.work; fi

printf '%s\n' '--- related test and API usage ---'
rg -n -C3 'TestFailedWritebacks|FlushOutboxOnce|t\.Context\(' --glob '*.go' .

Repository: AgentWorkforce/relayfile

Length of output: 24599


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path("cmd/relayfile-cli/relay_state_counter_test.go")
text = p.read_text()
print("t.Context call count:", len(re.findall(r'\bt\.Context\(\)', text)))
print("context import present:", bool(re.search(r'(?m)^\s*"context"\s*$', text)))
print("goroutine launches:", len(re.findall(r'\bgo\s+func\s*\(', text)))
print("explicit cancel calls:", len(re.findall(r'\bcancel\s*\(\)', text)))
PY

Repository: AgentWorkforce/relayfile

Length of output: 263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- helper and writer definitions ---'
rg -n -C5 'func (incrementFailedWritebacksInState|readPersistedFailedWritebacks|newPublicStateWriter)|func \(s \*Syncer\) FlushOutboxOnce|func .*savePublicState' \
  cmd/relayfile-cli internal/mountsync --glob '*.go'

printf '%s\n' '--- test gating and cleanup patterns ---'
rg -n -C3 'TestFailedWritebacksLostUpdateUnderConcurrency|testing.Short|RUN.*Test|Skip\(' \
  cmd/relayfile-cli .github Makefile* --glob '*.go' --glob '*.yml' --glob '*.yaml' --glob 'Makefile*' 2>/dev/null || true

Repository: AgentWorkforce/relayfile

Length of output: 5773


Use a Go 1.22-compatible context and isolate the concurrency probe.

  • testing.T.Context() is unavailable with go 1.22. Create a test-owned context and pass it to FlushOutboxOnce.
  • Gate TestFailedWritebacksLostUpdateUnderConcurrency behind explicit opt-in because its unsynchronized timing can produce nondeterministic results.
  • Ensure cleanup cancels the context, closes stop, and waits for the worker. t.Fatalf at line 78 exits before the current shutdown code runs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/relayfile-cli/relay_state_counter_test.go` around lines 3 - 6, Update
TestFailedWritebacksLostUpdateUnderConcurrency to create a Go 1.22-compatible
test-owned context and pass it to FlushOutboxOnce, and require an explicit
opt-in before running this nondeterministic concurrency probe. Register cleanup
immediately to cancel the context, close stop, and wait for the worker, ensuring
cleanup still runs when t.Fatalf exits early.

Source: Linters/SAST tools


// TestFailedWritebacksSurvivesAlternation checks the specific claim that the
// failedWritebacks counter is zeroed when the two writers alternate.
//
// It is NOT zeroed. Both writers deliberately read the counter back before
// writing it: mountsync at internal/mountsync/syncer.go:7020
// (readPublicFailedWritebacks) and the CLI mirror at
// cmd/relayfile-cli/main.go:10986 (max against the persisted value). This test
// documents that hardening so a future refactor does not remove it.
func TestFailedWritebacksSurvivesAlternation(t *testing.T) {
localDir := t.TempDir()
syncer := newPublicStateWriter(t, localDir)

for i := 0; i < 5; i++ {
if err := incrementFailedWritebacksInState(localDir); err != nil {
t.Fatalf("increment %d: %v", i, err)
}
}
if got := readPersistedFailedWritebacks(localDir); got != 5 {
t.Fatalf("precondition: counter = %d, want 5", got)
}

// Alternate the two writers several times.
for i := 0; i < 3; i++ {
writeMountsyncPublicState(t, syncer)
if got := readPersistedFailedWritebacks(localDir); got != 5 {
t.Errorf("after mountsync write %d: counter = %d, want 5 (mountsync dropped the counter)", i, got)
}
writeCLIMirrorState(t, localDir)
if got := readPersistedFailedWritebacks(localDir); got != 5 {
t.Errorf("after CLI mirror write %d: counter = %d, want 5 (CLI mirror dropped the counter)", i, got)
}
}
}

// TestFailedWritebacksLostUpdateUnderConcurrency probes the real hazard: both
// writers preserve the counter via read-modify-write, but neither holds a lock
// the other respects, so an increment landing between mountsync's read
// (syncer.go:7020) and its write (syncer.go:7200) is silently overwritten with
// the stale value.
//
// failedWritebacksStateMu (cmd/relayfile-cli/main.go) serializes the CLI-side
// increments against each other, but mountsync lives in another package and
// cannot take it. The mutex looks synchronized and is not.
//
// N increments must yield a counter of N.
func TestFailedWritebacksLostUpdateUnderConcurrency(t *testing.T) {
Comment on lines +42 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -i -C2 'go\s+test|relay_state_counter|RELAYFILE_RUN_FLAKY_STATE_RACE' .github Makefile 2>/dev/null || true

Repository: AgentWorkforce/relayfile

Length of output: 1229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- module Go version ---'
sed -n '1,40p' go.mod

printf '%s\n' '--- test file outline ---'
ast-grep outline cmd/relayfile-cli/relay_state_counter_test.go 2>/dev/null || true

printf '%s\n' '--- relevant test source ---'
cat -n cmd/relayfile-cli/relay_state_counter_test.go | sed -n '1,180p'

printf '%s\n' '--- test and CI references ---'
rg -n -i -C3 'go test|relay_state_counter|TestFailedWritebacksLostUpdateUnderConcurrency|RELAYFILE_RUN_FLAKY_STATE_RACE|t\.Context\(\)' \
  --glob '!vendor/**' --glob '!node_modules/**' .

printf '%s\n' '--- Go tool version, if available ---'
go version 2>/dev/null || true

Repository: AgentWorkforce/relayfile

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant package test declarations and imports ---'
rg -n -C8 '^func Test|^import \(' cmd/relayfile-cli/relay_state_counter_test.go

printf '%s\n' '--- worker lifecycle and fatal paths ---'
rg -n -C10 'WaitGroup|go func|Fatalf|Errorf|context\.|cancel|Done\(' cmd/relayfile-cli/relay_state_counter_test.go

printf '%s\n' '--- repository test targets and workflow commands ---'
rg -n -i -C5 'go test|test:' Makefile .github --glob '*.yml' --glob '*.yaml' 2>/dev/null || true

Repository: AgentWorkforce/relayfile

Length of output: 7412


🌐 Web query:

Official Go documentation: which Go release added testing.T.Context, and what is the Go 1.22 testing.T API?

💡 Result:

The testing.T.Context method was added in Go 1.24.0 [1][2][3]. Regarding the Go 1.22 testing.T API, it did not include the Context method. The testing.T API in Go 1.22 consisted of the following primary methods: ArtifactDir (added in later versions) Cleanup Error Errorf Fail FailNow Failed Fatal Fatalf Helper Log Logf Name Parallel Run Setenv Skip SkipNow Skipf Skipped TempDir Deadline Please note that the exact composition of the API can vary based on the minor release (such as the addition of Chdir or Setenv in earlier versions), but Context was definitively not part of the standard library testing package until the Go 1.24 release [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- helper implementations used by the concurrent probe ---'
rg -n -C12 'func (newPublicStateWriter|incrementFailedWritebacksInState|readPersistedFailedWritebacks|writeMountsyncPublicState|writeCLIMirrorState)' cmd/relayfile-cli --glob '*.go'

printf '%s\n' '--- all direct uses of the test context API and Go version settings ---'
rg -n -C3 't\.Context\(\)|go-version|GO_VERSION|^go [0-9]' \
  go.mod .github Makefile cmd/relayfile-cli --glob '*.go' --glob '*.yml' --glob '*.yaml' 2>/dev/null

Repository: AgentWorkforce/relayfile

Length of output: 11416


Gate the probe and make it compile with Go 1.22.

go test ./..., the Makefile test target, and both CI workflows discover this test. Add an opt-in environment guard before starting the worker. Replace t.Context() because testing.T.Context was added in Go 1.24, while CI uses Go 1.22. Ensure worker shutdown runs when t.Fatalf exits the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/relayfile-cli/relay_state_counter_test.go` around lines 42 - 53, Update
TestFailedWritebacksLostUpdateUnderConcurrency to return immediately unless its
opt-in environment variable is enabled, before starting the worker; replace
t.Context() with a Go 1.22-compatible context and ensure worker shutdown is
deferred before any t.Fatalf path can exit the test.

const increments = 200

localDir := t.TempDir()
syncer := newPublicStateWriter(t, localDir)

var wg sync.WaitGroup
stop := make(chan struct{})

// mountsync republishing public state, as the live daemon does ~1x/sec.
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = syncer.FlushOutboxOnce(t.Context())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Synchronize the lost-update interleaving

The background flush is not synchronized with any point in the increment loop, so this test depends entirely on scheduler timing: if the goroutine only flushes before the increments or the increment loop completes after its final write, the counter remains 200 and the test passes; if a stale flush lands during the loop, it fails. Since this runs in the default Go suite, the same code can therefore produce intermittent CI outcomes; use barriers or a controllable writer hook to force the read-before-increment/write-after-increment sequence.

Useful? React with 👍 / 👎.

}
}
}()

for i := 0; i < increments; i++ {
if err := incrementFailedWritebacksInState(localDir); err != nil {
t.Fatalf("increment %d: %v", i, err)
}
}

close(stop)
wg.Wait()
Comment on lines +63 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Always stop the worker after a fatal test exit.

If incrementFailedWritebacksInState fails at Line 77, t.Fatalf exits before Lines 82-83 run. The mountsync worker then continues to write state. Register cleanup immediately after starting the worker.

Proposed fix
 	go func() {
 		defer wg.Done()
 		for {
@@
 		}
 	}()
+	defer func() {
+		close(stop)
+		wg.Wait()
+	}()
 
 	for i := 0; i < increments; i++ {
@@
-	close(stop)
-	wg.Wait()
-
 	if got := readPersistedFailedWritebacks(localDir); got != increments {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = syncer.FlushOutboxOnce(t.Context())
}
}
}()
for i := 0; i < increments; i++ {
if err := incrementFailedWritebacksInState(localDir); err != nil {
t.Fatalf("increment %d: %v", i, err)
}
}
close(stop)
wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = syncer.FlushOutboxOnce(t.Context())
}
}
}()
defer func() {
close(stop)
wg.Wait()
}()
for i := 0; i < increments; i++ {
if err := incrementFailedWritebacksInState(localDir); err != nil {
t.Fatalf("increment %d: %v", i, err)
}
}
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 71-71: stdversion: testing.Context requires go1.24 or later (module is go1.22)

(govet)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/relayfile-cli/relay_state_counter_test.go` around lines 63 - 83, Register
test cleanup immediately after starting the worker goroutine, before the
increment loop in the relay state counter test, so a t.Fatalf from
incrementFailedWritebacksInState still signals stop and waits for the worker to
exit. Reuse the existing stop channel and sync.WaitGroup cleanup behavior, while
preserving the normal close-and-wait path without duplicating cleanup.


if got := readPersistedFailedWritebacks(localDir); got != increments {
t.Errorf("failedWritebacks = %d after %d increments: %d writeback failures were silently lost. "+
"failedWritebacksStateMu (cmd/relayfile-cli) does not cross into internal/mountsync, so "+
"savePublicState's read at syncer.go:7020 / write at syncer.go:7200 overwrites increments "+
"that land in between",
got, increments, increments-got)
}
}
209 changes: 209 additions & 0 deletions cmd/relayfile-cli/relay_state_two_writers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package main

import (
"context"
"encoding/json"
"net/http"
"os"
"path/filepath"
"testing"
"time"

"github.com/agentworkforce/relayfile/internal/mountsync"
)

// newPublicStateWriter builds a real mountsync.Syncer rooted at localDir.
// Its LocalRoot is the same directory the CLI mirror writer is handed in
// cmd/relayfile-cli/main.go:6852-6892, so both writers derive the identical
// <localDir>/.relay/state.json path — that shared derivation is the defect.
//
// No network call is made: FlushOutboxOnce with an empty outbox goes straight
// to markSyncSuccess -> saveStateWithoutLocalScan -> savePublicState.
func newPublicStateWriter(t *testing.T, localDir string) *mountsync.Syncer {
t.Helper()
client := mountsync.NewHTTPClient("http://127.0.0.1:1", "test-token", &http.Client{
Timeout: time.Second,
})
websocketDisabled := false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The variable websocketDisabled := false is passed as WebSocket: &websocketDisabled, but NewSyncer reads websocketEnabled = *opts.WebSocket, so the value false actually DISABLES the websocket — the name reads as "websocket is not disabled / enabled", contradicting its effect (and the disableWebSocket := false convention in internal/mountsync/syncer_test.go where true means disabled). The field is inert in these tests (FlushOutboxOnce never dials), so there's no behavioral bug, but the inverted naming will mislead a future reader about whether the websocket path is on.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/relayfile-cli/relay_state_two_writers_test.go, line 27:

<comment>The variable `websocketDisabled := false` is passed as `WebSocket: &websocketDisabled`, but NewSyncer reads `websocketEnabled = *opts.WebSocket`, so the value `false` actually DISABLES the websocket — the name reads as "websocket is not disabled / enabled", contradicting its effect (and the `disableWebSocket := false` convention in internal/mountsync/syncer_test.go where `true` means disabled). The field is inert in these tests (FlushOutboxOnce never dials), so there's no behavioral bug, but the inverted naming will mislead a future reader about whether the websocket path is on.</comment>

<file context>
@@ -0,0 +1,209 @@
+	client := mountsync.NewHTTPClient("http://127.0.0.1:1", "test-token", &http.Client{
+		Timeout: time.Second,
+	})
+	websocketDisabled := false
+	syncer, err := mountsync.NewSyncer(client, mountsync.SyncerOptions{
+		WorkspaceID: "rw_twowriters",
</file context>

syncer, err := mountsync.NewSyncer(client, mountsync.SyncerOptions{
WorkspaceID: "rw_twowriters",
RemoteRoot: "/linear",
LocalRoot: localDir,
WebSocket: &websocketDisabled,
RootCtx: context.Background(),
})
if err != nil {
t.Fatalf("NewSyncer: %v", err)
}
return syncer
}

// writeMountsyncPublicState drives WRITER 1 (internal/mountsync/syncer.go:7200,
// path derived at syncer.go:1608).
func writeMountsyncPublicState(t *testing.T, syncer *mountsync.Syncer) {
t.Helper()
if err := syncer.FlushOutboxOnce(context.Background()); err != nil {
t.Fatalf("FlushOutboxOnce (writer 1): %v", err)
}
}

// writeCLIMirrorState drives WRITER 2 (cmd/relayfile-cli/main.go:10998), with a
// snapshot shaped like the one buildSyncStateSnapshot produces for a live
// mount: providers from the cloud feed, a daemon block, guards, a stall reason.
func writeCLIMirrorState(t *testing.T, localDir string) {
t.Helper()
snapshot := syncStateFile{
WorkspaceID: "rw_twowriters",
RemoteRoot: "/linear",
Mode: defaultMountMode,
Status: "ready",
// Provider-feed clock. Deliberately old: this is the field an
// operator misread as "the feed moved".
LastEventAt: "2026-08-03T07:26:26.334Z",
IntervalMs: 30000,
Providers: []syncStateProvider{
{Provider: "linear", Status: "ready", LastEventAt: "2026-08-03T07:26:26.334Z"},
},
StallReason: "provider feed frozen",
Daemon: &syncStateDaemon{PID: 63173},
Guards: &syncStateGuards{
CircuitOpenEvents: 7,
TombstonesConfirmed: 3,
},
}
if err := writeMirrorStateFile(localDir, snapshot); err != nil {
t.Fatalf("writeMirrorStateFile (writer 2): %v", err)
}
}

func readStateDocument(t *testing.T, localDir string) map[string]any {
t.Helper()
payload, err := os.ReadFile(filepath.Join(localDir, ".relay", "state.json"))
if err != nil {
t.Fatalf("read .relay/state.json: %v", err)
}
document := map[string]any{}
if err := json.Unmarshal(payload, &document); err != nil {
t.Fatalf("unmarshal .relay/state.json: %v", err)
}
return document
}

// TestRelayStateJSONHasExactlyOneWriter is the contract this file exists to
// pin: <localDir>/.relay/state.json must be a single document with a single
// owner. Today two writers in the same binary emit two disjoint schemas to
// that one path, so whichever wrote last defines what every consumer sees.
//
// The test drives writer 1 then writer 2 then writer 1 again against one
// localDir and asserts, after each write, that the document still satisfies
// BOTH consumer contracts. It fails on HEAD at the first assertion.
func TestRelayStateJSONHasExactlyOneWriter(t *testing.T) {
localDir := t.TempDir()
syncer := newPublicStateWriter(t, localDir)

// --- writer 1 (mountsync) writes first ---------------------------------
writeMountsyncPublicState(t, syncer)
afterWriter1 := readStateDocument(t, localDir)
if _, ok := afterWriter1["localRoot"]; !ok {
t.Fatalf("precondition failed: writer 1 did not emit its own document; keys=%v", sortedKeys(afterWriter1))
}

// --- writer 2 (CLI mirror) writes second --------------------------------
writeCLIMirrorState(t, localDir)
afterWriter2 := readStateDocument(t, localDir)

// Writer 1's fields must survive writer 2. They do not: writeMirrorStateFile
// marshals a syncStateFile from scratch, so every publicState-only field is
// dropped from the file on disk.
for _, field := range []string{"localRoot", "states", "counters", "files", "outbox", "credExpiresInSecs"} {
if _, ok := afterWriter2[field]; !ok {
t.Errorf("writer 2 clobbered writer 1: field %q is absent from .relay/state.json after the CLI mirror write (keys now: %v)",
Comment on lines +118 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the default test suite from failing unconditionally

This assertion is guaranteed to fail with the implementation under test: writeMirrorStateFile serializes syncStateFile, which has neither localRoot nor states, so at least those iterations always call t.Errorf. Because .github/workflows/ci.yml runs go test ./..., the commit cannot produce a green test run even when the existing behavior is unchanged; quarantine this red contract test or include the production fix that makes the contract pass.

Useful? React with 👍 / 👎.

field, sortedKeys(afterWriter2))
}
}

// --- writer 1 writes again (this is what happens ~1x/second live) -------
writeMountsyncPublicState(t, syncer)
afterWriter1Again := readStateDocument(t, localDir)

// Writer 2's fields must survive writer 1. They do not.
for _, field := range []string{"providers", "daemon", "guards", "stallReason"} {
if _, ok := afterWriter1Again[field]; !ok {
t.Errorf("writer 1 clobbered writer 2: field %q is absent from .relay/state.json after the mountsync write (keys now: %v)",
field, sortedKeys(afterWriter1Again))
}
}

// The production readers in this package must keep working across both
// writes. They do not: each reads a field only one writer emits.
// readGuardCounters is the subtle one. It does not return nil here — it
// falls through to writer 1's `counters`/`circuit` block and returns a
// *different* guards document than the one writer 2 persisted. The CLI
// surface silently swaps its data source depending on who wrote last.
guards := readGuardCounters(localDir)
if guards == nil {
t.Errorf("readGuardCounters (main.go:10912) returned nil after a mountsync write")
} else {
if guards.CircuitOpenEvents != 7 {
t.Errorf("readGuardCounters (main.go:10912) circuitOpenEvents = %d, want 7 — it silently switched from writer 2's persisted `guards` block to writer 1's `counters` block",
guards.CircuitOpenEvents)
}
if guards.TombstonesConfirmed != 3 {
t.Errorf("readGuardCounters (main.go:10912) tombstonesConfirmed = %d, want 3 — writer 2's guard telemetry is gone from the file",
guards.TombstonesConfirmed)
}
}
if reason := readPersistedStallReason(localDir); reason != "provider feed frozen" {
t.Errorf("readPersistedStallReason (main.go:8556) = %q, want %q — the stall reason is erased every time writer 1 wins the race",
reason, "provider feed frozen")
}
state, err := readWritebackState(localDir)
if err != nil {
t.Fatalf("readWritebackState: %v", err)
}
if len(state.Providers) == 0 {
t.Errorf("readWritebackState (main.go:5487) sees 0 providers — the TypeScript SDK readiness check (packages/sdk/typescript/src/mount-launcher.ts:355) treats an empty providers list as ready, so this races into a false ready")
}
}

// TestRelayStateJSONLastEventAtHasOneMeaning pins the field that produced the
// real-world misreport. Both writers emit `lastEventAt`, with different
// semantics: writer 1 means "mountsync's own event clock" and writer 2 means
// "the cloud provider feed". A consumer polling this one field gets a value
// whose meaning changes between reads.
func TestRelayStateJSONLastEventAtHasOneMeaning(t *testing.T) {
localDir := t.TempDir()
syncer := newPublicStateWriter(t, localDir)

writeCLIMirrorState(t, localDir)
fromProviderFeed, _ := readStateDocument(t, localDir)["lastEventAt"].(string)
if fromProviderFeed != "2026-08-03T07:26:26.334Z" {
t.Fatalf("precondition failed: writer 2 lastEventAt = %q", fromProviderFeed)
}

writeMountsyncPublicState(t, syncer)
afterMountsync := readStateDocument(t, localDir)
fromMountsync, present := afterMountsync["lastEventAt"].(string)

if !present {
t.Errorf("lastEventAt vanished from .relay/state.json after a mountsync write: a consumer polling this field sees the provider-feed timestamp %q disappear and reappear as the mount loop and the CLI mirror alternate",
fromProviderFeed)
}
if present && fromMountsync != fromProviderFeed {
t.Errorf("lastEventAt changed meaning without changing name: %q (cloud provider feed, writer 2) then %q (mountsync event clock, writer 1)",
fromProviderFeed, fromMountsync)
}
}

func sortedKeys(document map[string]any) []string {
keys := make([]string, 0, len(document))
for key := range document {
keys = append(keys, key)
}
for i := 1; i < len(keys); i++ {
for j := i; j > 0 && keys[j] < keys[j-1]; j-- {
keys[j], keys[j-1] = keys[j-1], keys[j]
}
}
return keys
}
74 changes: 74 additions & 0 deletions docs/evidence/mount-latency-20260807/CLEANUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Cleanup for the 2026-08-07 mount latency run

Everything this run created is disposable. Nothing here touches the
pre-existing `.dev-collab-stack/` or `.salvaged-from-minis/` directories,
their processes, their ports, or the existing sf-mini mounts — those were
deliberately left alone and must stay that way.

## What this run started

**On the sender host (`khaliqs-macbook-pro`, Tailscale `100.89.219.17`)**

| Thing | Where |
|---|---|
| `relayfile-server` | bound to `100.89.219.17:18299` (Tailscale address only, not `0.0.0.0`) |
| `dev-authd.py serve` (JWKS) | `127.0.0.1:19091`, loopback only |
| Server state file | `<scratch>/latency-run/state/state.json` — outside the repo |
| Throwaway RSA private key + minted tokens | `<scratch>/latency-run/keys/` — outside the repo, mode 0600 |

**On the receiver host (`sf-mac-mini`, Tailscale `100.102.30.76`)**

| Thing | Where |
|---|---|
| `relayfile-cli mount ws_latency_20260807` | mirror at `~/relayfile-latency-mount-20260807` |
| `receiver-watch.py` | writing `~/.relayfile-latency-harness/raw/` |
| `clock-offset.py server` | port `19299` |
| Deployed harness + receiver token | `~/.relayfile-latency-harness/` |

## Teardown

Receiver:

```sh
ssh sf-mini '
pkill -f "relayfile-cli mount ws_latency_20260807"
pkill -f receiver-watch.py
pkill -f "clock-offset.py server"
rm -rf ~/relayfile-latency-mount-20260807

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The receiver teardown runs rm -rf ~/relayfile-latency-mount-20260807 immediately after pkill without confirming the FUSE mount process actually exited and that the mount table no longer lists the path, so a slow-to-exit or respawned process can leave a stale mounted path alongside a partially deleted run directory. Wait for the exact process to exit and check the mount table before removing the run directory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/evidence/mount-latency-20260807/CLEANUP.md, line 37:

<comment>The receiver teardown runs `rm -rf ~/relayfile-latency-mount-20260807` immediately after `pkill` without confirming the FUSE mount process actually exited and that the mount table no longer lists the path, so a slow-to-exit or respawned process can leave a stale mounted path alongside a partially deleted run directory. Wait for the exact process to exit and check the mount table before removing the run directory.</comment>

<file context>
@@ -0,0 +1,74 @@
+  pkill -f "relayfile-cli mount ws_latency_20260807"
+  pkill -f receiver-watch.py
+  pkill -f "clock-offset.py server"
+  rm -rf ~/relayfile-latency-mount-20260807
+  rm -rf ~/.relayfile-latency-harness
+'
</file context>

rm -rf ~/.relayfile-latency-harness
'
```

Sender:

```sh
pkill -f "bin/relayfile-server"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The sender teardown kills by broad process name (pkill -f "bin/relayfile-server", pkill -f "dev-authd.py serve"), which also matches the pre-existing dev-collab server that this very doc says must stay untouched (lsof -nP -iTCP:8299 ... expected untouched, port 8299 on the sender). Since bin/relayfile-server is exactly this repo's server binary name (Makefile SERVER_BIN := relayfile-server, built to bin/), a pre-existing dev-collab deployment on the same host matches the pattern and would be killed along with the run's own server. Scope the sender teardown by the run's recorded PID(s) or by port 18299 (e.g. lsof -tiTCP:18299 | xargs kill) the way the receiver side already scopes on the full workspace name.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/evidence/mount-latency-20260807/CLEANUP.md, line 45:

<comment>The sender teardown kills by broad process name (`pkill -f "bin/relayfile-server"`, `pkill -f "dev-authd.py serve"`), which also matches the pre-existing dev-collab server that this very doc says must stay untouched (`lsof -nP -iTCP:8299 ... expected untouched`, port 8299 on the sender). Since `bin/relayfile-server` is exactly this repo's server binary name (Makefile `SERVER_BIN := relayfile-server`, built to `bin/`), a pre-existing dev-collab deployment on the same host matches the pattern and would be killed along with the run's own server. Scope the sender teardown by the run's recorded PID(s) or by port 18299 (e.g. `lsof -tiTCP:18299 | xargs kill`) the way the receiver side already scopes on the full workspace name.</comment>

<file context>
@@ -0,0 +1,74 @@
+Sender:
+
+```sh
+pkill -f "bin/relayfile-server"
+pkill -f "dev-authd.py serve"
+rm -rf <scratch>/latency-run
</file context>

pkill -f "dev-authd.py serve"
rm -rf <scratch>/latency-run
Comment on lines +33 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Scope teardown commands to this measurement run.

pkill -f receiver-watch.py, pkill -f "clock-offset.py server", pkill -f "bin/relayfile-server", and pkill -f "dev-authd.py serve" can terminate unrelated processes.

Record each started process ID and terminate that ID during cleanup. Alternatively, match a unique run-specific argument such as the latency-run state directory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/evidence/mount-latency-20260807/CLEANUP.md` around lines 33 - 47, Update
the cleanup commands in the measurement-run documentation to avoid broad pkill
patterns: record each process ID when starting the receiver watcher,
clock-offset server, relayfile server, and dev-auth daemon, then terminate those
specific IDs, or constrain matching to the run-specific latency state directory.
Preserve cleanup of only this run’s scratch resources.

```

`pkill -f "relayfile-cli mount ws_latency_20260807"` is deliberately matched on
the full workspace name. sf-mini also runs unrelated pre-existing mounts
(`relayfile-dev-collab`, `relay-dev-collab`); a looser pattern would kill them.

## Verifying nothing else was disturbed

```sh
lsof -nP -iTCP:8299 -sTCP:LISTEN # dev-collab server port: expected untouched
ssh sf-mini 'pgrep -fl "relayfile-cli.*dev-collab"' # pre-existing mounts still up
git -C <repo> status --short # .dev-collab-stack/ and .salvaged-from-minis/ still untracked, unmodified

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The verification step claims .dev-collab-stack/ and .salvaged-from-minis/ are "still untracked, unmodified", but git status --short only proves the dirs remain untracked (??); it cannot detect content changes inside untracked directories, so "unmodified" is unverified. Either take a before/after content snapshot or hash, or reword to "still untracked" and drop the unchanged-content claim (the harness assertion in assertions.py already limits itself to the untracked status).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/evidence/mount-latency-20260807/CLEANUP.md, line 59:

<comment>The verification step claims `.dev-collab-stack/` and `.salvaged-from-minis/` are "still untracked, unmodified", but `git status --short` only proves the dirs remain untracked (`??`); it cannot detect content changes inside untracked directories, so "unmodified" is unverified. Either take a before/after content snapshot or hash, or reword to "still untracked" and drop the unchanged-content claim (the harness assertion in assertions.py already limits itself to the untracked status).</comment>

<file context>
@@ -0,0 +1,74 @@
+```sh
+lsof -nP -iTCP:8299 -sTCP:LISTEN     # dev-collab server port: expected untouched
+ssh sf-mini 'pgrep -fl "relayfile-cli.*dev-collab"'   # pre-existing mounts still up
+git -C <repo> status --short         # .dev-collab-stack/ and .salvaged-from-minis/ still untracked, unmodified
+```
+
</file context>

```

## Credentials

The RSA key and the bearer tokens minted for this run are throwaway, scoped to
workspace `ws_latency_20260807`, short-lived, and were never written into any
committed artifact or sent over Relay. Deleting the scratch directory and
`~/.relayfile-latency-harness` on the receiver destroys them.

Separately, and unrelated to this run: a routine `ps` on sf-mini exposes live
`RELAY_API_KEY` and agent-token values in broker process argv, because they are
passed as command-line arguments. Those are pre-existing production credentials,
readable by any local process, and were reported for rotation. This run
deliberately passed its own receiver token via the `RELAYFILE_TOKEN` environment
variable rather than `--token` so as not to add to that exposure.
Loading