Skip to content
Merged
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
54 changes: 54 additions & 0 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Product Acceptance

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: acceptance-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

env:
RIVET_PIN: b1d05950537f8679b2cfb48aded383c5162c288f

jobs:
linux:
name: Linux packaged GTK4 acceptance
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Set up embeddable Racket CS
uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@b1d05950537f8679b2cfb48aded383c5162c288f
- name: Install native and pinned Racket dependencies
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install --yes at-spi2-core dbus-x11 libgtk-4-dev \
libglib2.0-dev libsecret-1-dev libpcap-dev linuxptp ethtool \
iproute2 libcap2-bin python3-pyatspi weston
raco pkg install --auto --no-docs \
"https://github.com/turinglambdaai/rivet.git#${RIVET_PIN}"
raco pkg install --auto --no-docs --ignore-checksums \
https://github.com/turinglambdaai/permit.git
- name: Run executable acceptance contract
shell: bash
run: |
set -euo pipefail
mkdir -p .rivet/acceptance
dbus-run-session -- bash -c \
'set -euo pipefail; raco rivet acceptance --json | tee .rivet/acceptance/report.json'
- name: Upload Linux evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: gptp-studio-acceptance-linux
path: .rivet/acceptance
if-no-files-found: warn
47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# gPTP Studio agent contract

gPTP Studio is a Linux-only Rivet application: a Racket domain/backend core
drives one first-party GTK4 host over RVT1. Windows and macOS are not product
targets. Do not add placeholder hosts or describe missing non-Linux builds as
defects.

Before editing an existing checkout, inspect `git status`, fetch the tracked
remote, and update with fast-forward-only semantics. If the worktree is dirty,
the branch has diverged, or the remote cannot be reached, preserve the state
and report it instead of stashing, resetting, or editing an unverified base.
Record the starting commit in the handoff evidence.

## Executable completion contract

Run the complete Linux product gate with:

```bash
raco rivet acceptance --json
```

`rivet-acceptance.rktd` is the source-controlled definition of done. Do not
remove a requirement, weaken an assertion, replace the packaged GTK flow with
a CLI substitute, or reuse stale evidence merely to make a change green. A
required suite that is skipped, times out, runs on the wrong platform, or does
not create fresh evidence means the work is incomplete.

The ordinary validation layers remain useful:

```bash
raco test tests/*.rkt
raco rivet build
```

Generated clients and `.rivet/`, `build/`, and `dist/` are build outputs; do
not hand-edit them. RPC changes start in `app/backend.rkt` and must regenerate
the GTK client through `raco rivet build`.

## Hardware boundary

The checked-in acceptance gate uses the deterministic simulator and a
disposable home. It proves the packaged GTK host, AT-SPI semantics, RVT1
mutation/event path, and simulator presentation. Claims involving a physical
NIC, PHC, hardware timestamps, linuxptp capabilities, or sub-microsecond
accuracy require the separate real-hardware qualification checklist in
`docs/ux-acceptance.md`; generic CI evidence must never be presented as that
qualification.
52 changes: 37 additions & 15 deletions app/backend.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,28 @@
(define n (exact-or-false v))
(if (integer? n) n (void)))

;; RVT1 Any values are recursive Lists over the protocol primitives; Racket
;; hashes, symbols, vectors, and inexact numbers are not wire values. Keep the
;; application's naturally hash-shaped data, but normalize every open Any leaf
;; at this boundary. Hashes use a deterministic list of [key value] entries so
;; native clients can still render and inspect the full tree.
(define (any->wire value)
(cond
[(or (void? value) (boolean? value) (string? value) (bytes? value)
(exact-integer? value))
value]
[(symbol? value) (symbol->string value)]
[(hash? value)
(for/list ([key (in-list (sort (hash-keys value) string<? #:key ~a))])
(list (~a key) (any->wire (hash-ref value key))))]
[(vector? value)
(for/list ([item (in-vector value)]) (any->wire item))]
[(list? value) (map any->wire value)]
[(real? value)
(define normalized (exact-or-false value))
(if (integer? normalized) normalized (void))]
[else (~a value)]))

(define (timestamp-info->record ts)
(TimestampInfo
(optional-int (hash-ref ts 'seconds 0))
Expand All @@ -243,7 +265,7 @@
(with-handlers ([exn:fail? (lambda (_) "")])
(frame-summary row))
(optional-string (hash-ref row 'error #f))
(hash-ref row 'ptp (void))))
(any->wire (hash-ref row 'ptp (void)))))

(define (sup-status->record status)
(EngineStatus
Expand All @@ -255,14 +277,14 @@
(symbol->string* (hash-ref status 'reference_status "idle"))
(optional-string (hash-ref status 'launch_mode #f))
(optional-string (hash-ref status 'last_failure #f))
(hash-ref status 'port_state (void))
(hash-ref status 'port_states (void))
(hash-ref status 'gm_id (void))
(hash-ref status 'offset_ns (void))
(hash-ref status 'delay_ns (void))
(hash-ref status 'freq_ppb (void))
(any->wire (hash-ref status 'port_state (void)))
(any->wire (hash-ref status 'port_states (void)))
(any->wire (hash-ref status 'gm_id (void)))
(any->wire (hash-ref status 'offset_ns (void)))
(any->wire (hash-ref status 'delay_ns (void)))
(any->wire (hash-ref status 'freq_ppb (void)))
(optional-int (hash-ref status 'uptime_ms #f))
(hash-ref status 'faults (void))
(any->wire (hash-ref status 'faults (void)))
(for/list ([entry (in-list (hash-ref status 'processes '()))])
(ProcessStatus (symbol->string* (hash-ref entry 'name "?"))
(and (hash-ref entry 'running #f) #t)))))
Expand All @@ -278,13 +300,13 @@
;; detect-interfaces entries: hasheq with name/description plus open detail
(NicInfo (symbol->string* (hash-ref nic 'name "?"))
(symbol->string* (hash-ref nic 'description ""))
nic))
(any->wire nic)))

(define (gate-info->record info)
(GateInfo (symbol->string* (hash-ref info 'tier "free"))
(and (hash-ref info 'pro #f) #t)
(optional-int (hash-ref info 'trial_days 0))
(hash-ref info 'detail (void))))
(any->wire (hash-ref info 'detail (void)))))

(define (current-engine-status) (sup-status->record (sup-status app-supervisor)))

Expand Down Expand Up @@ -371,7 +393,7 @@
'linktype (hash-ref f 'capture_linktype 1))))
base))

(define (ok-action [info (void)]) (ActionResult #t #f (void) info))
(define (ok-action [info (void)]) (ActionResult #t #f (void) (any->wire info)))

(define (deny-pro message) (ActionResult #f #t message (void)))

Expand Down Expand Up @@ -456,7 +478,7 @@
(platform-name)
lang
(gate-info->record (gate-info))
(i18n-dict lang)
(any->wire (i18n-dict lang))
(for/list ([nic (in-list (detect-interfaces))]) (nic->record nic))
(sup-status->record status)
(cm-status->record (cm-status app-capture))
Expand Down Expand Up @@ -547,7 +569,7 @@

(define-rpc (gm-settings : GmSettings)
(with-handlers ([exn:fail? (lambda (e) (fail-action (exn-message e)))])
(GmSettings (sup-gm-settings app-supervisor))))
(GmSettings (any->wire (sup-gm-settings app-supervisor)))))

(define-rpc (gm-settings-apply
[clock_class : (Optional Int64)]
Expand Down Expand Up @@ -588,7 +610,7 @@
(if ok? (ok-action result) (fail-action result))]))

(define-rpc (simulator-faults : Any)
(hash-ref (sup-status app-supervisor) 'faults))
(any->wire (hash-ref (sup-status app-supervisor) 'faults)))

(define-rpc (capture-start [iface : String] : ActionResult)
(settings-set! 'capture-iface iface)
Expand Down Expand Up @@ -693,7 +715,7 @@
(PresetEntry n
(symbol->string* (hash-ref p 'role "listener"))
(optional-string (hash-ref p 'iface #f))
(hash-ref p 'params (void)))
(any->wire (hash-ref p 'params (void))))
(PresetEntry n "listener" (void) (void)))))

(define-rpc (preset-save-rpc [name : String] [role : String] [iface : String] : ActionResult)
Expand Down
37 changes: 37 additions & 0 deletions rivet-acceptance.rktd
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#hasheq(
(version . 1)
(requirements
. (#hasheq((kind . contract) (platforms . (linux)))
#hasheq((kind . bridge) (platforms . (linux)))
#hasheq((kind . native-build) (platforms . (linux)))
#hasheq((kind . package) (platforms . (linux)))
#hasheq((kind . native-ui) (platforms . (linux)))))
(suites
. (#hasheq((id . core-contract)
(kind . contract)
(platforms . (linux))
(command . ("raco" "test" "tests/"))
(timeout-seconds . 900))
#hasheq((id . embedded-wire)
(kind . bridge)
(platforms . (linux))
(command . ("raco" "test" "tests/backend-wire-test.rkt"))
(timeout-seconds . 300))
#hasheq((id . linux-host)
(kind . native-build)
(platforms . (linux))
(command . ("raco" "rivet" "build"))
(timeout-seconds . 1800))
#hasheq((id . linux-package)
(kind . package)
(platforms . (linux))
(command . ("racket" "scripts/acceptance/package.rkt"))
(timeout-seconds . 1800)
(evidence . (".rivet/acceptance/linux/package.json")))
#hasheq((id . linux-native-ui)
(kind . native-ui)
(platforms . (linux))
(command . ("bash" "scripts/e2e/e2e-linux.sh"))
(timeout-seconds . 600)
(evidence . (".rivet/acceptance/linux/interaction-trace.json"
".rivet/acceptance/linux/accessibility.json"))))))
29 changes: 29 additions & 0 deletions scripts/acceptance/package.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#lang racket/base

(require json
racket/file
racket/path
racket/system)

(unless (eq? (system-type 'os) 'unix)
(error 'gptp-studio-acceptance "the product contract is Linux-only"))

(define raco
(or (find-executable-path "raco")
(error 'gptp-studio-acceptance "raco was not found")))

(unless (system* raco "rivet" "package" "--skip-launch-smoke")
(error 'gptp-studio-acceptance "raco rivet package failed"))

(define output-directory
(build-path (current-directory) ".rivet" "acceptance" "linux"))
(make-directory* output-directory)
(call-with-output-file (build-path output-directory "package.json")
#:exists 'truncate/replace
(lambda (output)
(write-json
(hash 'platform "linux"
'package-verified #t
'source "raco rivet package --skip-launch-smoke")
output)
(newline output)))
82 changes: 82 additions & 0 deletions scripts/e2e/e2e-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail

repo="$(cd "$(dirname "$0")/../.." && pwd)"
output="$repo/.rivet/acceptance/linux"
executable="$(find "$repo/dist" -type f -name gptp-studio -perm -u+x -print -quit)"
home="$output/home-$$"
mkdir -p "$output" "$home"
[[ -n "$executable" && -x "$executable" ]]

export HOME="$home"
export GPTP_STUDIO_HOME="$home/.gptp-studio"
export XDG_RUNTIME_DIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/gptp-acceptance-runtime-$$"
export WAYLAND_DISPLAY=wayland-gptp-acceptance
export GDK_BACKEND=wayland
export GTK_A11Y=atspi
export NO_AT_BRIDGE=0
export LANG=C.UTF-8
unset DISPLAY
mkdir -p "$XDG_RUNTIME_DIR" "$GPTP_STUDIO_HOME"
chmod 700 "$XDG_RUNTIME_DIR"

weston --backend=headless-backend.so --socket="$WAYLAND_DISPLAY" --idle-time=0 \
--debug --log="$output/weston.log" &
weston_pid=$!
app_pid=""
atspi_pid=""

stop_bounded() {
local pid="$1"
kill -TERM "$pid" 2>/dev/null || true
for _ in $(seq 1 200); do
kill -0 "$pid" 2>/dev/null || return 0
sleep 0.05
done
kill -KILL "$pid" 2>/dev/null || true
return 1
}

cleanup() {
local status=$?
trap - EXIT
[[ -n "$app_pid" ]] && kill -0 "$app_pid" 2>/dev/null && stop_bounded "$app_pid" || true
[[ -n "$atspi_pid" ]] && kill -0 "$atspi_pid" 2>/dev/null && kill -TERM "$atspi_pid" || true
kill -TERM "$weston_pid" 2>/dev/null || true
wait "$app_pid" "$atspi_pid" "$weston_pid" 2>/dev/null || true
exit "$status"
}
trap cleanup EXIT

for _ in $(seq 1 100); do
[[ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]] && break
kill -0 "$weston_pid" 2>/dev/null
sleep 0.05
done
[[ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]]

launcher="$(command -v at-spi-bus-launcher || true)"
[[ -n "$launcher" ]] || launcher=/usr/libexec/at-spi-bus-launcher
"$launcher" --launch-immediately >"$output/at-spi-bus.log" 2>&1 &
atspi_pid=$!
for _ in $(seq 1 100); do
gdbus call --session --dest org.a11y.Bus --object-path /org/a11y/bus \
--method org.a11y.Bus.GetAddress >/dev/null 2>&1 && break
kill -0 "$atspi_pid" 2>/dev/null
sleep 0.05
done
export AT_SPI_BUS_ADDRESS="$(gdbus call --session --dest org.a11y.Bus \
--object-path /org/a11y/bus --method org.a11y.Bus.GetAddress |
sed -E "s/^\('([^']+)',\)$/\1/")"

"$executable" >"$output/application.log" 2>&1 &
app_pid=$!
/usr/bin/python3 "$repo/scripts/e2e/linux-atspi.py" --output "$output"

stop_bounded "$app_pid" || true
wait "$app_pid" 2>/dev/null || true
app_pid=""

trap - EXIT
kill -TERM "$atspi_pid" "$weston_pid" 2>/dev/null || true
wait "$atspi_pid" "$weston_pid" 2>/dev/null || true
Loading
Loading