replay ui fix (#56)

* refactor: rename inspect to replay across the codebase

Renames inspect-ui/ to replay-ui/, internal/inspect/ to internal/replay/,
the CLI subcommand from `sanderling inspect` to `sanderling replay`, and
updates all references in docs, Makefile, README, and Go comments.

* feat(replay-ui): show spec filename with full path on hover

RunList and RunDetail now render the basename of spec_path (e.g.
login.spec.ts) with the full path available as a title tooltip.
This commit is contained in:
pj authored and GitHub committed 2026-06-03 16:17:26 +05:30
1 parent a6f43e15b2
commit b44077afde
71 files changed
+104 -92

No files matched your search

+9 -7
View File
@@ -38,15 +38,15 @@ pkg/spec/dist/
# goreleaser local output # goreleaser local output
/dist/ /dist/
# inspect web bundle output; built by `make web-build` before go build. # replay web bundle output; built by `make web-build` before go build.
# Only .gitkeep is tracked so //go:embed all:dist compiles on a fresh checkout. # Only .gitkeep is tracked so //go:embed all:dist compiles on a fresh checkout.
/internal/inspect/dist/* /internal/replay/dist/*
!/internal/inspect/dist/.gitkeep !/internal/replay/dist/.gitkeep
# inspect web frontend # replay web frontend
inspect-ui/node_modules/ replay-ui/node_modules/
inspect-ui/dist/ replay-ui/dist/
inspect-ui/.vite/ replay-ui/.vite/
# folio-web frontend # folio-web frontend
examples/folio-web/node_modules/ examples/folio-web/node_modules/
@@ -59,3 +59,5 @@ examples/folio-web/.vite/
# Personal research notes # Personal research notes
/research/ /research/
/talk/
+11 -11
View File
@@ -19,10 +19,10 @@ PAGE_OUT := $(patsubst docs/%.md,build/site/%/index.html,$(PAGE_SRC))
DOCS_OUT := $(INDEX_OUT) $(PAGE_OUT) DOCS_OUT := $(INDEX_OUT) $(PAGE_OUT)
DOCS_TEMPLATE := docs/_template/page.html DOCS_TEMPLATE := docs/_template/page.html
INSPECT_DIST := internal/inspect/dist REPLAY_DIST := internal/replay/dist
WEB_DIST := inspect-ui/dist WEB_DIST := replay-ui/dist
.PHONY: bootstrap proto sidecar sanderling install test test-go test-browser test-kotlin test-spec-api web-typecheck web-build web-dev inspect-dev docs clean release-cli release-npm-dry .PHONY: bootstrap proto sidecar sanderling install test test-go test-browser test-kotlin test-spec-api web-typecheck web-build web-dev replay-dev docs clean release-cli release-npm-dry
bootstrap: bootstrap:
$(GO) mod download $(GO) mod download
@@ -48,19 +48,19 @@ install: $(SIDECAR_EMBED) web-build
@dest="$$($(GO) env GOBIN)"; [ -n "$$dest" ] || dest="$$($(GO) env GOPATH)/bin"; echo "installed sanderling to $$dest" @dest="$$($(GO) env GOBIN)"; [ -n "$$dest" ] || dest="$$($(GO) env GOPATH)/bin"; echo "installed sanderling to $$dest"
web-build: web-build:
cd inspect-ui && bun install --frozen-lockfile && bun run build cd replay-ui && bun install --frozen-lockfile && bun run build
mkdir -p $(INSPECT_DIST) mkdir -p $(REPLAY_DIST)
rm -rf $(INSPECT_DIST)/assets $(INSPECT_DIST)/fonts rm -rf $(REPLAY_DIST)/assets $(REPLAY_DIST)/fonts
cp -R $(WEB_DIST)/. $(INSPECT_DIST)/ cp -R $(WEB_DIST)/. $(REPLAY_DIST)/
web-dev: web-dev:
cd inspect-ui && bun run dev cd replay-ui && bun run dev
inspect-dev: $(SIDECAR_EMBED) replay-dev: $(SIDECAR_EMBED)
$(GO) run -tags withsidecar ./cmd/sanderling inspect --dev $(GO) run -tags withsidecar ./cmd/sanderling replay --dev
web-typecheck: web-typecheck:
cd inspect-ui && bun install --frozen-lockfile && bun run typecheck cd replay-ui && bun install --frozen-lockfile && bun run typecheck
$(SIDECAR_JAR): $(SIDECAR_SRC) $(SIDECAR_JAR): $(SIDECAR_SRC)
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sidecar:shadowJar ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sidecar:shadowJar
+2 -2
View File
@@ -8,11 +8,11 @@ Autonomous property-based testing for mobile and web apps.
- [Getting started](https://priyanshujain.github.io/sanderling/manual/getting-started.html) - [Getting started](https://priyanshujain.github.io/sanderling/manual/getting-started.html)
- [Writing specs](https://priyanshujain.github.io/sanderling/manual/writing-specs.html) - [Writing specs](https://priyanshujain.github.io/sanderling/manual/writing-specs.html)
- [`sanderling inspect` UI](https://priyanshujain.github.io/sanderling/manual/inspect.html) - [`sanderling replay` UI](https://priyanshujain.github.io/sanderling/manual/replay.html)
- Examples: [folio](https://github.com/priyanshujain/sanderling/tree/master/examples/folio) (KMP, Android/iOS/web), [folio-web](https://github.com/priyanshujain/sanderling/tree/master/examples/folio-web) (React + Vite) - Examples: [folio](https://github.com/priyanshujain/sanderling/tree/master/examples/folio) (KMP, Android/iOS/web), [folio-web](https://github.com/priyanshujain/sanderling/tree/master/examples/folio-web) (React + Vite)
- [Architecture](https://priyanshujain.github.io/sanderling/development/architecture.html) - [Architecture](https://priyanshujain.github.io/sanderling/development/architecture.html)
After a `sanderling test` run, browse traces locally with `sanderling inspect`. It opens a web UI for stepping through actions, screenshots, snapshots, residual formulas, and exceptions. After a `sanderling test` run, browse traces locally with `sanderling replay`. It opens a web UI for stepping through actions, screenshots, snapshots, residual formulas, and exceptions.
--- ---
+4 -4
View File
@@ -34,7 +34,7 @@ Usage:
Commands: Commands:
test Run a spec against an app for a fixed duration. test Run a spec against an app for a fixed duration.
inspect Serve a local web UI for browsing runs/. replay Serve a local web UI for browsing runs/.
doctor Check that the host environment is ready to run sanderling. doctor Check that the host environment is ready to run sanderling.
version Print the sanderling version. version Print the sanderling version.
@@ -98,12 +98,12 @@ func run(args []string, stdout, stderr io.Writer) error {
return err return err
} }
return runTest(options, stdout) return runTest(options, stdout)
case "inspect": case "replay":
options, err := parseInspectArgs(args[2:], stderr) options, err := parseReplayArgs(args[2:], stderr)
if err != nil { if err != nil {
return err return err
} }
return runInspect(options, stdout) return runReplay(options, stdout)
case "doctor": case "doctor":
return runDoctor(args[2:], stdout, stderr) return runDoctor(args[2:], stdout, stderr)
case "version", "-v", "--version": case "version", "-v", "--version":
@@ -14,29 +14,29 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/priyanshujain/sanderling/internal/inspect" "github.com/priyanshujain/sanderling/internal/replay"
) )
type inspectOptions struct { type replayOptions struct {
port int port int
noOpen bool noOpen bool
dev bool dev bool
directory string directory string
} }
func parseInspectArgs(args []string, stderr io.Writer) (inspectOptions, error) { func parseReplayArgs(args []string, stderr io.Writer) (replayOptions, error) {
flagSet := flag.NewFlagSet("inspect", flag.ContinueOnError) flagSet := flag.NewFlagSet("replay", flag.ContinueOnError)
flagSet.SetOutput(stderr) flagSet.SetOutput(stderr)
var options inspectOptions var options replayOptions
flagSet.IntVar(&options.port, "port", 0, "TCP port to listen on (0 = ephemeral)") flagSet.IntVar(&options.port, "port", 0, "TCP port to listen on (0 = ephemeral)")
flagSet.BoolVar(&options.noOpen, "no-open", false, "do not open the default browser on startup") flagSet.BoolVar(&options.noOpen, "no-open", false, "do not open the default browser on startup")
flagSet.BoolVar(&options.dev, "dev", false, "reverse-proxy non-API requests to "+inspect.DevTarget) flagSet.BoolVar(&options.dev, "dev", false, "reverse-proxy non-API requests to "+replay.DevTarget)
if err := flagSet.Parse(args); err != nil { if err := flagSet.Parse(args); err != nil {
return inspectOptions{}, err return replayOptions{}, err
} }
rest := flagSet.Args() rest := flagSet.Args()
if len(rest) > 1 { if len(rest) > 1 {
return inspectOptions{}, errors.New("inspect takes at most one positional argument (run or runs directory)") return replayOptions{}, errors.New("replay takes at most one positional argument (run or runs directory)")
} }
if len(rest) == 1 { if len(rest) == 1 {
options.directory = rest[0] options.directory = rest[0]
@@ -44,16 +44,16 @@ func parseInspectArgs(args []string, stderr io.Writer) (inspectOptions, error) {
return options, nil return options, nil
} }
func runInspect(options inspectOptions, stdout io.Writer) error { func runReplay(options replayOptions, stdout io.Writer) error {
runsDirectory, deepLinkID, err := inspect.ResolveRunsDirectory(options.directory) runsDirectory, deepLinkID, err := replay.ResolveRunsDirectory(options.directory)
if err != nil { if err != nil {
return err return err
} }
devTarget := "" devTarget := ""
if options.dev { if options.dev {
devTarget = inspect.DevTarget devTarget = replay.DevTarget
} }
server, err := inspect.NewServer(inspect.ServerOptions{ server, err := replay.NewServer(replay.ServerOptions{
RunsDirectory: runsDirectory, RunsDirectory: runsDirectory,
DevTarget: devTarget, DevTarget: devTarget,
}) })
@@ -67,7 +67,7 @@ func runInspect(options inspectOptions, stdout io.Writer) error {
} }
address := listener.Addr().(*net.TCPAddr) address := listener.Addr().(*net.TCPAddr)
browseURL := buildBrowseURL(address, deepLinkID) browseURL := buildBrowseURL(address, deepLinkID)
fmt.Fprintf(stdout, "sanderling inspect listening on %s (runs=%s)\n", browseURL, runsDirectory) fmt.Fprintf(stdout, "sanderling replay listening on %s (runs=%s)\n", browseURL, runsDirectory)
context, cancel := context.WithCancel(context.Background()) context, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -7,8 +7,8 @@ import (
"testing" "testing"
) )
func TestParseInspectArgs_Defaults(t *testing.T) { func TestParseReplayArgs_Defaults(t *testing.T) {
options, err := parseInspectArgs(nil, io.Discard) options, err := parseReplayArgs(nil, io.Discard)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -17,8 +17,8 @@ func TestParseInspectArgs_Defaults(t *testing.T) {
} }
} }
func TestParseInspectArgs_AllFlags(t *testing.T) { func TestParseReplayArgs_AllFlags(t *testing.T) {
options, err := parseInspectArgs([]string{"--port", "9090", "--no-open", "--dev", "/tmp/runs"}, io.Discard) options, err := parseReplayArgs([]string{"--port", "9090", "--no-open", "--dev", "/tmp/runs"}, io.Discard)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -27,19 +27,19 @@ func TestParseInspectArgs_AllFlags(t *testing.T) {
} }
} }
func TestParseInspectArgs_RejectsTooManyPositional(t *testing.T) { func TestParseReplayArgs_RejectsTooManyPositional(t *testing.T) {
_, err := parseInspectArgs([]string{"a", "b"}, io.Discard) _, err := parseReplayArgs([]string{"a", "b"}, io.Discard)
if err == nil || !strings.Contains(err.Error(), "at most one") { if err == nil || !strings.Contains(err.Error(), "at most one") {
t.Fatalf("expected too-many-args error, got %v", err) t.Fatalf("expected too-many-args error, got %v", err)
} }
} }
func TestRun_HelpListsInspectCommand(t *testing.T) { func TestRun_HelpListsReplayCommand(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
if err := run([]string{"sanderling"}, &stdout, io.Discard); err != nil { if err := run([]string{"sanderling"}, &stdout, io.Discard); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !strings.Contains(stdout.String(), "inspect") { if !strings.Contains(stdout.String(), "replay") {
t.Errorf("usage missing inspect command: %q", stdout.String()) t.Errorf("usage missing replay command: %q", stdout.String())
} }
} }
File renamed without changes.
+1 -1
View File
@@ -36,7 +36,7 @@
<li><a href="__ROOT__manual/writing-specs/">Writing specs</a></li> <li><a href="__ROOT__manual/writing-specs/">Writing specs</a></li>
<li><a href="__ROOT__manual/spec-language/">Spec language reference</a></li> <li><a href="__ROOT__manual/spec-language/">Spec language reference</a></li>
<li><a href="__ROOT__manual/runs/">Runs</a></li> <li><a href="__ROOT__manual/runs/">Runs</a></li>
<li><a href="__ROOT__manual/inspect/">Inspect</a></li> <li><a href="__ROOT__manual/replay/">Replay</a></li>
<li><a href="__ROOT__manual/cli/">CLI reference</a></li> <li><a href="__ROOT__manual/cli/">CLI reference</a></li>
</ul> </ul>
<h3>Development</h3> <h3>Development</h3>
+3 -3
View File
@@ -18,7 +18,7 @@ flowchart TB
DC["Device / Emulator"] DC["Device / Emulator"]
CH["Chrome (CDP)"] CH["Chrome (CDP)"]
RD[("runs/")] RD[("runs/")]
IN["sanderling inspect\nHTTP + SSE"] IN["sanderling replay\nHTTP + SSE"]
UI["Web UI (React)"] UI["Web UI (React)"]
D -->|gRPC| SC D -->|gRPC| SC
@@ -45,9 +45,9 @@ flowchart TB
On native, the transport split exists because only real UI events need to cross process and OS-API boundaries. Introspection is cheap, frequent, and lives on a fast local socket directly to the app. On web, CDP handles both. On native, the transport split exists because only real UI events need to cross process and OS-API boundaries. Introspection is cheap, frequent, and lives on a fast local socket directly to the app. On web, CDP handles both.
## Inspect UI ## Replay UI
`sanderling inspect` is a separate mode of the same Go binary. It serves an embedded React bundle and reads `runs/` from disk, streaming file-watcher events over SSE so the UI updates as new steps land. It has no connection to any driver; it only consumes the trace artifacts. `sanderling replay` is a separate mode of the same Go binary. It serves an embedded React bundle and reads `runs/` from disk, streaming file-watcher events over SSE so the UI updates as new steps land. It has no connection to any driver; it only consumes the trace artifacts.
## Per-step cycle ## Per-step cycle
+5 -5
View File
@@ -10,9 +10,9 @@ Architectural and organizational decisions worth recording. Each entry states th
## Directory and Package Organization ## Directory and Package Organization
### `web/` renamed to `inspect-ui/` ### `web/` renamed to `replay-ui/`
The directory containing the React/TypeScript frontend is `inspect-ui/`, not `web/`. The name `web/` was ambiguous (the project also has a web/Chrome driver target). `inspect-ui/` makes the purpose explicit: this is the UI for the `sanderling inspect` command. The directory containing the React/TypeScript frontend is `replay-ui/`, not `web/`. The name `web/` was ambiguous (the project also has a web/Chrome driver target). `replay-ui/` makes the purpose explicit: this is the UI for the `sanderling replay` command.
### Keep `internal/` ### Keep `internal/`
@@ -22,9 +22,9 @@ Go's `internal/` directory restriction prevents any code outside this module fro
The `driver.go` file defines the `DeviceDriver` interface. Concrete implementations live in subdirectories: `sidecar/` (gRPC to the native sidecar), `chrome/` (CDP), `mock/` (tests). This pattern keeps the runner and verifier decoupled from any specific platform. The `driver.go` file defines the `DeviceDriver` interface. Concrete implementations live in subdirectories: `sidecar/` (gRPC to the native sidecar), `chrome/` (CDP), `mock/` (tests). This pattern keeps the runner and verifier decoupled from any specific platform.
### `internal/verifier/marshal.go` moves to `internal/inspect/` ### `internal/verifier/marshal.go` moves to `internal/replay/`
`marshal.go` serializes LTL formulas to JSON for the inspect UI. That is an inspect concern, not a verifier concern. Verifier should not know inspect exists. `marshal.go` serializes LTL formulas to JSON for the replay UI. That is a replay concern, not a verifier concern. Verifier should not know replay exists.
### `internal/verifier/bindings.go` splits into `types.go` + `bindings.go` ### `internal/verifier/bindings.go` splits into `types.go` + `bindings.go`
@@ -44,7 +44,7 @@ Android device enumeration, AVD selection, and emulator boot logic moves to `int
Driver setup, agent connection, verifier init, trace setup, and runner orchestration extract to `internal/testrun/`. `cmd/sanderling/` wires CLI flags to `testrun` calls and nothing more. Driver setup, agent connection, verifier init, trace setup, and runner orchestration extract to `internal/testrun/`. `cmd/sanderling/` wires CLI flags to `testrun` calls and nothing more.
### `internal/inspect/runs.go` splits into multiple files ### `internal/replay/runs.go` splits into multiple files
429 LOC with mixed concerns (cache, file I/O, JSON decoding, summary types) splits into at least `runs_cache.go` and `runs_decode.go` within the same package. 429 LOC with mixed concerns (cache, file I/O, JSON decoding, summary types) splits into at least `runs_cache.go` and `runs_decode.go` within the same package.
+1 -1
View File
@@ -12,7 +12,7 @@ Alpha: Scope of v0.1.0 is tracked in [issue #4](https://github.com/priyanshujain
- [Getting started](./manual/getting-started/) - [Getting started](./manual/getting-started/)
- [Writing specs](./manual/writing-specs/) - [Writing specs](./manual/writing-specs/)
- [Runs](./manual/runs/) - [Runs](./manual/runs/)
- [Inspect](./manual/inspect/) - [Replay](./manual/replay/)
- [CLI reference](./manual/cli/) - [CLI reference](./manual/cli/)
--- ---
+2 -2
View File
@@ -25,7 +25,7 @@ Run a spec against an app for a fixed duration.
| `--output` | `./runs` | Output directory for traces. | | `--output` | `./runs` | Output directory for traces. |
| `--clear-data` | `false` | Clear app data before launching so the run starts from a fresh install. | | `--clear-data` | `false` | Clear app data before launching so the run starts from a fresh install. |
## `sanderling inspect [run-or-runs-dir]` ## `sanderling replay [run-or-runs-dir]`
Serve a local web UI for browsing traces. The positional argument is optional and may point at either a runs directory (the parent of many runs) or a single run directory (auto-detected by the presence of `meta.json`). Defaults to `./runs`. Serve a local web UI for browsing traces. The positional argument is optional and may point at either a runs directory (the parent of many runs) or a single run directory (auto-detected by the presence of `meta.json`). Defaults to `./runs`.
@@ -35,7 +35,7 @@ Serve a local web UI for browsing traces. The positional argument is optional an
| `--no-open` | `false` | Skip opening the default browser on startup. | | `--no-open` | `false` | Skip opening the default browser on startup. |
| `--dev` | `false` | Reverse-proxy non-API requests to the Vite dev server on `127.0.0.1:5173`. | | `--dev` | `false` | Reverse-proxy non-API requests to the Vite dev server on `127.0.0.1:5173`. |
See [the inspect UI page](./inspect/) for the panel reference and keyboard shortcuts. See [the replay UI page](./replay/) for the panel reference and keyboard shortcuts.
## `sanderling doctor` ## `sanderling doctor`
+1 -1
View File
@@ -92,6 +92,6 @@ runs/2026-04-18T12-34-56/
└── meta.json └── meta.json
``` ```
Browse it with `sanderling inspect` (see [inspect](./inspect/)), or read `trace.jsonl` step by step. Browse it with `sanderling replay` (see [replay](./replay/)), or read `trace.jsonl` step by step.
Next: [writing specs](./writing-specs/). Next: [writing specs](./writing-specs/).
@@ -1,18 +1,18 @@
--- ---
title: sanderling inspect title: sanderling replay
--- ---
# sanderling inspect # sanderling replay
Local web UI for exploring runs produced by `sanderling test`. Reads `runs/<id>/meta.json` and `runs/<id>/trace.jsonl` from disk. Local web UI for exploring runs produced by `sanderling test`. Reads `runs/<id>/meta.json` and `runs/<id>/trace.jsonl` from disk.
``` ```
sanderling inspect [run-or-runs-dir] [--port N] [--no-open] [--dev] sanderling replay [run-or-runs-dir] [--port N] [--no-open] [--dev]
``` ```
The positional argument can be a runs directory or a single run directory (auto-detected by `meta.json`). Defaults to `./runs`. The positional argument can be a runs directory or a single run directory (auto-detected by `meta.json`). Defaults to `./runs`.
![sanderling inspect](../../_assets/inspect-ui.png) ![sanderling replay](../../_assets/replay-ui.png)
## Panels ## Panels
@@ -44,15 +44,15 @@ Arrow keys inside a tablist or listbox yield to those widgets. Use `j`/`k` when
`/runs/:id/steps/:n` links to a specific step. Use it in issues or PRs when pointing at a violation. `/runs/:id/steps/:n` links to a specific step. Use it in issues or PRs when pointing at a violation.
The run index auto-refreshes over SSE as new runs land, so `sanderling inspect` and `sanderling test` can run side by side. The run index auto-refreshes over SSE as new runs land, so `sanderling replay` and `sanderling test` can run side by side.
## Development ## Development
Two-process loop while iterating on the UI: Two-process loop while iterating on the UI:
``` ```
make web-dev # bun + vite on http://127.0.0.1:5173 make web-dev # bun + vite on http://127.0.0.1:5173
make inspect-dev # sanderling inspect --dev, proxies non-API to 5173 make replay-dev # sanderling replay --dev, proxies non-API to 5173
``` ```
Single binary with the bundle embedded: Single binary with the bundle embedded:
+1 -1
View File
@@ -265,7 +265,7 @@ func violated() reduceResult { return reduceResult{status: statusViolated} }
// violatedWith reports a violation that originates at the given sub-formula // violatedWith reports a violation that originates at the given sub-formula
// with the given reason. The reason distinguishes a thrown predicate from a // with the given reason. The reason distinguishes a thrown predicate from a
// plain false so callers (and the inspect UI) can render the cause. // plain false so callers (and the replay UI) can render the cause.
func violatedWith(formula Formula, reason string) reduceResult { func violatedWith(formula Formula, reason string) reduceResult {
return reduceResult{ return reduceResult{
status: statusViolated, status: statusViolated,
+1 -1
View File
@@ -23,7 +23,7 @@ type PredicateLabel interface {
// ErrorFormula represents a thunk that threw during evaluation. The verifier // ErrorFormula represents a thunk that threw during evaluation. The verifier
// substitutes one of these into the residual when MarshalJSON would otherwise // substitutes one of these into the residual when MarshalJSON would otherwise
// have to encode an opaque thunk that already errored. It exists so that the // have to encode an opaque thunk that already errored. It exists so that the
// inspect UI can render "predicate threw" inline. // replay UI can render "predicate threw" inline.
type ErrorFormula struct { type ErrorFormula struct {
Message string Message string
} }
@@ -1,5 +1,5 @@
// Package inspect serves the embedded web UI for browsing recorded runs. // Package replay serves the embedded web UI for browsing recorded runs.
package inspect package replay
import ( import (
"embed" "embed"
@@ -11,11 +11,11 @@ var distFS embed.FS
// Assets returns the embedded SPA bundle rooted at the dist directory. // Assets returns the embedded SPA bundle rooted at the dist directory.
// In Stage 2 this contains a stub index.html. Stage 4 wires the real // In Stage 2 this contains a stub index.html. Stage 4 wires the real
// bundle in via Makefile (copy inspect-ui/dist -> internal/inspect/dist). // bundle in via Makefile (copy replay-ui/dist -> internal/replay/dist).
func Assets() fs.FS { func Assets() fs.FS {
sub, err := fs.Sub(distFS, "dist") sub, err := fs.Sub(distFS, "dist")
if err != nil { if err != nil {
panic("inspect: dist embed missing: " + err.Error()) panic("replay: dist embed missing: " + err.Error())
} }
return sub return sub
} }
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"net/http" "net/http"
File renamed without changes.
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"errors" "errors"
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"bufio" "bufio"
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"bufio" "bufio"
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"encoding/json" "encoding/json"
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"encoding/json" "encoding/json"
@@ -22,7 +22,7 @@ type ServerOptions struct {
AssetsFS fs.FS AssetsFS fs.FS
} }
// Server holds the HTTP handlers for `sanderling inspect`. // Server holds the HTTP handlers for `sanderling replay`.
type Server struct { type Server struct {
options ServerOptions options ServerOptions
cache *Cache cache *Cache
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"context" "context"
@@ -1,4 +1,4 @@
package inspect package replay
import ( import (
"testing" "testing"
+1 -1
View File
@@ -172,7 +172,7 @@ func Run(ctx context.Context, options Options) (Summary, error) {
// one would poison the verifier's previous/current extractor // one would poison the verifier's previous/current extractor
// advance, so the next clean step would compare against this // advance, so the next clean step would compare against this
// transient state and emit false-positive violations. We still // transient state and emit false-positive violations. We still
// record the step (hierarchy + screenshot) for inspect-side // record the step (hierarchy + screenshot) for replay-side
// debugging, but skip the verifier entirely and pick the next // debugging, but skip the verifier entirely and pick the next
// action against the unchanged prior state to keep the loop // action against the unchanged prior state to keep the loop
// progressing. // progressing.
+1 -1
View File
@@ -50,7 +50,7 @@ type Witness struct {
// ExtractorChange records the prev/curr JSON values of an extractor whose // ExtractorChange records the prev/curr JSON values of an extractor whose
// observation differed between two consecutive steps. Surfaced under // observation differed between two consecutive steps. Surfaced under
// violation rows in the inspect UI as a "what changed at this step" // violation rows in the replay UI as a "what changed at this step"
// breadcrumb. // breadcrumb.
type ExtractorChange struct { type ExtractorChange struct {
Prev json.RawMessage `json:"prev"` Prev json.RawMessage `json:"prev"`
+1 -1
View File
@@ -53,7 +53,7 @@ type Exception struct {
// ExtractorChange records a single extractor's value transition across one // ExtractorChange records a single extractor's value transition across one
// step. Used to surface "what changed at this step" breadcrumbs at violation // step. Used to surface "what changed at this step" breadcrumbs at violation
// markers in the inspect UI. // markers in the replay UI.
type ExtractorChange struct { type ExtractorChange struct {
Prev []byte Prev []byte
Curr []byte Curr []byte
+2 -2
View File
@@ -321,7 +321,7 @@ func encodeExtractorValue(value goja.Value) []byte {
// ChangedExtractors returns the named extractors whose value changed between // ChangedExtractors returns the named extractors whose value changed between
// the prior PushSnapshot and the current one. The map is keyed by extractor // the prior PushSnapshot and the current one. The map is keyed by extractor
// name; unnamed extractors (extractor_N fallback) are included so the inspect // name; unnamed extractors (extractor_N fallback) are included so the replay
// UI can still display them under a numeric label. The very first snapshot // UI can still display them under a numeric label. The very first snapshot
// emits every non-null extractor as a change (Prev=null, Curr=current) since // emits every non-null extractor as a change (Prev=null, Curr=current) since
// the runner can otherwise misread "no diff yet" as "nothing initialized". // the runner can otherwise misread "no diff yet" as "nothing initialized".
@@ -525,7 +525,7 @@ func (v *Verifier) NewlyViolatedProperties() []string {
// Residuals returns the residual formula for each registered property after // Residuals returns the residual formula for each registered property after
// the most recent EvaluateProperties call. Properties whose violation was // the most recent EvaluateProperties call. Properties whose violation was
// caused by a thrown predicate surface as ErrorFormula, sourced from the // caused by a thrown predicate surface as ErrorFormula, sourced from the
// captured witness, so the inspect UI can render "predicate threw" inline. // captured witness, so the replay UI can render "predicate threw" inline.
func (v *Verifier) Residuals() map[string]ltl.Formula { func (v *Verifier) Residuals() map[string]ltl.Formula {
residuals := map[string]ltl.Formula{} residuals := map[string]ltl.Formula{}
for name, evaluator := range v.evaluators { for name, evaluator := range v.evaluators {
File renamed without changes.
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>sanderling inspect</title> <title>sanderling replay</title>
<link rel="stylesheet" href="/src/styles/typography.css" /> <link rel="stylesheet" href="/src/styles/typography.css" />
<link rel="stylesheet" href="/src/styles/tokens.css" /> <link rel="stylesheet" href="/src/styles/tokens.css" />
<link rel="stylesheet" href="/src/styles/app.css" /> <link rel="stylesheet" href="/src/styles/app.css" />
@@ -1,5 +1,5 @@
{ {
"name": "@sanderling/inspect-web", "name": "@sanderling/replay-web",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export type ThemeMode = "light" | "dark"; export type ThemeMode = "light" | "dark";
const STORAGE_KEY = "sanderling-inspect-theme"; const STORAGE_KEY = "sanderling-replay-theme";
function detectInitial(): ThemeMode { function detectInitial(): ThemeMode {
if (typeof window === "undefined") { if (typeof window === "undefined") {
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -15,6 +15,11 @@ import { useStep } from "../hooks/useStep";
import { useKeyboardNav } from "../hooks/useKeyboardNav"; import { useKeyboardNav } from "../hooks/useKeyboardNav";
import { useTheme } from "../hooks/useTheme"; import { useTheme } from "../hooks/useTheme";
function basename(specPath: string): string {
const index = specPath.lastIndexOf("/");
return index >= 0 ? specPath.slice(index + 1) : specPath;
}
interface RunHistory { interface RunHistory {
names: string[]; names: string[];
lanes: PropertyLane[]; lanes: PropertyLane[];
@@ -242,7 +247,7 @@ export default function RunDetail() {
<Link to="/">runs</Link> <Link to="/">runs</Link>
<span>{run.id}</span> <span>{run.id}</span>
<span> <span>
<strong>{run.spec_path}</strong> seed={run.seed} <strong title={run.spec_path}>{basename(run.spec_path)}</strong> seed={run.seed}
</span> </span>
<span> <span>
step {stepIndex} / {stepCount ?? 0} step {stepIndex} / {stepCount ?? 0}
@@ -28,6 +28,11 @@ function formatStartedAt(value: string): string {
return date.toLocaleString(); return date.toLocaleString();
} }
function basename(specPath: string): string {
const index = specPath.lastIndexOf("/");
return index >= 0 ? specPath.slice(index + 1) : specPath;
}
export default function RunList() { export default function RunList() {
const [runs, setRuns] = useState<RunSummary[] | null>(null); const [runs, setRuns] = useState<RunSummary[] | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -76,7 +81,7 @@ export default function RunList() {
<td> <td>
<Link to={`/runs/${run.id}`}>{formatStartedAt(run.started_at)}</Link> <Link to={`/runs/${run.id}`}>{formatStartedAt(run.started_at)}</Link>
</td> </td>
<td>{run.spec_path}</td> <td title={run.spec_path}>{basename(run.spec_path)}</td>
<td>{run.seed}</td> <td>{run.seed}</td>
<td>{run.platform}</td> <td>{run.platform}</td>
<td>{formatDuration(run.duration_millis)}</td> <td>{formatDuration(run.duration_millis)}</td>
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.