diff --git a/README.md b/README.md index f45a724d..dd7af6fe 100644 --- a/README.md +++ b/README.md @@ -238,8 +238,10 @@ registered mirror) rather than accepting a partial reset. The exported TypeScript mount launcher currently models one `remotePath` and rejects `RELAYFILE_MOUNT_PATHS_FILE` before creating directories or spawning a -process. Use the `relayfile` CLI directly for multi-path mounts until the -TypeScript session and status types represent multiple roots. +process. The `relayfile` CLI is also single-root while scoped mount operator +surfaces are unavailable, so configure one remote path only. Multi-root +configuration will return when the TypeScript session and CLI operator surfaces +represent every scoped child root. The FUSE layer caches file content in kernel memory independently of its attribute TTL. By default content is held for 30 seconds and attributes for 2 seconds. Tune with `--fuse-content-ttl` (or `RELAYFILE_MOUNT_FUSE_CONTENT_TTL`) if your workload needs fresher reads or can tolerate a longer cache window: diff --git a/cmd/relayfile-cli/main_test.go b/cmd/relayfile-cli/main_test.go index 71b4e9c2..3f6c638e 100644 --- a/cmd/relayfile-cli/main_test.go +++ b/cmd/relayfile-cli/main_test.go @@ -93,6 +93,40 @@ func TestResolveCLIRequestedLocalLayoutRefusesAllScopedRoutes(t *testing.T) { } } +func TestMountRefusesRepeatedRemotePathsWithScopedLayoutBeforeSideEffects(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + clearRelayfileEnv(t) + + localRoot := filepath.Join(t.TempDir(), "mirror") + var requests atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests.Add(1) + http.Error(w, "scoped mounts must be rejected before contacting the server", http.StatusInternalServerError) + })) + defer server.Close() + + err := run([]string{ + "mount", "ws_demo", localRoot, + "--server", server.URL, + "--token", testJWTWithWorkspace("ws_demo"), + "--remote-path", "/github", + "--remote-path", "/slack", + "--local-layout", "scoped", + "--once", + }, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}) + if err == nil || + !strings.Contains(err.Error(), "--local-layout=scoped") || + !strings.Contains(err.Error(), "temporarily unavailable") { + t.Fatalf("expected scoped-layout refusal for repeated remote paths, got %v", err) + } + if got := requests.Load(); got != 0 { + t.Fatalf("scoped multi-path mount contacted server %d times before refusal", got) + } + if _, statErr := os.Stat(localRoot); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("scoped multi-path mount initialized mirror before refusal: %v", statErr) + } +} + // skipUntilScopedOperatorSurfacesReady keeps the scoped-runtime coverage // dormant only while the CLI guard is active. When the guard is removed, the // helper stops skipping these tests automatically; if the guard is deleted