feat: add Linux server operational delivery assets

This commit is contained in:
2026-09-11 05:52:26 +00:00
parent 409b64a2fb
commit c709b27e5e
12 changed files with 412 additions and 50 deletions
+3
View File
@@ -8,3 +8,6 @@
*.db-wal
*.sock
*.tmp
/deploy/production/server.toml
/deploy/production/state/
/deploy/production/run/
+28
View File
@@ -0,0 +1,28 @@
# Production-shaped RVBox Linux-server Compose deployment
This directory is intentionally separate from the development/toolchain Compose
files. It starts only the Linux server and nginx TLS terminator; Windows clients
connect through nginx at `/v1/agent`.
Before the first start, create `server.toml` from the authoritative example and
prepare writable state directories for the runtime image UID/GID `65532`:
```sh
cp ../../docs/examples/server.toml server.toml
install -d -m 0700 -o 65532 -g 65532 state run
chmod 0640 server.toml
```
Set `RVBOX_SERVER_IMAGE` to an immutable image reference, plus absolute paths
for `RVBOX_TLS_CERT` and `RVBOX_TLS_KEY`. The TLS key must be readable by Docker
but should remain inaccessible to ordinary host users. Validate before start:
```sh
docker compose -f compose.yaml config
docker compose -f compose.yaml up -d
```
Only `state/` and `run/` are persistent/owned deployment data. Back up the
whole `state/` directory while the server is stopped; `run/` contains only the
ephemeral local control socket. Do not publish, proxy, or enable JSON-RPC except
for intentional loopback debugging.
+68
View File
@@ -0,0 +1,68 @@
# Production-shaped Linux server deployment. Copy server.toml from docs/examples/
# and supply the TLS certificate/key as read-only files.
#
# `rvbox-server` stays private to this Compose network: nginx is the only public
# listener. JSON-RPC remains disabled in server.toml by default.
name: rvbox-server
services:
server:
image: "${RVBOX_SERVER_IMAGE:?set RVBOX_SERVER_IMAGE to a pinned rvbox-server image}"
restart: unless-stopped
command: ["--config", "/etc/rvbox/server.toml"]
read_only: true
tmpfs:
- /tmp:mode=1777,size=32m
volumes:
- type: bind
source: ./server.toml
target: /etc/rvbox/server.toml
read_only: true
- type: bind
source: ./state
target: /var/lib/rvbox-server
- type: bind
source: ./run
target: /run/rvbox
expose:
- "6899"
- "6901"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:6901/readyz"]
interval: 15s
timeout: 5s
retries: 4
start_period: 20s
security_opt:
- no-new-privileges:true
cap_drop: ["ALL"]
nginx:
image: nginx:1.27.5-alpine
restart: unless-stopped
read_only: true
depends_on:
server:
condition: service_healthy
ports:
- "${RVBOX_HTTPS_PORT:-443}:443"
tmpfs:
- /var/cache/nginx:uid=101,gid=101,mode=0755,size=16m
- /var/run:uid=101,gid=101,mode=0755,size=4m
volumes:
- type: bind
source: ./nginx.conf
target: /etc/nginx/conf.d/default.conf
read_only: true
- type: bind
source: ${RVBOX_TLS_CERT:?set RVBOX_TLS_CERT to the public certificate path}
target: /etc/nginx/tls/server.pem
read_only: true
- type: bind
source: ${RVBOX_TLS_KEY:?set RVBOX_TLS_KEY to the private key path}
target: /etc/nginx/tls/server-key.pem
read_only: true
security_opt:
- no-new-privileges:true
cap_drop: ["ALL"]
cap_add: ["NET_BIND_SERVICE"]
+42
View File
@@ -0,0 +1,42 @@
# TLS terminator for the RVBox agent WebSocket. Do not add a JSON-RPC route:
# its deliberately unauthenticated debug adapter is loopback-only by default.
map $http_upgrade $rvbox_connection_upgrade {
default upgrade;
'' close;
}
upstream rvbox_agent {
server server:6899;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/tls/server.pem;
ssl_certificate_key /etc/nginx/tls/server-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location = /v1/agent {
proxy_pass http://rvbox_agent;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $rvbox_connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 75s;
proxy_send_timeout 15s;
proxy_buffering off;
}
location = /livez {
proxy_pass http://server:6901/livez;
}
location = /readyz {
proxy_pass http://server:6901/readyz;
}
location / {
return 404;
}
}
+34
View File
@@ -0,0 +1,34 @@
[Unit]
Description=RVBox server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=rvbox
Group=rvbox
ExecStartPre=/usr/local/bin/rvbox-server --check-config --config /etc/rvbox/server.toml
ExecStart=/usr/local/bin/rvbox-server --config /etc/rvbox/server.toml
Restart=on-failure
RestartSec=5s
TimeoutStopSec=35s
WorkingDirectory=/var/lib/rvbox-server
StateDirectory=rvbox-server
RuntimeDirectory=rvbox
RuntimeDirectoryMode=0750
UMask=0077
LimitNOFILE=65536
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
ReadWritePaths=/var/lib/rvbox-server /run/rvbox
[Install]
WantedBy=multi-user.target
+4 -2
View File
@@ -9,8 +9,8 @@ Read the documents in this order:
script transfer, heartbeat, and failure containment.
3. [Control plane](control-plane.md) — `rvc`, Unix-socket gRPC, JSON-RPC, and
query/foreground semantics.
4. [Platform and operations](platform-and-operations.md) — Unix/Windows
contracts, recovery, storage safety, telemetry, and defaults.
4. [Platform and operations](platform-and-operations.md) — Windows contract,
recovery, storage safety, telemetry, and defaults.
5. [Configuration contract](configuration.md) — strict TOML loading, shell
resolution, cross-field validation, and annotated server/client examples.
6. [Go implementation plan](implementation-plan.v1.md) — phased build order,
@@ -22,6 +22,8 @@ Read the documents in this order:
procedure, and known limitations.
9. [Interactive VM access](../test/rdp-access/README.md) — temporary,
self-signed HTTPS browser gateway for the rare manual UAC recovery step.
10. [Linux-server operations runbook](operations-runbook.md) — deployment,
backup/restore, upgrade, and incident response.
The wire authority is in [`../protos/rvbox/v1`](../protos/rvbox/v1):
`common.proto` contains shared data types, `agent.proto` contains the
+21 -27
View File
@@ -11,12 +11,10 @@ binaries:
spool, and reconnect/reconciliation owner.
- `rvc`: local CLI over the server's Unix-domain gRPC socket.
The first supported client target is Windows connecting to a Linux server;
Linux client support remains part of complete v1 but is explicitly deferred
from the current implementation effort. Implement the Linux server and native
Windows client first; do not begin Unix-like client code now. Build shared
client runtime code behind OS interfaces without prematurely implementing the
Unix supervisor. Windows v1 requires Windows 10 or newer, or Windows Server 2016
The v1 product boundary is a Linux server and a native Windows client. A
Unix-like client is outside v1 and must not be started as part of this plan.
Build shared client runtime code behind OS interfaces without prematurely
implementing the Unix supervisor. Windows v1 requires Windows 10 or newer, or Windows Server 2016
or newer. Desktop Experience is required only for the tray and active-session
command contexts; the service and Session 0 execution contexts support headless
Server Core.
@@ -2326,7 +2324,7 @@ version. Assert no Task Scheduler object is created or required.
loss/restart, execute up to capacity at most once, preserve/replay bounded
history, manage complete Job trees, and satisfy tray/elevation/autostart behavior
on Windows CI, including all execution-hierarchy rows. Reaching this gate does
not automatically start the deferred Unix-client work; that requires an explicit
not automatically start future post-v1 Unix-client work; that requires an explicit
later implementation decision.
## 8. End-to-end agent protocol (Phase 5)
@@ -2383,7 +2381,7 @@ the visible CLI result. Include these cross-cutting cases:
server demonstrates a complete background command, history/follow behavior,
stdin interaction, signal, reconnect, and restart recovery through nginx WSS.
This is the first supported-client milestone. It satisfies a prerequisite for
the deferred Linux supervisor work but does not automatically authorize it.
future Linux-supervisor work but does not automatically authorize it.
## 9. Server control plane and `rvc` (Phase 6)
@@ -2534,14 +2532,14 @@ stack; the Unix socket has mode `0600`; JSON-RPC behavior matches gRPC unary
semantics and is off unless explicitly enabled; the control integration suite
can be interrupted, resumed, and reset through the shared run ID.
## 10. Deferred Linux client implementation (Phase 7; still required for full v1)
## 10. Future Unix-like client work (outside v1)
This phase is design-only in the current effort. Do not implement it now. Retain
these tasks so the later Unix-client work completes the already defined v1
contract without weakening the shipped Windows behavior.
This work is outside the v1 product boundary. Do not implement it now. Retain
these tasks as the starting point for a later Unix-client scope without
weakening the shipped Windows behavior.
Begin this phase only after both an explicit later implementation decision and
the Windows Phase 4/5 release gates. Keep Unix code in platform-specific files/
Begin this work only after an explicit later implementation decision and the
Windows release gates. Keep Unix code in platform-specific files/
build tags and reuse the proven runtime/store/protocol contracts without
changing their wire semantics to suit Linux.
@@ -2602,9 +2600,9 @@ reconnect/replay, truncation, scripts, tombstones, queue limits, `/proc`
absence, profile failure, and shutdown interruption. Add explicit tests for
pidfd/`clone3` availability fallbacks and deliberate `setsid` escape behavior.
**Exit criteria:** the Linux client passes the same protocol/durability suite as
Windows, plus cgroup/process-group tests, without weakening the already shipped
Windows behavior or changing the v1 wire contract.
**Future exit criteria:** the Linux client passes the same protocol/durability
suite as Windows, plus cgroup/process-group tests, without weakening the
already shipped Windows behavior or changing the established wire contract.
## 11. Reliability, observability, and operational delivery (Phase 8)
@@ -2691,9 +2689,8 @@ The currently authorized merge order is deliberately vertical:
8. The Windows-applicable Phase 8 operational, packaging, stress, and release
gates needed for the Linux-server/Windows-client milestone.
Stop there for the current effort. When Unix-client implementation is explicitly
started later, continue with Phase 7 (Linux supervisor/cgroup and client parity),
then the remaining Phase 8 Unix operational/release gates to complete v1.
Stop there for v1. Any Unix-client implementation is a separately authorized
post-v1 effort and does not change this milestone's completion criteria.
Do not merge a later vertical slice by stubbing a durability/safety invariant.
For example: foreground mode may wait on a durable background command, but must
@@ -2701,12 +2698,9 @@ not bypass persistence; client output may be truncated under the documented
caps, but must never block a child pipe; and a reconnection may replay work,
but may never re-execute an already accepted UUID.
The current Windows-client milestone is releasable only after Phases 0–6 plus its
applicable Phase 8 packaging/security gates pass on native Windows and a clean
Linux server environment. Windows support cannot be marked optional or replaced
by cross-compilation-only checks. Stop the current implementation effort at that
milestone; Phase 7 remains deferred until explicitly started later. Full v1 is
ready only after that later Linux client also passes the common protocol/
durability suite and Linux-specific cgroup/process tests. Every release must
The Linux-server/Windows-client v1 milestone is releasable only after Phases 0–6
plus its applicable Phase 8 packaging/security gates pass on native Windows and
a clean Linux server environment. Windows support cannot be marked optional or
replaced by cross-compilation-only checks. Every release must
conspicuously document self-reported identity, the elevated Windows execution
authority, and unauthenticated optional JSON-RPC.
+74
View File
@@ -0,0 +1,74 @@
# RVBox Linux-server operations runbook
This runbook covers the v1 Linux server and native Windows clients. It does not
describe a Unix-like client, which is outside v1.
## Deploy and verify
Use either the checked-in Compose deployment or the systemd unit, never both
for the same server data directory. Start from the annotated
[`server.toml`](examples/server.toml) and retain `json_rpc.enabled = false`
unless performing loopback-only debugging.
For Compose, follow the setup in
[`deploy/production/README.md`](../deploy/production/README.md), set a pinned
`RVBOX_SERVER_IMAGE`, `RVBOX_TLS_CERT`, and `RVBOX_TLS_KEY`, then run:
```sh
docker compose -f deploy/production/compose.yaml config
docker compose -f deploy/production/compose.yaml up -d
docker compose -f deploy/production/compose.yaml ps
```
The first command must succeed before any containers start. `/livez` shows that
the process is up; `/readyz` becomes successful only after durable recovery.
The public endpoint accepts only `wss://HOST/v1/agent`. Do not publish port
6900 or add a proxy route for JSON-RPC.
For systemd, create the `rvbox` service account, install the binary and
`deploy/systemd/rvbox-server.service`, place a root:`rvbox` owned `0640`
`/etc/rvbox/server.toml`, then run `systemctl daemon-reload` and
`systemctl enable --now rvbox-server`. The unit performs `--check-config`
before every start.
## Backup and restore
Stop dispatch before copying data: stop the server gracefully, confirm it is
down, then copy the entire configured `server.data_dir` tree. That tree contains
SQLite, WAL/SHM state, retained command segments, audit segments, and incidents;
backing up SQLite alone is incomplete.
To restore, keep the server stopped, move the failed directory aside without
deleting it, restore the complete backup with ownership restricted to the
server account, and run `rvbox-server --check-config` followed by a normal
start. Keep readiness under observation. A failed recovery leaves readiness
false and records an incident; do not delete segments to force readiness.
## Upgrade and rollback
1. Record the running image/binary digest and `rvc stat` output.
2. Stop the server gracefully so no new dispatch is accepted.
3. Take a full data-directory backup as above.
4. Install the new image/binary without changing configuration, and run
`--check-config` before starting it.
5. Start, wait for `/readyz`, and verify `rvc stat` plus one known client
reconnect.
6. If readiness or storage recovery fails, stop, restore the prior binary/image
and full data directory, then start the known-good version. Preserve logs
and the failed copy for diagnosis.
## Common incidents
- **No client / stale session:** verify nginx has WebSocket `101` entries for
`/v1/agent`, server readiness is true, and the Windows service is running.
Use `rvc stat CLIENT_ID`; do not restart the client merely to clear history.
- **Spool or storage full:** `CAPACITY_EXHAUSTED` is intentional admission
protection. Inspect command retention and free space, allow terminal-age or
quota rotation to reclaim eligible data, or enlarge the owned filesystem.
Never manually remove live SQLite/WAL/segment files.
- **Output truncated:** query command status and output history for its explicit
loss/truncation markers. The command may still have completed correctly.
- **Server restart / dirty health:** wait for readiness and inspect the
incident record. Use the documented repair/acknowledgement controls only
after preserving evidence; a late client report is authoritative and is not
rewritten to match an earlier provisional state.
+3 -3
View File
@@ -4,9 +4,9 @@ Daemon configuration uses strict TOML as specified in
[`configuration.md`](configuration.md); the annotated examples contain every
v1 knob and default.
The current implementation scope is the Linux server plus Windows client. The
Unix-like client remains part of complete v1 but is deferred and must not be
implemented yet. Its section below preserves the agreed future v1 contract; the
The v1 implementation scope is the Linux server plus Windows client. A
Unix-like client is post-v1 work and must not be implemented as part of this
milestone. Its section below is retained only as future design material; the
document order does not authorize or reprioritize that work.
## Unix-like clients
+4
View File
@@ -99,6 +99,8 @@ respective durability or wire boundary:
```sh
scripts/test-env doctor
scripts/test-integration --suite sample --run-id my-sample
scripts/test-integration --list
scripts/test-integration --suite store --case '^TestStore' --run-id store-one-case
scripts/test-env status --run-id my-sample
scripts/test-env collect --run-id my-sample
scripts/test-env reset --run-id my-sample
@@ -120,6 +122,8 @@ scripts/test-env reset --run-id session-smoke
scripts/test-env purge --run-id session-smoke
scripts/test-e2e --scenario smoke --run-id e2e-smoke
scripts/test-e2e --list
scripts/test-e2e --scenario smoke --case '^Test' --run-id e2e-one-case
scripts/test-env status --run-id e2e-smoke
scripts/test-env recover --run-id e2e-smoke
scripts/test-e2e --scenario smoke --run-id e2e-smoke --resume
+105 -18
View File
@@ -38,6 +38,7 @@ type manifest struct {
RunID string `toml:"run_id" json:"run_id"`
Layer string `toml:"layer" json:"layer"`
Suite string `toml:"suite" json:"suite"`
TestCase string `toml:"test_case,omitempty" json:"test_case,omitempty"`
Seed int64 `toml:"seed" json:"seed"`
GitCommit string `toml:"git_commit" json:"git_commit"`
DirtyDiffSHA256 string `toml:"dirty_diff_sha256" json:"dirty_diff_sha256"`
@@ -63,6 +64,40 @@ type harness struct {
out io.Writer
}
var integrationSuites = []string{"sample", "store", "server-session"}
var e2eScenarios = []string{"smoke", "script", "recovery", "all"}
func containsChoice(choices []string, value string) bool {
for _, choice := range choices {
if choice == value {
return true
}
}
return false
}
func printChoices(out io.Writer, heading string, choices []string) error {
if _, err := fmt.Fprintf(out, "%s:\n", heading); err != nil {
return err
}
for _, choice := range choices {
if _, err := fmt.Fprintf(out, "%s\n", choice); err != nil {
return err
}
}
return nil
}
func validateTestCase(value string) error {
if value == "" {
return nil
}
if _, err := regexp.Compile(value); err != nil {
return fmt.Errorf("--case must be a valid Go test regexp: %w", err)
}
return nil
}
func runCLI(ctx context.Context, args []string) error {
repoRoot, err := os.Getwd()
if err != nil {
@@ -123,13 +158,27 @@ func (h *harness) integration(ctx context.Context, args []string) error {
flags := flag.NewFlagSet("integration", flag.ContinueOnError)
flags.SetOutput(io.Discard)
suite := flags.String("suite", "sample", "suite name")
list := flags.Bool("list", false, "list stable suites")
testCase := flags.String("case", "", "Go test name regexp for one suite")
runID := flags.String("run-id", "", "run ID")
resume := flags.Bool("resume", false, "resume an existing run")
if err := flags.Parse(args); err != nil {
return err
}
if *suite != "sample" && *suite != "store" && *suite != "server-session" {
return fmt.Errorf("suite %q is not implemented yet; available: sample, store, server-session", *suite)
if *list {
if flags.NArg() != 0 || *runID != "" || *resume || *testCase != "" || *suite != "sample" {
return errors.New("--list cannot be combined with run options")
}
return printChoices(h.out, "integration suites", integrationSuites)
}
if !containsChoice(integrationSuites, *suite) {
return fmt.Errorf("suite %q is not implemented yet; available: %s", *suite, strings.Join(integrationSuites, ", "))
}
if err := validateTestCase(*testCase); err != nil {
return err
}
if *suite == "sample" && *testCase != "" {
return errors.New("--case is only supported by executable integration suites")
}
var current *manifest
@@ -145,6 +194,9 @@ func (h *harness) integration(ctx context.Context, args []string) error {
if current.Layer != "integration" || current.Suite != *suite {
return errors.New("run layer/suite does not match resume request")
}
if current.TestCase != *testCase {
return errors.New("run test case does not match resume request")
}
if current.Phase != "ready" && current.Phase != "interrupted" && current.Phase != "stopped" && current.Phase != "running" && current.Phase != "failed" {
return fmt.Errorf("run in phase %q is not resumable; recover or reuse it first", current.Phase)
}
@@ -154,6 +206,10 @@ func (h *harness) integration(ctx context.Context, args []string) error {
return err
}
}
current.TestCase = *testCase
if err := h.writeManifest(current); err != nil {
return err
}
fmt.Fprintln(h.out, current.RunID)
if err := h.transition(current, "running", *suite+"-start", *suite+" integration run started"); err != nil {
return err
@@ -172,9 +228,9 @@ func (h *harness) integration(ctx context.Context, args []string) error {
var suiteErr error
switch *suite {
case "store":
suiteErr = h.runStoreSuite(ctx, current)
suiteErr = h.runStoreSuite(ctx, current, *testCase)
case "server-session":
suiteErr = h.runServerSessionSuite(ctx, current)
suiteErr = h.runServerSessionSuite(ctx, current, *testCase)
}
if suiteErr != nil {
_ = h.transition(current, "failed", *suite+"-failed", suiteErr.Error())
@@ -192,14 +248,28 @@ func (h *harness) integration(ctx context.Context, args []string) error {
func (h *harness) e2e(ctx context.Context, args []string) error {
flags := flag.NewFlagSet("e2e", flag.ContinueOnError)
flags.SetOutput(io.Discard)
scenario := flags.String("scenario", "smoke", "scenario name: smoke, script, recovery, or all")
scenario := flags.String("scenario", "smoke", "scenario name")
list := flags.Bool("list", false, "list stable scenarios")
testCase := flags.String("case", "", "Go test name regexp for one scenario")
runID := flags.String("run-id", "", "run ID")
resume := flags.Bool("resume", false, "resume an existing run")
if err := flags.Parse(args); err != nil {
return err
}
if *scenario != "smoke" && *scenario != "script" && *scenario != "recovery" && *scenario != "all" {
return fmt.Errorf("scenario %q is not implemented yet; available: smoke, script, recovery, all", *scenario)
if *list {
if flags.NArg() != 0 || *runID != "" || *resume || *testCase != "" || *scenario != "smoke" {
return errors.New("--list cannot be combined with run options")
}
return printChoices(h.out, "e2e scenarios", e2eScenarios)
}
if !containsChoice(e2eScenarios, *scenario) {
return fmt.Errorf("scenario %q is not implemented yet; available: %s", *scenario, strings.Join(e2eScenarios, ", "))
}
if err := validateTestCase(*testCase); err != nil {
return err
}
if *scenario == "all" && *testCase != "" {
return errors.New("--case requires one named e2e scenario")
}
var current *manifest
var err error
@@ -214,6 +284,9 @@ func (h *harness) e2e(ctx context.Context, args []string) error {
if current.Layer != "e2e" || current.Suite != *scenario {
return errors.New("run layer/scenario does not match resume request")
}
if current.TestCase != *testCase {
return errors.New("run test case does not match resume request")
}
if current.Phase != "ready" && current.Phase != "interrupted" && current.Phase != "stopped" && current.Phase != "running" && current.Phase != "failed" {
return fmt.Errorf("run in phase %q is not resumable; recover or reuse it first", current.Phase)
}
@@ -223,6 +296,10 @@ func (h *harness) e2e(ctx context.Context, args []string) error {
return err
}
}
current.TestCase = *testCase
if err := h.writeManifest(current); err != nil {
return err
}
fmt.Fprintln(h.out, current.RunID)
if err := h.transition(current, "running", "e2e-start", "e2e scenario started"); err != nil {
return err
@@ -232,7 +309,7 @@ func (h *harness) e2e(ctx context.Context, args []string) error {
scenarios = []string{"smoke", "script", "recovery"}
}
for _, item := range scenarios {
if err := h.runE2EScenario(ctx, current, item); err != nil {
if err := h.runE2EScenario(ctx, current, item, *testCase); err != nil {
_ = h.transition(current, "failed", "e2e-"+item+"-failed", err.Error())
return err
}
@@ -240,18 +317,18 @@ func (h *harness) e2e(ctx context.Context, args []string) error {
return h.transition(current, "completed", "e2e-complete", "e2e scenario completed")
}
func (h *harness) runE2EScenario(ctx context.Context, current *manifest, scenario string) error {
func (h *harness) runE2EScenario(ctx context.Context, current *manifest, scenario, testCase string) error {
if err := h.appendJournal(current.RunID, journalEntry{At: h.now(), Step: "e2e-" + scenario, Status: "running", Detail: "scenario started"}); err != nil {
return err
}
var err error
switch scenario {
case "smoke":
err = h.runGoSuite(ctx, current, "e2e-client-agent", "real client/server WebSocket and control flow", "./test/integration/clientagent")
err = h.runGoSuite(ctx, current, "e2e-client-agent", "real client/server WebSocket and control flow", "./test/integration/clientagent", testCase)
case "script":
err = h.runGoSuite(ctx, current, "e2e-script-transfer", "durable script transfer and replay", "./internal/client/agent")
err = h.runGoSuite(ctx, current, "e2e-script-transfer", "durable script transfer and replay", "./internal/client/agent", testCase)
case "recovery":
err = h.runStoreSuite(ctx, current)
err = h.runStoreSuite(ctx, current, testCase)
default:
err = fmt.Errorf("unknown e2e scenario %q", scenario)
}
@@ -261,7 +338,7 @@ func (h *harness) runE2EScenario(ctx context.Context, current *manifest, scenari
return h.appendJournal(current.RunID, journalEntry{At: h.now(), Step: "e2e-" + scenario, Status: "passed", Detail: "scenario passed"})
}
func (h *harness) runStoreSuite(ctx context.Context, current *manifest) error {
func (h *harness) runStoreSuite(ctx context.Context, current *manifest, testCase string) error {
if err := h.appendJournal(current.RunID, journalEntry{At: h.now(), Step: "store-real-sqlite", Status: "running", Detail: "running real SQLite/WAL and filesystem cases"}); err != nil {
return err
}
@@ -270,7 +347,12 @@ func (h *harness) runStoreSuite(ctx context.Context, current *manifest) error {
return err
}
capture := &limitedCapture{limit: maxSuiteLogSize}
command := exec.CommandContext(ctx, "go", "test", "-count=1", "-tags=integration", "-shuffle="+strconv.FormatInt(current.Seed, 10), "-timeout=2m", "./test/integration/store")
arguments := []string{"test", "-count=1", "-tags=integration", "-shuffle=" + strconv.FormatInt(current.Seed, 10), "-timeout=2m"}
if testCase != "" {
arguments = append(arguments, "-run", testCase)
}
arguments = append(arguments, "./test/integration/store")
command := exec.CommandContext(ctx, "go", arguments...)
command.Stdout = capture
command.Stderr = capture
err := command.Run()
@@ -284,11 +366,11 @@ func (h *harness) runStoreSuite(ctx context.Context, current *manifest) error {
return h.appendJournal(current.RunID, journalEntry{At: h.now(), Step: "store-real-sqlite", Status: "passed", Detail: "real SQLite/WAL and filesystem cases passed"})
}
func (h *harness) runServerSessionSuite(ctx context.Context, current *manifest) error {
return h.runGoSuite(ctx, current, "server-session-real-websocket", "running real HTTP/WebSocket, protobuf, SQLite, and fencing cases", "./internal/server/session")
func (h *harness) runServerSessionSuite(ctx context.Context, current *manifest, testCase string) error {
return h.runGoSuite(ctx, current, "server-session-real-websocket", "running real HTTP/WebSocket, protobuf, SQLite, and fencing cases", "./internal/server/session", testCase)
}
func (h *harness) runGoSuite(ctx context.Context, current *manifest, step, detail, packagePath string) error {
func (h *harness) runGoSuite(ctx context.Context, current *manifest, step, detail, packagePath, testCase string) error {
if err := h.appendJournal(current.RunID, journalEntry{At: h.now(), Step: step, Status: "running", Detail: detail}); err != nil {
return err
}
@@ -297,7 +379,12 @@ func (h *harness) runGoSuite(ctx context.Context, current *manifest, step, detai
return err
}
capture := &limitedCapture{limit: maxSuiteLogSize}
command := exec.CommandContext(ctx, "go", "test", "-race", "-count=1", "-shuffle="+strconv.FormatInt(current.Seed, 10), "-timeout=2m", packagePath)
arguments := []string{"test", "-race", "-count=1", "-shuffle=" + strconv.FormatInt(current.Seed, 10), "-timeout=2m"}
if testCase != "" {
arguments = append(arguments, "-run", testCase)
}
arguments = append(arguments, packagePath)
command := exec.CommandContext(ctx, "go", arguments...)
command.Stdout = capture
command.Stderr = capture
err := command.Run()
+26
View File
@@ -123,6 +123,32 @@ func TestE2EScenarioValidationAndRunIdentity_HP_E2E_01(t *testing.T) {
}
}
func TestStableSuiteAndScenarioListing_HP_CFG_01(t *testing.T) {
t.Parallel()
var output bytes.Buffer
h := &harness{root: t.TempDir(), now: time.Now, out: &output}
if err := h.integration(context.Background(), []string{"--list"}); err != nil {
t.Fatalf("list integration suites: %v", err)
}
if got, want := output.String(), "integration suites:\nsample\nstore\nserver-session\n"; got != want {
t.Fatalf("integration list = %q, want %q", got, want)
}
output.Reset()
if err := h.e2e(context.Background(), []string{"--list"}); err != nil {
t.Fatalf("list e2e scenarios: %v", err)
}
if got, want := output.String(), "e2e scenarios:\nsmoke\nscript\nrecovery\nall\n"; got != want {
t.Fatalf("e2e list = %q, want %q", got, want)
}
if err := h.integration(context.Background(), []string{"--suite", "store", "--case", "["}); err == nil {
t.Fatal("invalid --case regexp succeeded")
}
if err := h.e2e(context.Background(), []string{"--scenario", "all", "--case", "Test"}); err == nil {
t.Fatal("--case with all scenarios succeeded")
}
}
func TestBoundedSuiteLogAndFailedRunRecovery_BH_STORE_03(t *testing.T) {
t.Parallel()