From 7f055efc0f8c5718c05828a37eb60411723082d0 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 14 Apr 2026 17:09:36 -0400 Subject: [PATCH 01/36] Replace WriteWithoutResponse with Write --- cmd/provisioning-client/bluetooth.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/provisioning-client/bluetooth.go b/cmd/provisioning-client/bluetooth.go index 75b7be2e..0bcf7cc3 100644 --- a/cmd/provisioning-client/bluetooth.go +++ b/cmd/provisioning-client/bluetooth.go @@ -125,7 +125,7 @@ func ExitProvisioning(chars map[string]bluetooth.DeviceCharacteristic) error { return err } - _, err = chars[exitProvisioningKey].WriteWithoutResponse(cryptExit) + _, err = chars[exitProvisioningKey].Write(cryptExit) if err != nil { return errw.Wrap(err, "writing app address") } @@ -312,22 +312,22 @@ func BTSetDeviceCreds(chars map[string]bluetooth.DeviceCharacteristic) error { return err } - _, err = chars[robotPartIDKey].WriteWithoutResponse(cryptPartID) + _, err = chars[robotPartIDKey].Write(cryptPartID) if err != nil { return errw.Wrap(err, "writing part id") } - _, err = chars[robotPartSecretKey].WriteWithoutResponse(cryptSecret) + _, err = chars[robotPartSecretKey].Write(cryptSecret) if err != nil { return errw.Wrap(err, "writing secret") } - _, err = chars[apiKeyCredsKey].WriteWithoutResponse(cryptAPIKey) + _, err = chars[apiKeyCredsKey].Write(cryptAPIKey) if err != nil { return errw.Wrap(err, "writing api key") } - _, err = chars[appAddressKey].WriteWithoutResponse(cryptAppAddr) + _, err = chars[appAddressKey].Write(cryptAppAddr) if err != nil { return errw.Wrap(err, "writing app address") } @@ -351,12 +351,12 @@ func BTSetWifiCreds(chars map[string]bluetooth.DeviceCharacteristic) error { return err } - _, err = chars[ssidKey].WriteWithoutResponse(cryptSSID) + _, err = chars[ssidKey].Write(cryptSSID) if err != nil { return errw.Wrap(err, "writing ssid") } - _, err = chars[pskKey].WriteWithoutResponse(cryptPSK) + _, err = chars[pskKey].Write(cryptPSK) if err != nil { return errw.Wrap(err, "writing psk") } @@ -374,7 +374,7 @@ func BTUnlockPairing(chars map[string]bluetooth.DeviceCharacteristic) error { return err } - _, err = chars[unlockPairingKey].WriteWithoutResponse(cryptAddr) + _, err = chars[unlockPairingKey].Write(cryptAddr) if err != nil { return errw.Wrap(err, "writing unlock pairing request") } @@ -393,7 +393,7 @@ func BTExitProvisioning(chars map[string]bluetooth.DeviceCharacteristic) error { return err } - _, err = chars[exitProvisioningKey].WriteWithoutResponse(cryptExit) + _, err = chars[exitProvisioningKey].Write(cryptExit) if err != nil { return errw.Wrap(err, "writing exit command") } From e6fc415721ff961c5541af89825d65acfb0ff6d1 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 14 Apr 2026 17:22:44 -0400 Subject: [PATCH 02/36] Change some terminology, add ble test cases --- agent_serial_test.go | 14 +++++++++++--- features/serial/provision-ble.feature | 13 +++++++++++++ features/serial/provision-wifi.feature | 8 ++++---- 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 features/serial/provision-ble.feature diff --git a/agent_serial_test.go b/agent_serial_test.go index 25aebb5a..b3bda07f 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -195,13 +195,17 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Step(`viam-agent is connected to a network`, testEnsureOnline) ctx.Step(`viam-agent is in forced provisioning mode`, testForceProvisioningMode) ctx.Step(`the provisioning hotspot (is|comes) up`, testProvisioningHotspotEnablesWithinTimeout) - ctx.Step(`the tester shares a secure wifi network`, testSendSecureConnectionInfo) - ctx.Step(`the tester shares an insecure wifi network`, testSendInsecureConnectionInfo) - ctx.Step(`the tester shares an invalid wifi network`, testSendInvalidConnectionInfo) + ctx.Step(`the host shares a secure wifi network via the hotspot`, testSendSecureConnectionInfo) + ctx.Step(`the host shares an insecure wifi network via the hotspot`, testSendInsecureConnectionInfo) + ctx.Step(`the host shares an invalid wifi network via the hotspot`, testSendInvalidConnectionInfo) ctx.Step(`the provisioning hotspot (goes away|is not up)`, testProvisioningHotspotDisables) ctx.Step(`viam-agent can reach the app`, testAgentCanReachApp) ctx.Step(`viam-agent cannot reach the app`, testAgentCannotReachApp) + // Bluetooth provisioning + ctx.Step(`the viam-agent bluetooth device is discoverable`, testBleIsDiscoverable) + ctx.Step(`the host shares a secure wifi network via bluetooth`, testBleSendSecureConnectionInfo) + // Agent upgrade/downgrade steps (version/URL/file) ctx.Step(fmt.Sprintf(`the viam-agent systemd unit is running with %s$`, versionGroup), testAgentRunningWithVersion) ctx.Step(fmt.Sprintf(`the viam-agent systemd unit started with %s`, versionGroup), testSystemdAgentStartVersion) @@ -650,6 +654,10 @@ func testSendSecureConnectionInfo(ctx context.Context) (context.Context, error) return ctx, sendNetworkCredentials(ctx, cfg.Wifi.SSID, cfg.Wifi.Password) } +func testBleSendSecureConnectionInfo(ctx context.Context) (context.Context, error) { + +} + func testSendInvalidConnectionInfo(ctx context.Context) (context.Context, error) { return ctx, sendNetworkCredentials(ctx, "thisnetwork", "doesnotexist") } diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature new file mode 100644 index 00000000..ec2d3c0f --- /dev/null +++ b/features/serial/provision-ble.feature @@ -0,0 +1,13 @@ +@darwin +Feature: bluetooth provisioning + Background: + Given viam-agent is installed + And the viam-agent systemd unit is enabled + And the viam-agent systemd unit is running + And there are no available wifi networks + And viam-agent cannot reach the app + Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning + When viam-agent is in forced provisioning mode + # And the viam-agent bluetooth device is discoverable + And the host shares a secure wifi network via bluetooth + \ No newline at end of file diff --git a/features/serial/provision-wifi.feature b/features/serial/provision-wifi.feature index bc63ca80..58373432 100644 --- a/features/serial/provision-wifi.feature +++ b/features/serial/provision-wifi.feature @@ -12,25 +12,25 @@ Feature: wifi provisioning Scenario: The agent can join an unknown insecure network when one is provided during wifi hotspot provisioning When viam-agent is in forced provisioning mode And the provisioning hotspot comes up - And the tester shares an insecure wifi network + And the host shares an insecure wifi network via the hotspot Then the provisioning hotspot goes away And viam-agent can reach the app Scenario: The agent can join an unknown secure network when one is provided during wifi hotspot provisioning When viam-agent is in forced provisioning mode And the provisioning hotspot comes up - And the tester shares a secure wifi network + And the host shares a secure wifi network via the hotspot Then the provisioning hotspot goes away And viam-agent can reach the app Scenario: The agent can join a known secure network when one is provided during wifi hotspot provisioning When viam-agent is connected to a network And viam-agent is in forced provisioning mode And the provisioning hotspot comes up - And the tester shares a secure wifi network + And the host shares a secure wifi network via the hotspot Then the provisioning hotspot goes away And viam-agent can reach the app Scenario: Fail to connect to a network and revert to provisioning hotspot mode When viam-agent is in forced provisioning mode And the provisioning hotspot comes up - And the tester shares an invalid wifi network + And the host shares an invalid wifi network via the hotspot Then the provisioning hotspot comes up again And viam-agent cannot reach the app From 6a4f2844202a78391cecefb9915ec91317cb2cbf Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 14 Apr 2026 19:07:33 -0400 Subject: [PATCH 03/36] mvp bluetooth provisioning test --- agent_serial_test.go | 48 +++++++++++++++++++++++++++ features/serial/provision-ble.feature | 4 +-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index b3bda07f..94b24a7b 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -654,8 +654,56 @@ func testSendSecureConnectionInfo(ctx context.Context) (context.Context, error) return ctx, sendNetworkCredentials(ctx, cfg.Wifi.SSID, cfg.Wifi.Password) } +func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { + filter := fmt.Sprintf("viam-setup-%s", hostName) + + var lastErr error + // don't need much in the way of retries because the client already tries for 30 seconds + for range 3 { + if lastErr != nil { + time.Sleep(1 * time.Second) + } + cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", + "-b", + "--status", + "--filter", filter, + ) + out, lastErr := cmd.CombinedOutput() + if lastErr != nil { + lastErr = fmt.Errorf("BLE device not discoverable: %w\n%s", lastErr, out) + continue + } + return ctx, nil + } + return ctx, lastErr +} + func testBleSendSecureConnectionInfo(ctx context.Context) (context.Context, error) { + robotKeysResp, err := appClient.GetRobotAPIKeys(ctx, &apppb.GetRobotAPIKeysRequest{ + RobotId: cfg.RobotID, + }) + if err != nil { + return ctx, fmt.Errorf("getting robot API keys: %w", err) + } + robotKeys := robotKeysResp.ApiKeys[0] + cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", + "-b", + "--filter", fmt.Sprintf("viam-setup-%s", hostName), + "--psk", "viamsetup", + "--wifi-ssid", cfg.Wifi.SSID, + "--wifi-psk", cfg.Wifi.Password, + "--part-id", cfg.PartID, + "--api-key-id", robotKeys.ApiKey.Id, + "--api-key-key", robotKeys.ApiKey.Key, + ) + out, err := cmd.CombinedOutput() + if err != nil { + return ctx, fmt.Errorf("BLE provisioning failed: %w\n%s", err, out) + } + // wait a bit after sending the network before going on + time.Sleep(5 * time.Second) + return ctx, nil } func testSendInvalidConnectionInfo(ctx context.Context) (context.Context, error) { diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature index ec2d3c0f..90bfee95 100644 --- a/features/serial/provision-ble.feature +++ b/features/serial/provision-ble.feature @@ -8,6 +8,6 @@ Feature: bluetooth provisioning And viam-agent cannot reach the app Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning When viam-agent is in forced provisioning mode - # And the viam-agent bluetooth device is discoverable + And the viam-agent bluetooth device is discoverable And the host shares a secure wifi network via bluetooth - \ No newline at end of file + Then viam-agent can reach the app \ No newline at end of file From 3287b7841bf13e51ab68389ad7449d40f8d75921 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 22 Apr 2026 11:55:52 -0400 Subject: [PATCH 04/36] Bump tinygo/bluetooth 0.11.0 -> 0.15.0 --- go.mod | 11 ++++++----- go.sum | 22 ++++++++++++---------- mise.toml | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 87892eee..81deaf9d 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( golang.org/x/sys v0.39.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.10 - tinygo.org/x/bluetooth v0.11.0 + tinygo.org/x/bluetooth v0.15.0 ) require ( @@ -100,15 +100,16 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect + github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5 // indirect - github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect + github.com/soypat/cyw43439 v0.1.0 // indirect + github.com/soypat/lneto v0.1.0 // indirect + github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/srikrsna/protoc-gen-gotag v0.6.2 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tinygo-org/cbgo v0.0.4 // indirect - github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect + github.com/tinygo-org/pio v0.3.0 // indirect github.com/viamrobotics/ice/v2 v2.3.40 // indirect github.com/viamrobotics/webrtc/v3 v3.99.16 // indirect github.com/viamrobotics/zeroconf v1.0.13 // indirect diff --git a/go.sum b/go.sum index f56953a6..f48e084f 100644 --- a/go.sum +++ b/go.sum @@ -539,8 +539,8 @@ github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik= -github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= +github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 h1:IXxzj3yjfDNXZJ35foY+RpFShqPsZZ81hhCckgfh5PI= +github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/mo v1.16.0 h1:qpEPCI63ou6wXlsNDMLE0IIN8A+devbGX/K1xdgr4b4= @@ -563,10 +563,12 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5 h1:arwJFX1x5zq+wUp5ADGgudhMQEXKNMQOmTh+yYgkwzw= -github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE= -github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4= -github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= +github.com/soypat/cyw43439 v0.1.0 h1:3Nyqg2LSndhCYgCr2VXuL2nn73vyaJXAnD02veMoLvA= +github.com/soypat/cyw43439 v0.1.0/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc= +github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I= +github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U= +github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA= +github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -608,8 +610,8 @@ github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc= github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU= github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= -github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ= -github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8= +github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI= +github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= @@ -990,5 +992,5 @@ nhooyr.io/websocket v1.8.9 h1:+U/9DCNIH1XnzrWKs7yZp4jO0e/m6mUEh2kRPKRQYeg= nhooyr.io/websocket v1.8.9/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= -tinygo.org/x/bluetooth v0.11.0 h1:32ludjNnqz6RyVRpmw2qgod7NvDePbBTWXkJm6jj4cg= -tinygo.org/x/bluetooth v0.11.0/go.mod h1:XLRopLvxWmIbofpZSXc7BGGCpgFOV5lrZ1i/DQN0BCw= +tinygo.org/x/bluetooth v0.15.0 h1:hLn8+iZFXvVxBzPIdZfvc6TD8JP32ixF22lCEWHAbIo= +tinygo.org/x/bluetooth v0.15.0/go.mod h1:meayNB+9rC1igTUNmNU7KftlSEzrFHe37rBSQZjHN8Y= diff --git a/mise.toml b/mise.toml index 81edb612..b575cb17 100644 --- a/mise.toml +++ b/mise.toml @@ -14,4 +14,4 @@ run = [ [tasks.test-e2e-serial] description = "Run tests on a real device connected via serial. See readme for setup requirements." -run = "go test . -count 1 -v -timeout 20m -run TestSerialFeatures -tags=serialtests" +run = "go test . -count 1 -v -timeout 30m -run TestSerialFeatures -tags=serialtests" From 32ede49d522cf7fdc176fab910baba13e7d7d565 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 28 Apr 2026 11:01:18 -0400 Subject: [PATCH 05/36] Refactor, new scenario, check ble characteristics --- agent_serial_test.go | 94 ++++++++++++++++++--------- features/serial/provision-ble.feature | 10 ++- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 94b24a7b..1b042de2 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -71,6 +71,26 @@ type wifiCfg struct { SSIDInsecure string `toml:"ssid_insecure"` } +// expected agent Bluetooth Low Energy characteristics +var agentBleChars = []string{ + "61eba4df-b901-502c-b278-fadf9d52802b (networks)", + "37a720ee-86e5-55a8-b876-d200fb7e4f72 (ssid)", + "10cd57f1-01bb-5937-89fb-64cc40be53d2 (exit_provisioning)", + "49d34f00-cf76-55fa-9f8d-23f6836136ab (manufacturer)", + "ea8a8689-548f-5941-829b-82aeff8095b7 (status)", + "96a4bebb-a361-5c73-9d76-45a0faa9d4a0 (unlock_pairing)", + "c2be234e-1975-5e85-b97b-bb30c3bf43d2 (id)", + "444ee2b0-b3fa-5d74-bb35-f194642188b3 (app_address)", + "cd5b8fb9-4006-56e5-a78b-044cf6ae48cb (psk)", + "70bc8310-68ca-5011-9282-72f201293dfb (pub_key)", + "f30e26d6-155c-5a24-bd7f-6b1a40e463da (api_key)", + "d2eae9e8-30bc-5fdf-bd9f-bd4e8a4017d2 (fragment_id)", + "d0029d11-a8c2-5231-8fcc-83de66952f01 (secret)", + "6e164616-ee96-5835-b199-115ddbcd885f (agent_version)", + "e7e1ac15-fb59-54ab-8538-72008ad4ee43 (model)", + "75f64044-b604-5547-a723-7f8618255ddc (errors)", +} + var cfg config func TestSerialFeatures(t *testing.T) { @@ -80,7 +100,7 @@ func TestSerialFeatures(t *testing.T) { Options: &godog.Options{ // Options at time of writing: cucumber, events, junit, pretty, progress Format: "pretty", - Paths: []string{"features/serial"}, + Paths: []string{"features/serial/provision-ble.feature"}, Tags: serialTestTags(), TestingT: t, Strict: true, @@ -205,6 +225,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) { // Bluetooth provisioning ctx.Step(`the viam-agent bluetooth device is discoverable`, testBleIsDiscoverable) ctx.Step(`the host shares a secure wifi network via bluetooth`, testBleSendSecureConnectionInfo) + ctx.Step(`the host shares an invalid wifi network via bluetooth`, testBleSendInvalidConnectionInfo) // Agent upgrade/downgrade steps (version/URL/file) ctx.Step(fmt.Sprintf(`the viam-agent systemd unit is running with %s$`, versionGroup), testAgentRunningWithVersion) @@ -646,6 +667,34 @@ func sendNetworkCredentials(ctx context.Context, ssid, psk string) error { return lastErr } +func sendNetworkCredentialsBle(ctx context.Context, ssid, psk string) error { + robotKeysResp, err := appClient.GetRobotAPIKeys(ctx, &apppb.GetRobotAPIKeysRequest{ + RobotId: cfg.RobotID, + }) + if err != nil { + return fmt.Errorf("getting robot API keys: %w", err) + } + robotKeys := robotKeysResp.ApiKeys[0] + + cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", + "-b", + "--filter", fmt.Sprintf("viam-setup-%s", hostName), + "--psk", "viamsetup", + "--wifi-ssid", ssid, + "--wifi-psk", psk, + "--part-id", cfg.PartID, + "--api-key-id", robotKeys.ApiKey.Id, + "--api-key-key", robotKeys.ApiKey.Key, + ) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("BLE provisioning failed: %w\n%s", err, out) + } + // wait a bit after sending the network before going on + time.Sleep(5 * time.Second) + return nil +} + func testSendInsecureConnectionInfo(ctx context.Context) (context.Context, error) { return ctx, sendNetworkCredentials(ctx, cfg.Wifi.SSIDInsecure, "") } @@ -660,17 +709,22 @@ func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { var lastErr error // don't need much in the way of retries because the client already tries for 30 seconds for range 3 { - if lastErr != nil { - time.Sleep(1 * time.Second) - } cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", "-b", "--status", "--filter", filter, ) out, lastErr := cmd.CombinedOutput() + // check for all the expected BLE characteristics + for _, char := range agentBleChars { + for _, line := range out { + if !strings.Contains(string(line), char) { + lastErr = fmt.Errorf("discovered BLE device missing characteristic: %s\n", char) + } + } + } if lastErr != nil { - lastErr = fmt.Errorf("BLE device not discoverable: %w\n%s", lastErr, out) + lastErr = fmt.Errorf("BLE discovery failed: %w\n%s", lastErr, out) continue } return ctx, nil @@ -679,37 +733,17 @@ func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { } func testBleSendSecureConnectionInfo(ctx context.Context) (context.Context, error) { - robotKeysResp, err := appClient.GetRobotAPIKeys(ctx, &apppb.GetRobotAPIKeysRequest{ - RobotId: cfg.RobotID, - }) - if err != nil { - return ctx, fmt.Errorf("getting robot API keys: %w", err) - } - robotKeys := robotKeysResp.ApiKeys[0] - - cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", - "-b", - "--filter", fmt.Sprintf("viam-setup-%s", hostName), - "--psk", "viamsetup", - "--wifi-ssid", cfg.Wifi.SSID, - "--wifi-psk", cfg.Wifi.Password, - "--part-id", cfg.PartID, - "--api-key-id", robotKeys.ApiKey.Id, - "--api-key-key", robotKeys.ApiKey.Key, - ) - out, err := cmd.CombinedOutput() - if err != nil { - return ctx, fmt.Errorf("BLE provisioning failed: %w\n%s", err, out) - } - // wait a bit after sending the network before going on - time.Sleep(5 * time.Second) - return ctx, nil + return ctx, sendNetworkCredentialsBle(ctx, cfg.Wifi.SSID, cfg.Wifi.Password) } func testSendInvalidConnectionInfo(ctx context.Context) (context.Context, error) { return ctx, sendNetworkCredentials(ctx, "thisnetwork", "doesnotexist") } +func testBleSendInvalidConnectionInfo(ctx context.Context) (context.Context, error) { + return ctx, sendNetworkCredentialsBle(ctx, "thisnetwork", "doesnotexist") +} + func installAgent(ctx context.Context) (context.Context, error) { agentStatus := serialClient.GetAgentStatus().MustGet() if agentStatus["SubState"] == "running" { diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature index 90bfee95..c5cbe8fa 100644 --- a/features/serial/provision-ble.feature +++ b/features/serial/provision-ble.feature @@ -8,6 +8,12 @@ Feature: bluetooth provisioning And viam-agent cannot reach the app Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning When viam-agent is in forced provisioning mode - And the viam-agent bluetooth device is discoverable + And the viam-agent bluetooth device is discoverable and has the expected characteristics And the host shares a secure wifi network via bluetooth - Then viam-agent can reach the app \ No newline at end of file + Then viam-agent can reach the app + Scenario: The agent responds with an error when invalid network credentials are provided during bluetooth provisioning + When viam-agent is in forced provisioning mode + And the viam-agent bluetooth device is discoverable + And the host shares an invalid wifi network via bluetooth + Then the viam-agent bluetooth device is discoverable + And viam-agent cannot reach the app \ No newline at end of file From e2b70b1211fbaa9377a0a8c31c611dc4d2e5fc38 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 28 Apr 2026 13:29:13 -0400 Subject: [PATCH 06/36] Add more ble scenarios --- agent_serial_test.go | 61 ++++++++++++++++++++++----- features/serial/provision-ble.feature | 20 ++++++--- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 1b042de2..10f75f7e 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -93,6 +93,9 @@ var agentBleChars = []string{ var cfg config +// cache the last BLE status because performing BLE scans with the provisioning client is slow +var lastBleStatus []string + func TestSerialFeatures(t *testing.T) { suite := godog.TestSuite{ TestSuiteInitializer: InitializeSuite(t), @@ -100,7 +103,7 @@ func TestSerialFeatures(t *testing.T) { Options: &godog.Options{ // Options at time of writing: cucumber, events, junit, pretty, progress Format: "pretty", - Paths: []string{"features/serial/provision-ble.feature"}, + Paths: []string{"features/serial"}, Tags: serialTestTags(), TestingT: t, Strict: true, @@ -225,7 +228,10 @@ func InitializeScenario(ctx *godog.ScenarioContext) { // Bluetooth provisioning ctx.Step(`the viam-agent bluetooth device is discoverable`, testBleIsDiscoverable) ctx.Step(`the host shares a secure wifi network via bluetooth`, testBleSendSecureConnectionInfo) - ctx.Step(`the host shares an invalid wifi network via bluetooth`, testBleSendInvalidConnectionInfo) + ctx.Step(`the host shares invalid wifi credentials for a valid SSID via bluetooth`, testBleSendInvalidPasswordConnectionInfo) + ctx.Step(`the host shares an invalid SSID via bluetooth`, testBleSendInvalidSSIDConnectionInfo) + ctx.Step(`viam-agent surfaces an invalid SSID error via bluetooth`, testBleSurfacesInvalidSSIDError) + ctx.Step(`viam-agent surfaces an invalid credentials error via bluetooth`, testBleSurfacesInvalidCredentialsErr) // Agent upgrade/downgrade steps (version/URL/file) ctx.Step(fmt.Sprintf(`the viam-agent systemd unit is running with %s$`, versionGroup), testAgentRunningWithVersion) @@ -690,8 +696,6 @@ func sendNetworkCredentialsBle(ctx context.Context, ssid, psk string) error { if err != nil { return fmt.Errorf("BLE provisioning failed: %w\n%s", err, out) } - // wait a bit after sending the network before going on - time.Sleep(5 * time.Second) return nil } @@ -709,25 +713,35 @@ func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { var lastErr error // don't need much in the way of retries because the client already tries for 30 seconds for range 3 { + lastBleStatus = []string{} cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", "-b", "--status", + "--info", "--filter", filter, ) out, lastErr := cmd.CombinedOutput() + outString := strings.Split(string(out), "\n") + // check for all the expected BLE characteristics for _, char := range agentBleChars { - for _, line := range out { - if !strings.Contains(string(line), char) { - lastErr = fmt.Errorf("discovered BLE device missing characteristic: %s\n", char) + charFound := false + for _, line := range outString { + // cache the last BLE status here so it can be used to check for errors later + lastBleStatus = append(lastBleStatus, line) + if strings.Contains(line, char) { + charFound = true } } + if !charFound { + lastErr = fmt.Errorf("discovered BLE device missing characteristic: %s\n", char) + } } if lastErr != nil { - lastErr = fmt.Errorf("BLE discovery failed: %w\n%s", lastErr, out) continue + } else { + return ctx, nil } - return ctx, nil } return ctx, lastErr } @@ -740,8 +754,33 @@ func testSendInvalidConnectionInfo(ctx context.Context) (context.Context, error) return ctx, sendNetworkCredentials(ctx, "thisnetwork", "doesnotexist") } -func testBleSendInvalidConnectionInfo(ctx context.Context) (context.Context, error) { - return ctx, sendNetworkCredentialsBle(ctx, "thisnetwork", "doesnotexist") +func testBleSendInvalidPasswordConnectionInfo(ctx context.Context) (context.Context, error) { + return ctx, sendNetworkCredentialsBle(ctx, cfg.Wifi.SSID, "itdoesnotexist") +} + +func testBleSendInvalidSSIDConnectionInfo(ctx context.Context) (context.Context, error) { + return ctx, sendNetworkCredentialsBle(ctx, "thisnetworkisfake", "itdoesnotexist") +} + +func testBleSurfacesInvalidSSIDError(ctx context.Context) (context.Context, error) { + return ctx, bleSurfacesExpectedError("NmDeviceStateReasonSsidNotFound") +} + +func testBleSurfacesInvalidCredentialsErr(ctx context.Context) (context.Context, error) { + return ctx, bleSurfacesExpectedError("bad or missing password") +} + +func bleSurfacesExpectedError(expectedErr string) error { + for _, line := range lastBleStatus { + fmt.Printf("%s\n", line) + if strings.Contains(line, "Errors:") { + if strings.Contains(line, expectedErr) { + return nil + } + return fmt.Errorf("found error, but not expected error (%s): %s", expectedErr, line) + } + } + return fmt.Errorf("did not find any error (expected %s) in BLE info", expectedErr) } func installAgent(ctx context.Context) (context.Context, error) { diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature index c5cbe8fa..392cfa3c 100644 --- a/features/serial/provision-ble.feature +++ b/features/serial/provision-ble.feature @@ -6,14 +6,22 @@ Feature: bluetooth provisioning And the viam-agent systemd unit is running And there are no available wifi networks And viam-agent cannot reach the app - Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning + # Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning + # When viam-agent is in forced provisioning mode + # And the viam-agent bluetooth device is discoverable and has the expected characteristics + # And the host shares a secure wifi network via bluetooth + # Then viam-agent can reach the app + Scenario: The agent responds with an error when an invalid SSID is provided during bluetooth provisioning When viam-agent is in forced provisioning mode - And the viam-agent bluetooth device is discoverable and has the expected characteristics - And the host shares a secure wifi network via bluetooth - Then viam-agent can reach the app + And the viam-agent bluetooth device is discoverable + And the host shares an invalid SSID via bluetooth + Then the viam-agent bluetooth device is discoverable again + And viam-agent surfaces an invalid SSID error via bluetooth + And viam-agent cannot reach the app Scenario: The agent responds with an error when invalid network credentials are provided during bluetooth provisioning When viam-agent is in forced provisioning mode And the viam-agent bluetooth device is discoverable - And the host shares an invalid wifi network via bluetooth - Then the viam-agent bluetooth device is discoverable + And the host shares invalid wifi credentials for a valid SSID via bluetooth + Then the viam-agent bluetooth device is discoverable again + And viam-agent surfaces an invalid credentials error via bluetooth And viam-agent cannot reach the app \ No newline at end of file From 6dfe25a7fe367cec2fd5dc097ead252d2b3860f2 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 30 Apr 2026 12:36:14 -0400 Subject: [PATCH 07/36] Fix discover error handling, update ensureonline --- agent_serial_test.go | 42 ++++++++++++++++--------- features/serial/provision-ble.feature | 10 +++--- features/serial/provision-wifi.feature | 12 +++---- internal/serialcontrol/serialcontrol.go | 4 +++ 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 10f75f7e..4b17da98 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -712,7 +712,7 @@ func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { var lastErr error // don't need much in the way of retries because the client already tries for 30 seconds - for range 3 { + for range 4 { lastBleStatus = []string{} cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", "-b", @@ -720,28 +720,43 @@ func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { "--info", "--filter", filter, ) - out, lastErr := cmd.CombinedOutput() - outString := strings.Split(string(out), "\n") - + out, err := cmd.CombinedOutput() + // the command failed to run at all, try again + if err != nil { + lastErr = err + continue + } + outString := string(out) + outStringSplit := strings.Split(outString, "\n") + // cache the last BLE status here so it can be used to check for errors in future steps + lastBleStatus = outStringSplit + // failed to find the device, try again + if !strings.Contains(outString, "Found device:") { + lastErr = fmt.Errorf("BLE device was not discoverable") + continue + } + if strings.Contains(outString, "timeout on Connect") { + lastErr = fmt.Errorf("BLE device found, but connection timed out") + continue + } + if strings.Contains(outString, "did not find all requested services") { + lastErr = fmt.Errorf("BLE device connected, but could not find all requested services") + continue + } // check for all the expected BLE characteristics for _, char := range agentBleChars { charFound := false - for _, line := range outString { - // cache the last BLE status here so it can be used to check for errors later - lastBleStatus = append(lastBleStatus, line) + for _, line := range outStringSplit { if strings.Contains(line, char) { charFound = true } } + // if we find a device but it's missing an expected characteristic, bail if !charFound { - lastErr = fmt.Errorf("discovered BLE device missing characteristic: %s\n", char) + return ctx, fmt.Errorf("discovered BLE device missing characteristic: %s", char) } } - if lastErr != nil { - continue - } else { - return ctx, nil - } + return ctx, nil } return ctx, lastErr } @@ -772,7 +787,6 @@ func testBleSurfacesInvalidCredentialsErr(ctx context.Context) (context.Context, func bleSurfacesExpectedError(expectedErr string) error { for _, line := range lastBleStatus { - fmt.Printf("%s\n", line) if strings.Contains(line, "Errors:") { if strings.Contains(line, expectedErr) { return nil diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature index 392cfa3c..07d83532 100644 --- a/features/serial/provision-ble.feature +++ b/features/serial/provision-ble.feature @@ -6,11 +6,11 @@ Feature: bluetooth provisioning And the viam-agent systemd unit is running And there are no available wifi networks And viam-agent cannot reach the app - # Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning - # When viam-agent is in forced provisioning mode - # And the viam-agent bluetooth device is discoverable and has the expected characteristics - # And the host shares a secure wifi network via bluetooth - # Then viam-agent can reach the app + Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning + When viam-agent is in forced provisioning mode + And the viam-agent bluetooth device is discoverable and has the expected characteristics + And the host shares a secure wifi network via bluetooth + Then viam-agent can reach the app Scenario: The agent responds with an error when an invalid SSID is provided during bluetooth provisioning When viam-agent is in forced provisioning mode And the viam-agent bluetooth device is discoverable diff --git a/features/serial/provision-wifi.feature b/features/serial/provision-wifi.feature index 58373432..54bcd67c 100644 --- a/features/serial/provision-wifi.feature +++ b/features/serial/provision-wifi.feature @@ -9,12 +9,12 @@ Feature: wifi provisioning Scenario: The agent enters automatic provisioning mode when expected When the provisioning hotspot is not up Then the provisioning hotspot comes up within 120 seconds - Scenario: The agent can join an unknown insecure network when one is provided during wifi hotspot provisioning - When viam-agent is in forced provisioning mode - And the provisioning hotspot comes up - And the host shares an insecure wifi network via the hotspot - Then the provisioning hotspot goes away - And viam-agent can reach the app + # Scenario: The agent can join an unknown insecure network when one is provided during wifi hotspot provisioning + # When viam-agent is in forced provisioning mode + # And the provisioning hotspot comes up + # And the host shares an insecure wifi network via the hotspot + # Then the provisioning hotspot goes away + # And viam-agent can reach the app Scenario: The agent can join an unknown secure network when one is provided during wifi hotspot provisioning When viam-agent is in forced provisioning mode And the provisioning hotspot comes up diff --git a/internal/serialcontrol/serialcontrol.go b/internal/serialcontrol/serialcontrol.go index df3e8480..0575d19b 100644 --- a/internal/serialcontrol/serialcontrol.go +++ b/internal/serialcontrol/serialcontrol.go @@ -542,6 +542,10 @@ func (c *Client) EnsureOnline(ssid, password string) error { ) } + // clear all wifi connections before trying to get back online + // some tests leave behind bad connections that will interfere with connecting + c.ClearWifiConnections() + var connectErr error for range 5 { conns := c.ListWifiConnections() From 4ea24f6c3126706cb8d989bdef52676fc6573b74 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Fri, 1 May 2026 09:56:08 -0400 Subject: [PATCH 08/36] Improve error catching/handling --- agent_serial_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 4b17da98..22b8d082 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -619,8 +619,11 @@ func testAgentCanReachApp(ctx context.Context) (context.Context, error) { lastErr = res.Error() continue } - if res.MustGet() == 0 { + resGet := res.MustGet() + if resGet == 0 { return ctx, nil + } else if resGet > 0 { + lastErr = fmt.Errorf("Bad connection, or no connection: %d%% packet loss", resGet) } time.Sleep(1 * time.Second) } @@ -787,7 +790,7 @@ func testBleSurfacesInvalidCredentialsErr(ctx context.Context) (context.Context, func bleSurfacesExpectedError(expectedErr string) error { for _, line := range lastBleStatus { - if strings.Contains(line, "Errors:") { + if strings.Contains(line, "Errors:") && !strings.Contains(line, "Errors: []") { if strings.Contains(line, expectedErr) { return nil } From 5991bd6249eec14b30e42c7689be39fd90d2eb53 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 5 May 2026 16:50:18 -0400 Subject: [PATCH 09/36] Morel logging, new ble cases --- agent_serial_test.go | 37 +++++++++++++++++--------- features/serial/provision-ble.feature | 24 ++++++++++------- features/serial/provision-wifi.feature | 12 ++++----- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 22b8d082..1fbd9a83 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -38,6 +38,7 @@ var uninstallScript string var ( serialClient *serialcontrol.Client appClient apppb.AppServiceClient + logger logging.Logger deviceArch string hostName string ) @@ -126,7 +127,7 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { OrElse("./agent-test.toml") mo.TupleToResult(toml.DecodeFile(cfgPath, &cfg)).MustGet() - logger := logging.NewTestLogger(t) + logger = logging.NewTestLogger(t) // Set to INFO to see the commands being sent to the terminal, DEBUG to // see the commands + any output they produce. logger.SetLevel(logging.WARN) @@ -163,9 +164,6 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { tsc.AfterSuite(func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() - if err := serialClient.EnsureOnline(cfg.Wifi.SSID, cfg.Wifi.Password); err != nil { - t.Logf("error reconnecting to wifi during cleanup: %v", err) - } if runtime.GOOS == "darwin" { if _, err := hostEnsureOnline(ctx); err != nil { t.Logf("error restoring host connection to internet during cleanup: %v", err) @@ -226,7 +224,8 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Step(`viam-agent cannot reach the app`, testAgentCannotReachApp) // Bluetooth provisioning - ctx.Step(`the viam-agent bluetooth device is discoverable`, testBleIsDiscoverable) + ctx.Step(`the viam-agent bluetooth device (is|becomes) discoverable`, testBleIsDiscoverable) + ctx.Step(`the host shares an insecure wifi network via bluetooth`, testBleSendInsecureConnectionInfo) ctx.Step(`the host shares a secure wifi network via bluetooth`, testBleSendSecureConnectionInfo) ctx.Step(`the host shares invalid wifi credentials for a valid SSID via bluetooth`, testBleSendInvalidPasswordConnectionInfo) ctx.Step(`the host shares an invalid SSID via bluetooth`, testBleSendInvalidSSIDConnectionInfo) @@ -518,6 +517,8 @@ func testClearWifiConnections(ctx context.Context) (context.Context, error) { if clearRes.Error() != nil { return ctx, clearRes.Error() } + // just sleep a bit after deleting the networks to give network manager time to settle + time.Sleep(time.Second * 3) output := serialClient.ListWifiConnections() return ctx, output.Error() } @@ -685,7 +686,7 @@ func sendNetworkCredentialsBle(ctx context.Context, ssid, psk string) error { } robotKeys := robotKeysResp.ApiKeys[0] - cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", + args := []string{"run", "./cmd/provisioning-client", "-b", "--filter", fmt.Sprintf("viam-setup-%s", hostName), "--psk", "viamsetup", @@ -693,9 +694,12 @@ func sendNetworkCredentialsBle(ctx context.Context, ssid, psk string) error { "--wifi-psk", psk, "--part-id", cfg.PartID, "--api-key-id", robotKeys.ApiKey.Id, - "--api-key-key", robotKeys.ApiKey.Key, - ) + "--api-key-key", robotKeys.ApiKey.Key} + cmdString := strings.Join([]string{"go", strings.Join(args, " ")}, " ") + logger.Infow("Running command", "cmd", cmdString) + cmd := exec.CommandContext(ctx, "go", args...) out, err := cmd.CombinedOutput() + logger.Debugf("Command output:", "output", out) if err != nil { return fmt.Errorf("BLE provisioning failed: %w\n%s", err, out) } @@ -714,15 +718,17 @@ func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { filter := fmt.Sprintf("viam-setup-%s", hostName) var lastErr error - // don't need much in the way of retries because the client already tries for 30 seconds + // each try is 30 seconds so 4 tries is 120 seconds for range 4 { lastBleStatus = []string{} - cmd := exec.CommandContext(ctx, "go", "run", "./cmd/provisioning-client", + args := []string{"run", "./cmd/provisioning-client", "-b", "--status", "--info", - "--filter", filter, - ) + "--filter", filter} + cmdString := strings.Join([]string{"go", strings.Join(args, " ")}, " ") + logger.Infow("Running command", "cmd", cmdString) + cmd := exec.CommandContext(ctx, "go", args...) out, err := cmd.CombinedOutput() // the command failed to run at all, try again if err != nil { @@ -730,6 +736,7 @@ func testBleIsDiscoverable(ctx context.Context) (context.Context, error) { continue } outString := string(out) + logger.Debugf("Command output:", "output", outString) outStringSplit := strings.Split(outString, "\n") // cache the last BLE status here so it can be used to check for errors in future steps lastBleStatus = outStringSplit @@ -768,6 +775,10 @@ func testBleSendSecureConnectionInfo(ctx context.Context) (context.Context, erro return ctx, sendNetworkCredentialsBle(ctx, cfg.Wifi.SSID, cfg.Wifi.Password) } +func testBleSendInsecureConnectionInfo(ctx context.Context) (context.Context, error) { + return ctx, sendNetworkCredentialsBle(ctx, cfg.Wifi.SSIDInsecure, "") +} + func testSendInvalidConnectionInfo(ctx context.Context) (context.Context, error) { return ctx, sendNetworkCredentials(ctx, "thisnetwork", "doesnotexist") } @@ -797,7 +808,7 @@ func bleSurfacesExpectedError(expectedErr string) error { return fmt.Errorf("found error, but not expected error (%s): %s", expectedErr, line) } } - return fmt.Errorf("did not find any error (expected %s) in BLE info", expectedErr) + return fmt.Errorf("did not find any error (expected %s) in BLE info: %s", expectedErr, strings.Join(lastBleStatus, "\n")) } func installAgent(ctx context.Context) (context.Context, error) { diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature index 07d83532..2afdb7c1 100644 --- a/features/serial/provision-ble.feature +++ b/features/serial/provision-ble.feature @@ -6,22 +6,28 @@ Feature: bluetooth provisioning And the viam-agent systemd unit is running And there are no available wifi networks And viam-agent cannot reach the app + Scenario: The agent enters automatic provisioning mode when expected + When the provisioning hotspot is not up + Then the viam-agent bluetooth device becomes discoverable with the expected characteristics within 120 seconds + Scenario: The agent can join an unknown insecure network when one is provided during bluetooth provisioning + When viam-agent is in forced provisioning mode + And the viam-agent bluetooth device is discoverable with the expected characteristics + And the host shares an insecure wifi network via bluetooth Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning When viam-agent is in forced provisioning mode - And the viam-agent bluetooth device is discoverable and has the expected characteristics + And the viam-agent bluetooth device is discoverable with the expected characteristics And the host shares a secure wifi network via bluetooth Then viam-agent can reach the app - Scenario: The agent responds with an error when an invalid SSID is provided during bluetooth provisioning + Scenario: The agent can join a known secure network when one is provided during bluetooth provisioning + When viam-agent is connected to a network When viam-agent is in forced provisioning mode - And the viam-agent bluetooth device is discoverable - And the host shares an invalid SSID via bluetooth - Then the viam-agent bluetooth device is discoverable again - And viam-agent surfaces an invalid SSID error via bluetooth - And viam-agent cannot reach the app + And the viam-agent bluetooth device is discoverable with the expected characteristics + And the host shares a secure wifi network via bluetooth + Then viam-agent can reach the app Scenario: The agent responds with an error when invalid network credentials are provided during bluetooth provisioning When viam-agent is in forced provisioning mode - And the viam-agent bluetooth device is discoverable + And the viam-agent bluetooth device is discoverable with the expected characteristics And the host shares invalid wifi credentials for a valid SSID via bluetooth - Then the viam-agent bluetooth device is discoverable again + Then the viam-agent bluetooth device is discoverable with the expected characteristics again And viam-agent surfaces an invalid credentials error via bluetooth And viam-agent cannot reach the app \ No newline at end of file diff --git a/features/serial/provision-wifi.feature b/features/serial/provision-wifi.feature index 54bcd67c..58373432 100644 --- a/features/serial/provision-wifi.feature +++ b/features/serial/provision-wifi.feature @@ -9,12 +9,12 @@ Feature: wifi provisioning Scenario: The agent enters automatic provisioning mode when expected When the provisioning hotspot is not up Then the provisioning hotspot comes up within 120 seconds - # Scenario: The agent can join an unknown insecure network when one is provided during wifi hotspot provisioning - # When viam-agent is in forced provisioning mode - # And the provisioning hotspot comes up - # And the host shares an insecure wifi network via the hotspot - # Then the provisioning hotspot goes away - # And viam-agent can reach the app + Scenario: The agent can join an unknown insecure network when one is provided during wifi hotspot provisioning + When viam-agent is in forced provisioning mode + And the provisioning hotspot comes up + And the host shares an insecure wifi network via the hotspot + Then the provisioning hotspot goes away + And viam-agent can reach the app Scenario: The agent can join an unknown secure network when one is provided during wifi hotspot provisioning When viam-agent is in forced provisioning mode And the provisioning hotspot comes up From ad42c4b709633c63c4ec2d68e9049e3c879ac40b Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 30 Apr 2026 14:34:14 -0400 Subject: [PATCH 10/36] Clear viamServerNeedsRestart on server restart (#231) --- manager.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manager.go b/manager.go index b28e496f..80d42a82 100644 --- a/manager.go +++ b/manager.go @@ -298,6 +298,8 @@ func (m *Manager) SubsystemUpdates(ctx context.Context) { if err := m.viamServer.Stop(stopCtx); err != nil { m.logger.Warn(err) + } else { + m.viamServerNeedsRestart = false } if m.viamAgentNeedsRestart { m.Exit(fmt.Sprintf("A new version of %s has been installed", SubsystemName)) From d5a06c337c4fb15ec8ee9cce47c7955f670abd42 Mon Sep 17 00:00:00 2001 From: Ale Paredes <1709578+ale7714@users.noreply.github.com> Date: Fri, 1 May 2026 14:29:46 -0400 Subject: [PATCH 11/36] stopProvisioning: always flip provisioning flag, even on cleanup error (#232) Co-authored-by: Claude Opus 4.7 (1M context) --- subsystems/networking/networkmanager_linux.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/subsystems/networking/networkmanager_linux.go b/subsystems/networking/networkmanager_linux.go index b65aa164..280d3ca2 100644 --- a/subsystems/networking/networkmanager_linux.go +++ b/subsystems/networking/networkmanager_linux.go @@ -314,11 +314,12 @@ func (n *Subsystem) stopProvisioning() error { n.stopProvisioningHotspot(), n.stopProvisioningBluetooth(), ) - if err != nil { - return err - } + // Always flip the provisioning flag, even if cleanup returned an error. + // Resources are already torn down at this point; leaving the flag set + // makes connState inaccurate with reality and blocks the agent from + // re-entering or exiting provisioning correctly until the next restart. n.connState.setProvisioning(false) - return nil + return err } func (n *Subsystem) stopProvisioningHotspot() error { From 805094e1f6cf7731a16c8b6821e1fb6baca239fb Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Mon, 4 May 2026 13:45:41 -0400 Subject: [PATCH 12/36] RSDK-13484: Add trixie to list of debian versions supporting unattended upgrades (#233) --- subsystems/syscfg/upgrades_linux.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/subsystems/syscfg/upgrades_linux.go b/subsystems/syscfg/upgrades_linux.go index 3cd3324f..583b52e0 100644 --- a/subsystems/syscfg/upgrades_linux.go +++ b/subsystems/syscfg/upgrades_linux.go @@ -22,6 +22,8 @@ const ( unattendedUpgradesPath = "/etc/apt/apt.conf.d/50unattended-upgrades" ) +var supportedCodenames = [...]string{"bookworm", "bullseye", "trixie"} + // runs inside s.mu.Lock(). func (s *Subsystem) EnforceUpgrades(ctx context.Context) error { cfg := s.cfg.OSAutoUpgradeType @@ -90,11 +92,14 @@ func checkSupportedDistro() error { return err } - if strings.Contains(string(data), "VERSION_CODENAME=bookworm") || strings.Contains(string(data), "VERSION_CODENAME=bullseye") { - return nil + dataStr := string(data) + for _, codename := range supportedCodenames { + if strings.Contains(dataStr, "VERSION_CODENAME="+codename) { + return nil + } } - return errw.New("cannot enable automatic upgrades for unknown distro, only support for Debian bullseye and bookworm is available") + return fmt.Errorf("cannot enable automatic upgrades for unknown distro, only support for Debian %v is available", supportedCodenames) } // make sure the needed package is installed. From 38c12a2f689c4b00e2b3fb75cd628748e2b38697 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 14:48:41 -0400 Subject: [PATCH 13/36] Remove ensureonline from scenario after hook --- agent_serial_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 1fbd9a83..193f0542 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -250,9 +250,6 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Step(`an old viam-server binary is present on the device$`, downloadOldViamServerBinary) ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { - if err := serialClient.EnsureOnline(cfg.Wifi.SSID, cfg.Wifi.Password); err != nil { - return ctx, err - } if runtime.GOOS == "darwin" { if _, err := hostEnsureOnline(ctx); err != nil { return ctx, err From b9d92441bbb9eed70342e2c17c0aea865cfc9324 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 15:09:16 -0400 Subject: [PATCH 14/36] Ignore some more files --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 8c41d439..ce067c42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/ etc/ +.vscode/ # GCP's auth action adds this in CI, dirtying the tree gha-creds-*.json @@ -8,7 +9,12 @@ viam-agent-windows-installer*.exe # Config file containing secrets used for serial console tests agent-test.toml +# Other config files +agent-test.toml.* # Claude Code .claude/worktrees .claude/settings.local.json + +# Test reports +report.xml From 0de5f9b62d5b0f58fe18a6887b0047a1e1dfa628 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 17:06:28 -0400 Subject: [PATCH 15/36] Enable specifying "test" version --- agent-test-example.toml | 2 ++ agent_serial_test.go | 44 ++++++++++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/agent-test-example.toml b/agent-test-example.toml index 49931633..c63421e2 100644 --- a/agent-test-example.toml +++ b/agent-test-example.toml @@ -12,8 +12,10 @@ part_id = "" # serial_pass = "" [versions] +viam_agent_test = "" viam_agent_stable = "" viam_agent_old = "" +viam_server_test = "" viam_server_stable = "" viam_server_old = "" diff --git a/agent_serial_test.go b/agent_serial_test.go index 193f0542..2e7780f9 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -54,8 +54,10 @@ type config struct { } type versionsCfg struct { + Test string `toml:"viam_agent_test"` Stable string `toml:"viam_agent_stable"` Old string `toml:"viam_agent_old"` + ViamServerTest string `toml:"viam_server_test"` ViamServerStable string `toml:"viam_server_stable"` ViamServerOld string `toml:"viam_server_old"` } @@ -171,11 +173,12 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { } // Just wait after reconnecting everything to make sure all the connections are back time.Sleep(time.Second * 3) - if _, err := applyAgentVersionPin(ctx, "stable"); err != nil { - t.Logf("error pinning agent back to stable during cleanup: %v", err) + // Pin back to the version under test + if _, err := applyAgentVersionPin(ctx, "test"); err != nil { + t.Logf("error pinning agent back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerTest, err) } - if _, err := applyViamServerVersionPin(ctx, "stable"); err != nil { - t.Logf("error pinning viam-server back to stable during cleanup: %v", err) + if _, err := applyViamServerVersionPin(ctx, "test"); err != nil { + t.Logf("error pinning viam-server back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerTest, err) } if err := serialClient.Close(); err != nil { t.Logf("error closing serial client during cleanup: %v", err) @@ -185,7 +188,7 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { } func InitializeScenario(ctx *godog.ScenarioContext) { - const versionGroup = `(an old version|dev|stable|version [^\s]+)` + const versionGroup = `(an old version|dev|stable|test|version [^\s]+)` // Restart viam-agent before each scenario (if it is running) so that every // scenario starts with a fresh systemd InvocationID. This ensures that @@ -327,9 +330,13 @@ func setField(root *structpb.Struct, value *structpb.Value, path ...string) erro return nil } -// translateVersion translates a version string into the format app expects. +// translateVersion translates a "version string" into the format app expects. +// A "version string" here is the string used to vaguely specify a version in the godog test +// and is not an actual version specification like the one used in a viam robot config. + // oldVersion is the concrete version string to use for the string "an old version". -func translateVersion(version, oldVersion string) string { +// testVersion is the concrete version string to use for the string "test". +func translateVersion(version, oldVersion, testVersion string) string { switch version { case "an old version": if oldVersion == "" { @@ -338,6 +345,11 @@ func translateVersion(version, oldVersion string) string { return oldVersion case "stable", "dev": return version + case "test": + if testVersion == "" { + panic("must set test version in config") + } + return testVersion } if strings.HasPrefix(version, "version ") { return strings.SplitN(version, " ", 2)[1] @@ -348,7 +360,7 @@ func translateVersion(version, oldVersion string) string { // versionStrToMatcherBase returns a matcher function for a version string. // oldVersion and stableVersion are the concrete version strings to compare // against for "an old version" and "stable" respectively. -func versionStrToMatcherBase(version, oldVersion, stableVersion string) func(string) string { +func versionStrToMatcherBase(version, oldVersion, stableVersion, testVersion string) func(string) string { switch version { case "an old version": return func(actual string) string { @@ -364,6 +376,13 @@ func versionStrToMatcherBase(version, oldVersion, stableVersion string) func(str } return test.ShouldEqual(actual, stableVersion) } + case "test": + return func(actual string) string { + if testVersion == "" { + panic("must set test version in config") + } + return test.ShouldEqual(actual, testVersion) + } case "dev": return func(actual string) string { devRegex := regexp.MustCompile(`-dev\.\d+(-[0-9a-f]+)?$`) @@ -389,6 +408,7 @@ func applyVersionPin(ctx context.Context, versionStr string, path ...string) (co return ctx, err } partCfg := partResp.Part.RobotConfig + logger.Infof("Pinning agent to version: %s\n", versionStr) if err = setField(partCfg, structpb.NewStringValue(versionStr), path...); err != nil { return ctx, err } @@ -888,11 +908,11 @@ func testAgentState(ctx context.Context, key, expectedVal string) (context.Conte } func translateToAppVersion(version string) string { - return translateVersion(version, cfg.Versions.Old) + return translateVersion(version, cfg.Versions.Old, cfg.Versions.Test) } func versionStrToMatcher(version string) func(string) string { - return versionStrToMatcherBase(version, cfg.Versions.Old, cfg.Versions.Stable) + return versionStrToMatcherBase(version, cfg.Versions.Old, cfg.Versions.Stable, cfg.Versions.Test) } func applyAgentVersionPin(ctx context.Context, version string) (context.Context, error) { @@ -950,11 +970,11 @@ func testViamServerRunningWithVersion(ctx context.Context, version string) (cont } func translateVersionViamServer(version string) string { - return translateVersion(version, cfg.Versions.ViamServerOld) + return translateVersion(version, cfg.Versions.ViamServerOld, cfg.Versions.ViamServerTest) } func versionStrToMatcherViamServer(version string) func(string) string { - return versionStrToMatcherBase(version, cfg.Versions.ViamServerOld, cfg.Versions.ViamServerStable) + return versionStrToMatcherBase(version, cfg.Versions.ViamServerOld, cfg.Versions.ViamServerStable, cfg.Versions.ViamServerTest) } func applyViamServerVersionPin(ctx context.Context, version string) (context.Context, error) { From 296bdedce4cdc62d328b8653297311f75b37cdc6 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 18:24:31 -0400 Subject: [PATCH 16/36] Support more version specifiers --- agent-test-example.toml | 6 +- agent_serial_test.go | 152 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/agent-test-example.toml b/agent-test-example.toml index c63421e2..c8c80b37 100644 --- a/agent-test-example.toml +++ b/agent-test-example.toml @@ -12,7 +12,11 @@ part_id = "" # serial_pass = "" [versions] -viam_agent_test = "" +# viam_agent_test can be any of the following. the corresponding binary will be installed from GCS +# a release version e.g. 0.27.3 +# "dev" - the tip of main +# github PR number (needs "dev-release" label): "pr.xxx" i.e. "pr.226" +viam_agent_test = "" viam_agent_stable = "" viam_agent_old = "" viam_server_test = "" diff --git a/agent_serial_test.go b/agent_serial_test.go index 2e7780f9..cc36b8f6 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -5,12 +5,16 @@ package agent_test import ( "context" _ "embed" + "encoding/json" "errors" "fmt" + "net/http" "os" "os/exec" "regexp" "runtime" + "sort" + "strconv" "strings" "testing" "time" @@ -289,15 +293,161 @@ func hostEnsureOnline(ctx context.Context) (context.Context, error) { return ctx, nil } +// Concrete-version shapes that gcsURL recognizes: +// - prVersionRe: "-pr.." (PR dev-release; lives in prerelease/pr-/) +// - devVersionRe: "-dev." (main-branch dev build; lives in prerelease/) +// - anything else is treated as a stable release at the top of apps//. +var ( + prVersionRe = regexp.MustCompile(`^[^-]+-pr\.(\d+)\.[a-f0-9]{40}$`) + devVersionRe = regexp.MustCompile(`^[^-]+-dev\.\d+$`) +) + // gcsURL constructs the GCS download URL for a viam binary given its subsystem -// name (e.g. "viam-agent", "viam-server"), version string, and device arch. +// name (e.g. "viam-agent", "viam-server") and a concrete version string. It +// routes PR and dev versions to the prerelease subdirectories. func gcsURL(subsystem, version string) string { + if m := prVersionRe.FindStringSubmatch(version); m != nil { + return fmt.Sprintf( + "https://storage.googleapis.com/packages.viam.com/apps/%s/prerelease/pr-%s/%s-v%s-%s", + subsystem, m[1], subsystem, version, deviceArch, + ) + } + if devVersionRe.MatchString(version) { + return fmt.Sprintf( + "https://storage.googleapis.com/packages.viam.com/apps/%s/prerelease/%s-v%s-%s", + subsystem, subsystem, version, deviceArch, + ) + } return fmt.Sprintf( "https://storage.googleapis.com/packages.viam.com/apps/%s/%s-v%s-%s", subsystem, subsystem, version, deviceArch, ) } +// resolveVersionSpec turns a TOML version specifier into a concrete version +// string. Recognized forms: +// - "stable" -> latest stable release (e.g. "0.27.3") +// - "dev" -> latest main-branch dev build (e.g. "0.27.3-dev.5") +// - "pr." -> latest dev-release for PR N (e.g. "0.27.3-pr.227.") +// - anything else is assumed to already be concrete and returned as-is. +// +// Only viam-agent specifiers are supported today; viam-server has different +// upload paths and would need its own resolver. +func resolveVersionSpec(ctx context.Context, spec string) (string, error) { + switch { + case spec == "stable": + return latestStableRelease(ctx) + case spec == "dev": + b, err := latestDevBuild(ctx) + if err != nil { + return "", err + } + return b, nil + case strings.HasPrefix(spec, "pr."): + n, err := strconv.Atoi(strings.TrimPrefix(spec, "pr.")) + if err != nil { + return "", fmt.Errorf("invalid pr specifier %q: %w", spec, err) + } + base, sha, err := latestPRBuild(ctx, n) + if err != nil { + return "", err + } + return fmt.Sprintf("%s-pr.%d.%s", base, n, sha), nil + default: + return spec, nil + } +} + +// gcsListItem is the trimmed shape of a GCS JSON list response entry. +type gcsListItem struct { + Name string `json:"name"` + TimeCreated time.Time `json:"timeCreated"` +} + +// listGCS lists viam-agent objects under prefix in the public packages bucket, +// sorted newest-first by upload time. +func listGCS(ctx context.Context, prefix string) ([]gcsListItem, error) { + listURL := "https://storage.googleapis.com/storage/v1/b/packages.viam.com/o" + + "?prefix=" + prefix + + "&fields=items(name,timeCreated)" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, listURL, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GCS list %q returned %s", prefix, resp.Status) + } + var body struct { + Items []gcsListItem `json:"items"` + } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return nil, err + } + sort.Slice(body.Items, func(i, j int) bool { + return body.Items[i].TimeCreated.After(body.Items[j].TimeCreated) + }) + return body.Items, nil +} + +// latestStableRelease returns the most recently uploaded stable viam-agent +// release version (bare semver, e.g. "0.27.3"). +func latestStableRelease(ctx context.Context) (string, error) { + items, err := listGCS(ctx, "apps/viam-agent/viam-agent-v") + if err != nil { + return "", err + } + // Stable filenames are exactly "viam-agent-v..-". + pat := regexp.MustCompile(`/viam-agent-v(\d+\.\d+\.\d+)-[^/]+$`) + for _, it := range items { + if m := pat.FindStringSubmatch(it.Name); m != nil { + return m[1], nil + } + } + return "", errors.New("no stable releases found") +} + +// latestDevBuild returns the most recently uploaded main-branch dev build +// version (e.g. "0.27.3-dev.5"). +func latestDevBuild(ctx context.Context) (string, error) { + items, err := listGCS(ctx, "apps/viam-agent/prerelease/viam-agent-v") + if err != nil { + return "", err + } + pat := regexp.MustCompile(`/viam-agent-v([^-]+-dev\.\d+)-[^/]+$`) + for _, it := range items { + if m := pat.FindStringSubmatch(it.Name); m != nil { + return m[1], nil + } + } + return "", errors.New("no dev builds found") +} + +// latestPRBuild returns the base version and 40-char head SHA of the most +// recently uploaded dev-release build for the given PR number. +func latestPRBuild(ctx context.Context, prNum int) (base, sha string, err error) { + items, err := listGCS(ctx, fmt.Sprintf("apps/viam-agent/prerelease/pr-%d/", prNum)) + if err != nil { + return "", "", err + } + if len(items) == 0 { + return "", "", fmt.Errorf("no dev-release artifacts found for PR %d", prNum) + } + pat := regexp.MustCompile( + fmt.Sprintf(`/viam-agent-v([^-]+)-pr\.%d\.([a-f0-9]{40})-`, prNum), + ) + for _, it := range items { + if m := pat.FindStringSubmatch(it.Name); m != nil { + return m[1], m[2], nil + } + } + return "", "", fmt.Errorf("no parsable binary names found for PR %d", prNum) +} + // setField sets a field nested in an arbitrarily deep tree of // [*structpb.Struct]s to the provided [*structpb.Value]. Any intermediary // fields that do not exist or are set to types other than structpb.Struct will From fdb6c9d0ca5eb69a9b0ff48342c8ceabc0c5ea42 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 18:34:15 -0400 Subject: [PATCH 17/36] Install via install.sh heredoc --- agent_serial_test.go | 19 +++++++++++-------- internal/serialcontrol/serialcontrol.go | 14 -------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index cc36b8f6..ec066d2a 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -39,6 +39,9 @@ import ( //go:embed uninstall.sh var uninstallScript string +//go:embed install.sh +var installScript string + var ( serialClient *serialcontrol.Client appClient apppb.AppServiceClient @@ -110,7 +113,7 @@ func TestSerialFeatures(t *testing.T) { Options: &godog.Options{ // Options at time of writing: cucumber, events, junit, pretty, progress Format: "pretty", - Paths: []string{"features/serial"}, + Paths: []string{"features/serial/install.feature"}, Tags: serialTestTags(), TestingT: t, Strict: true, @@ -209,7 +212,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) { // Agent utility steps ctx.Step(`^viam-agent is installed$`, installAgent) - ctx.Step(`viam-agent is (not |un)installed$`, removeViam) + ctx.Step(`viam-agent is (not |un)installed$`, uninstallAgent) ctx.Step(`the viam-agent systemd unit is enabled`, testAgentEnabled) ctx.Step(`the viam-agent systemd unit is running$`, testAgentRunning) ctx.Step(`the viam-agent systemd unit is dead$`, testAgentDead) @@ -570,7 +573,7 @@ func applyVersionPin(ctx context.Context, versionStr string, path ...string) (co return ctx, err } -func removeViam(ctx context.Context) (context.Context, error) { +func uninstallAgent(ctx context.Context) (context.Context, error) { if err := serialClient.RunScript(uninstallScript, "FORCE=1 sh").Error(); err != nil { return ctx, err } @@ -991,11 +994,11 @@ func installAgent(ctx context.Context) (context.Context, error) { return ctx, err } robotKeys := robotKeysResp.ApiKeys[0] - return ctx, serialClient.InstallViam( - cfg.PartID, - robotKeys.ApiKey.Id, - robotKeys.ApiKey.Key, - ).Error() + cmd := fmt.Sprintf( + "FORCE=1 VIAM_API_KEY_ID=%s VIAM_API_KEY=%s VIAM_PART_ID=%s sh", + robotKeys.ApiKey.Id, robotKeys.ApiKey.Key, cfg.PartID, + ) + return ctx, serialClient.RunScript(installScript, cmd).Error() } func testAgentEnabled(ctx context.Context) (context.Context, error) { diff --git a/internal/serialcontrol/serialcontrol.go b/internal/serialcontrol/serialcontrol.go index 0575d19b..43028619 100644 --- a/internal/serialcontrol/serialcontrol.go +++ b/internal/serialcontrol/serialcontrol.go @@ -364,20 +364,6 @@ func (c *Client) RunScript(script, command string) mo.Result[[]string] { return result } -// InstallViam installs viam-agent using the process presented to the user in -// the setup flow on app.viam.com. -func (c *Client) InstallViam(partID, keyID, key string) mo.Result[[]string] { - cmd := fmt.Sprintf( - //nolint: lll - `yes | /bin/sh -c "FORCE=1 VIAM_API_KEY_ID=%s VIAM_API_KEY=%s VIAM_PART_ID=%s; $(curl -fsSL https://storage.googleapis.com/packages.viam.com/apps/viam-agent/install.sh)"`, - keyID, key, partID, - ) - // TODO: this will log the command being run, including the API key in - // plaintext. This should change if we ever plan to run this anywhere other - // than local environments. - return c.runCmd(cmd) -} - // StartAgent starts the viam-agent systemd unit. func (c *Client) StartAgent() mo.Result[[]string] { return c.runCmd("systemctl start viam-agent") From 808dc2a61b6e6b2a99ae6a321b0f58ac48adc213 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 19:39:16 -0400 Subject: [PATCH 18/36] Modify install script to take custom url --- install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.sh b/install.sh index b87e85e4..8a91dc3b 100755 --- a/install.sh +++ b/install.sh @@ -17,6 +17,9 @@ if [ "$OS" = "Darwin" ]; then BINARY_OS_PREFIX="darwin-" fi URL="https://storage.googleapis.com/packages.viam.com/apps/viam-agent/viam-agent-stable-${BINARY_OS_PREFIX}${ARCH}" +if [ -n "$AGENT_CUSTOM_URL" ]; then + URL="$AGENT_CUSTOM_URL" +fi # Force will bypass all prompts by treating them as yes. May also be set as an environment variable when running as download. # sudo /bin/sh -c "FORCE=1; $(curl -fsSL https://storage.googleapis.com/packages.viam.com/apps/viam-agent/install.sh)" From 68cbb11bca6eb5ef0ace93ef3c18fcf779a4b590 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 19:42:16 -0400 Subject: [PATCH 19/36] Fix test version spec support --- agent_serial_test.go | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index ec066d2a..2406bb09 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -43,11 +43,12 @@ var uninstallScript string var installScript string var ( - serialClient *serialcontrol.Client - appClient apppb.AppServiceClient - logger logging.Logger - deviceArch string - hostName string + serialClient *serialcontrol.Client + appClient apppb.AppServiceClient + logger logging.Logger + deviceArch string + hostName string + concreteTestVersion string ) type config struct { @@ -113,7 +114,7 @@ func TestSerialFeatures(t *testing.T) { Options: &godog.Options{ // Options at time of writing: cucumber, events, junit, pretty, progress Format: "pretty", - Paths: []string{"features/serial/install.feature"}, + Paths: []string{"features/serial"}, Tags: serialTestTags(), TestingT: t, Strict: true, @@ -201,6 +202,16 @@ func InitializeScenario(ctx *godog.ScenarioContext) { // scenario starts with a fresh systemd InvocationID. This ensures that // journal-based checks cannot match log lines produced by a previous scenario. ctx.Before(func(ctx context.Context, _ *godog.Scenario) (context.Context, error) { + if cfg.Versions.Test != "" { + concrete, err := resolveVersionSpec(ctx, cfg.Versions.Test) + concreteTestVersion = concrete + logger.Infof("Version under test: %s\n", concreteTestVersion) + if err != nil { + panic(fmt.Errorf("resolving install version %q: %w", cfg.Versions.Test, err)) + } + } else { + panic(fmt.Errorf("viam_agent_test in agent-test.toml cannot be empty string")) + } status := serialClient.GetAgentStatus() if status.IsOk() && status.MustGet()["SubState"] == "running" { if err := serialClient.RestartAgent().Error(); err != nil { @@ -211,7 +222,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) { }) // Agent utility steps - ctx.Step(`^viam-agent is installed$`, installAgent) + ctx.Step(fmt.Sprintf(`^viam-agent version %s is installed$`, versionGroup), installAgent) ctx.Step(`viam-agent is (not |un)installed$`, uninstallAgent) ctx.Step(`the viam-agent systemd unit is enabled`, testAgentEnabled) ctx.Step(`the viam-agent systemd unit is running$`, testAgentRunning) @@ -534,7 +545,7 @@ func versionStrToMatcherBase(version, oldVersion, stableVersion, testVersion str if testVersion == "" { panic("must set test version in config") } - return test.ShouldEqual(actual, testVersion) + return test.ShouldEqual(actual, concreteTestVersion) } case "dev": return func(actual string) string { @@ -981,7 +992,13 @@ func bleSurfacesExpectedError(expectedErr string) error { return fmt.Errorf("did not find any error (expected %s) in BLE info: %s", expectedErr, strings.Join(lastBleStatus, "\n")) } -func installAgent(ctx context.Context) (context.Context, error) { +// installAgentVersion runs install.sh on the device. If version is empty, the script +// downloads the stable release; otherwise version is treated as a specifier +// (concrete, "stable", "dev", or "pr.N"), resolved to a concrete GCS URL, and +// injected into the script via AGENT_CUSTOM_URL. +func installAgent(ctx context.Context, version string) (context.Context, error) { + version = translateToAppVersion(version) + agentStatus := serialClient.GetAgentStatus().MustGet() if agentStatus["SubState"] == "running" { // Avoid wasting time and network traffic if agent is already running. @@ -995,9 +1012,11 @@ func installAgent(ctx context.Context) (context.Context, error) { } robotKeys := robotKeysResp.ApiKeys[0] cmd := fmt.Sprintf( - "FORCE=1 VIAM_API_KEY_ID=%s VIAM_API_KEY=%s VIAM_PART_ID=%s sh", + "FORCE=1 VIAM_API_KEY_ID=%s VIAM_API_KEY=%s VIAM_PART_ID=%s", robotKeys.ApiKey.Id, robotKeys.ApiKey.Key, cfg.PartID, ) + logger.Infof("Install version: %s\n", version) + cmd += fmt.Sprintf(" AGENT_CUSTOM_URL=%s", gcsURL("viam-agent", concreteTestVersion)) + " sh" return ctx, serialClient.RunScript(installScript, cmd).Error() } @@ -1027,6 +1046,7 @@ func testAgentRunningWithVersion(ctx context.Context, version string) (context.C func testSystemdAgentStartVersion(ctx context.Context, version string) (context.Context, error) { versionTest := versionStrToMatcher(version) + logger.Infof("Check for version %s with matcher %s\n", version, versionTest) var err error // Agent needs time to fetch the new config, possibly download the new // version, and restart. From 9a6817a745bfc48ac4a6d7f6faa1199d341d18f9 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 6 May 2026 19:47:44 -0400 Subject: [PATCH 20/36] Better test case syntax, update feature files --- agent_serial_test.go | 8 ++++---- features/serial/agent-pin-file.feature | 2 +- features/serial/agent-pin-url.feature | 2 +- features/serial/agent-reject-viam-server-binary.feature | 2 +- features/serial/downgrade.feature | 2 +- features/serial/install.feature | 6 +++--- features/serial/provision-ble.feature | 4 ++-- features/serial/provision-wifi.feature | 4 ++-- features/serial/uninstall.feature | 4 ++-- features/serial/upgrade.feature | 2 +- features/serial/viamserver-pin-file.feature | 2 +- features/serial/viamserver-pin-url.feature | 2 +- features/serial/viamserver-pin-version.feature | 2 +- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 2406bb09..cf118ff7 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -196,7 +196,7 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { } func InitializeScenario(ctx *godog.ScenarioContext) { - const versionGroup = `(an old version|dev|stable|test|version [^\s]+)` + const versionGroup = `(an old version|dev|stable|the version under test|version [^\s]+)` // Restart viam-agent before each scenario (if it is running) so that every // scenario starts with a fresh systemd InvocationID. This ensures that @@ -222,7 +222,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) { }) // Agent utility steps - ctx.Step(fmt.Sprintf(`^viam-agent version %s is installed$`, versionGroup), installAgent) + ctx.Step(fmt.Sprintf(`^viam-agent is installed at %s$`, versionGroup), installAgent) ctx.Step(`viam-agent is (not |un)installed$`, uninstallAgent) ctx.Step(`the viam-agent systemd unit is enabled`, testAgentEnabled) ctx.Step(`the viam-agent systemd unit is running$`, testAgentRunning) @@ -509,7 +509,7 @@ func translateVersion(version, oldVersion, testVersion string) string { return oldVersion case "stable", "dev": return version - case "test": + case "the version under test": if testVersion == "" { panic("must set test version in config") } @@ -540,7 +540,7 @@ func versionStrToMatcherBase(version, oldVersion, stableVersion, testVersion str } return test.ShouldEqual(actual, stableVersion) } - case "test": + case "the version under test": return func(actual string) string { if testVersion == "" { panic("must set test version in config") diff --git a/features/serial/agent-pin-file.feature b/features/serial/agent-pin-file.feature index 3ef20eb3..3beef640 100644 --- a/features/serial/agent-pin-file.feature +++ b/features/serial/agent-pin-file.feature @@ -1,7 +1,7 @@ Feature: Pin viam-agent to an old version via a local file Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And viam-agent is pinned to stable And the viam-agent systemd unit is running with stable diff --git a/features/serial/agent-pin-url.feature b/features/serial/agent-pin-url.feature index b589cc57..e30e9198 100644 --- a/features/serial/agent-pin-url.feature +++ b/features/serial/agent-pin-url.feature @@ -1,7 +1,7 @@ Feature: Pin viam-agent to an old version via a URL Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And viam-agent is pinned to stable And the viam-agent systemd unit is running with stable diff --git a/features/serial/agent-reject-viam-server-binary.feature b/features/serial/agent-reject-viam-server-binary.feature index fc618b74..ec018f6e 100644 --- a/features/serial/agent-reject-viam-server-binary.feature +++ b/features/serial/agent-reject-viam-server-binary.feature @@ -1,7 +1,7 @@ Feature: Pinning viam-agent to a viam-server binary is rejected Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And viam-agent is pinned to stable And the viam-agent systemd unit is running with stable diff --git a/features/serial/downgrade.feature b/features/serial/downgrade.feature index f4ab570b..66f2611c 100644 --- a/features/serial/downgrade.feature +++ b/features/serial/downgrade.feature @@ -1,6 +1,6 @@ Feature: Downgrade viam-agent Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And the viam-agent systemd unit is running with stable Scenario: Pin viam agent to an old version When viam-agent is pinned to an old version diff --git a/features/serial/install.feature b/features/serial/install.feature index ca93fb5d..4c56a8cc 100644 --- a/features/serial/install.feature +++ b/features/serial/install.feature @@ -1,9 +1,9 @@ Feature: install viam-agent Background: Given viam-agent is not installed - Scenario: Install current stable version of viam-agent - When viam-agent is installed - Then the viam-agent systemd unit is running + Scenario: Install the viam-agent version under test + When viam-agent is installed at the version under test + Then the viam-agent systemd unit is running with the version under test And the viam-agent systemd unit is enabled And the journald config is live And the wifi power save config is live diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature index 2afdb7c1..d9164f7c 100644 --- a/features/serial/provision-ble.feature +++ b/features/serial/provision-ble.feature @@ -1,9 +1,9 @@ @darwin Feature: bluetooth provisioning Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And the viam-agent systemd unit is enabled - And the viam-agent systemd unit is running + And the viam-agent systemd unit is running with version under test And there are no available wifi networks And viam-agent cannot reach the app Scenario: The agent enters automatic provisioning mode when expected diff --git a/features/serial/provision-wifi.feature b/features/serial/provision-wifi.feature index 58373432..51828d18 100644 --- a/features/serial/provision-wifi.feature +++ b/features/serial/provision-wifi.feature @@ -1,9 +1,9 @@ @darwin Feature: wifi provisioning Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And the viam-agent systemd unit is enabled - And the viam-agent systemd unit is running + And the viam-agent systemd unit is running with version under test And there are no available wifi networks And viam-agent cannot reach the app Scenario: The agent enters automatic provisioning mode when expected diff --git a/features/serial/uninstall.feature b/features/serial/uninstall.feature index 7c27bd6e..08f9e43e 100644 --- a/features/serial/uninstall.feature +++ b/features/serial/uninstall.feature @@ -1,7 +1,7 @@ Feature: uninstall viam-agent Background: - Given viam-agent is installed - And the viam-agent systemd unit is running + Given viam-agent is installed at the version under test + And the viam-agent systemd unit is running with version under test And the viam-agent systemd unit is enabled Scenario: Uninstall viam-agent When viam-agent is uninstalled diff --git a/features/serial/upgrade.feature b/features/serial/upgrade.feature index 9aa14943..90c4093f 100644 --- a/features/serial/upgrade.feature +++ b/features/serial/upgrade.feature @@ -1,6 +1,6 @@ Feature: Upgrade viam-agent Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And the viam-agent systemd unit is running with stable Scenario: Pin viam agent to stable When viam-agent is pinned to dev diff --git a/features/serial/viamserver-pin-file.feature b/features/serial/viamserver-pin-file.feature index e6bc2bec..e69124f9 100644 --- a/features/serial/viamserver-pin-file.feature +++ b/features/serial/viamserver-pin-file.feature @@ -1,7 +1,7 @@ Feature: Pin viam-server to an old version via a local file Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And viam-agent is pinned to stable And viam-server is pinned to stable And the viam-agent systemd unit is running with stable diff --git a/features/serial/viamserver-pin-url.feature b/features/serial/viamserver-pin-url.feature index c67af9c9..6e38cddb 100644 --- a/features/serial/viamserver-pin-url.feature +++ b/features/serial/viamserver-pin-url.feature @@ -1,7 +1,7 @@ Feature: Pin viam-server to an old version via a URL Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And viam-agent is pinned to stable And viam-server is pinned to stable And the viam-agent systemd unit is running with stable diff --git a/features/serial/viamserver-pin-version.feature b/features/serial/viamserver-pin-version.feature index ccf1c916..86f6ce1f 100644 --- a/features/serial/viamserver-pin-version.feature +++ b/features/serial/viamserver-pin-version.feature @@ -1,7 +1,7 @@ Feature: Pin viam-server to an older version Background: - Given viam-agent is installed + Given viam-agent is installed at the version under test And viam-agent is pinned to stable And viam-server is pinned to stable And the viam-agent systemd unit is running with stable From 91a2782865f034e1f0cde5ca052431661df58e58 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Mon, 11 May 2026 10:52:31 -0400 Subject: [PATCH 21/36] Fix post-test pinning --- agent_serial_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index cf118ff7..3211916d 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -114,7 +114,7 @@ func TestSerialFeatures(t *testing.T) { Options: &godog.Options{ // Options at time of writing: cucumber, events, junit, pretty, progress Format: "pretty", - Paths: []string{"features/serial"}, + Paths: []string{"features/serial/install.feature"}, Tags: serialTestTags(), TestingT: t, Strict: true, @@ -182,10 +182,10 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { // Just wait after reconnecting everything to make sure all the connections are back time.Sleep(time.Second * 3) // Pin back to the version under test - if _, err := applyAgentVersionPin(ctx, "test"); err != nil { + if _, err := applyAgentVersionPin(ctx, "the version under test"); err != nil { t.Logf("error pinning agent back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerTest, err) } - if _, err := applyViamServerVersionPin(ctx, "test"); err != nil { + if _, err := applyViamServerVersionPin(ctx, "the version under test"); err != nil { t.Logf("error pinning viam-server back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerTest, err) } if err := serialClient.Close(); err != nil { From 89ed8b1968d415edaa7a61b1b493310b94121e9f Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 12 May 2026 16:52:04 -0400 Subject: [PATCH 22/36] Support file pin --- agent_serial_test.go | 53 ++++++++++++++++++++++++------- features/serial/uninstall.feature | 2 +- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 3211916d..a6280f36 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -114,7 +114,7 @@ func TestSerialFeatures(t *testing.T) { Options: &godog.Options{ // Options at time of writing: cucumber, events, junit, pretty, progress Format: "pretty", - Paths: []string{"features/serial/install.feature"}, + Paths: []string{"features/serial"}, Tags: serialTestTags(), TestingT: t, Strict: true, @@ -541,11 +541,20 @@ func versionStrToMatcherBase(version, oldVersion, stableVersion, testVersion str return test.ShouldEqual(actual, stableVersion) } case "the version under test": - return func(actual string) string { - if testVersion == "" { - panic("must set test version in config") + if strings.HasPrefix(concreteTestVersion, "file://") { + return func(actual string) string { + if testVersion == "" { + panic("must set test version in config") + } + return test.ShouldEqual(actual, "custom") + } + } else { + return func(actual string) string { + if testVersion == "" { + panic("must set test version in config") + } + return test.ShouldEqual(actual, concreteTestVersion) } - return test.ShouldEqual(actual, concreteTestVersion) } case "dev": return func(actual string) string { @@ -997,13 +1006,18 @@ func bleSurfacesExpectedError(expectedErr string) error { // (concrete, "stable", "dev", or "pr.N"), resolved to a concrete GCS URL, and // injected into the script via AGENT_CUSTOM_URL. func installAgent(ctx context.Context, version string) (context.Context, error) { - version = translateToAppVersion(version) - agentStatus := serialClient.GetAgentStatus().MustGet() + // Avoid wasting time and network traffic if agent is already running at the desired version + + // First check, if agent is running, and running on the correct version if agentStatus["SubState"] == "running" { - // Avoid wasting time and network traffic if agent is already running. - return ctx, nil + _, err := testAgentRunningWithVersion(ctx, version) + if err == nil { + return ctx, nil + } } + + // Then install robotKeysResp, err := appClient.GetRobotAPIKeys(ctx, &apppb.GetRobotAPIKeysRequest{ RobotId: cfg.RobotID, }) @@ -1015,9 +1029,24 @@ func installAgent(ctx context.Context, version string) (context.Context, error) "FORCE=1 VIAM_API_KEY_ID=%s VIAM_API_KEY=%s VIAM_PART_ID=%s", robotKeys.ApiKey.Id, robotKeys.ApiKey.Key, cfg.PartID, ) - logger.Infof("Install version: %s\n", version) - cmd += fmt.Sprintf(" AGENT_CUSTOM_URL=%s", gcsURL("viam-agent", concreteTestVersion)) + " sh" - return ctx, serialClient.RunScript(installScript, cmd).Error() + logger.Infof("Install version: %s\n", concreteTestVersion) + // Don't use the concrete test version if it's a file pin, because gcsURL can't handle file pins + if !strings.HasPrefix(concreteTestVersion, "file://") { + cmd += fmt.Sprintf(" AGENT_CUSTOM_URL=%s", gcsURL("viam-agent", concreteTestVersion)) + } + cmd += " sh" + err = serialClient.RunScript(installScript, cmd).Error() + + // After install, if the version under test is a file pin, pin to the file + // assuming the binary is present on the device + + // problem: binaries print their version as "custom Git Revision: sha" + if strings.HasPrefix(concreteTestVersion, "file://") { + logger.Infof("Version under test is a file pin: %s", concreteTestVersion) + ctx, err := applyVersionPin(ctx, concreteTestVersion, "agent", "version_control", "agent") + return ctx, err + } + return ctx, err } func testAgentEnabled(ctx context.Context) (context.Context, error) { diff --git a/features/serial/uninstall.feature b/features/serial/uninstall.feature index 08f9e43e..0f129cc2 100644 --- a/features/serial/uninstall.feature +++ b/features/serial/uninstall.feature @@ -1,7 +1,7 @@ Feature: uninstall viam-agent Background: Given viam-agent is installed at the version under test - And the viam-agent systemd unit is running with version under test + And the viam-agent systemd unit is running with the version under test And the viam-agent systemd unit is enabled Scenario: Uninstall viam-agent When viam-agent is uninstalled From a5cd2eb2032485d0afd74f51b4c9495908922f2a Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Tue, 12 May 2026 16:54:05 -0400 Subject: [PATCH 23/36] Improve version spec docs --- agent-test-example.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/agent-test-example.toml b/agent-test-example.toml index c8c80b37..08e157d5 100644 --- a/agent-test-example.toml +++ b/agent-test-example.toml @@ -13,9 +13,10 @@ part_id = "" [versions] # viam_agent_test can be any of the following. the corresponding binary will be installed from GCS -# a release version e.g. 0.27.3 -# "dev" - the tip of main -# github PR number (needs "dev-release" label): "pr.xxx" i.e. "pr.226" +# a release version e.g. "0.27.3" +# a github PR number (the PR needs the "dev-release" label): "pr.xxx" i.e. "pr.226" +# "dev" for the tip of main +# "file://your/path/to/agent" a custom binary viam_agent_test = "" viam_agent_stable = "" viam_agent_old = "" From 3ed8c57772cbbc04338d7cba13ff3af6eaeba0b1 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 13:17:29 -0400 Subject: [PATCH 24/36] Update agent-test-example guidance --- agent-test-example.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/agent-test-example.toml b/agent-test-example.toml index 08e157d5..16b3692b 100644 --- a/agent-test-example.toml +++ b/agent-test-example.toml @@ -13,13 +13,15 @@ part_id = "" [versions] # viam_agent_test can be any of the following. the corresponding binary will be installed from GCS -# a release version e.g. "0.27.3" -# a github PR number (the PR needs the "dev-release" label): "pr.xxx" i.e. "pr.226" -# "dev" for the tip of main -# "file://your/path/to/agent" a custom binary +# a release version: e.g. "0.27.3" +# a github PR number (the PR needs the "dev-release" label): e.g. "pr.226" +# the tip of main: "dev" +# a custom binary (stable is installed from GCS, then the binary is pinned): "file://your/path/to/agent" viam_agent_test = "" viam_agent_stable = "" viam_agent_old = "" +# viam_server_test does not support file pinning or PR numbers yet. +# recommended to keep viam_server_test at "stable" viam_server_test = "" viam_server_stable = "" viam_server_old = "" From 890a8e26f89654b03144478c8be48ea698a66c32 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 13:18:39 -0400 Subject: [PATCH 25/36] Print versions under test, less get version retry --- agent_serial_test.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index a6280f36..f3cda754 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -13,6 +13,7 @@ import ( "os/exec" "regexp" "runtime" + "slices" "sort" "strconv" "strings" @@ -218,6 +219,31 @@ func InitializeScenario(ctx *godog.ScenarioContext) { return ctx, err } } + + centerPrint := func(msg string, width int) { + padLenTotal := width - len(msg) + padLeft := padLenTotal / 2 + padRight := padLenTotal - padLeft + + fmt.Printf("%s%s%s\n", strings.Repeat(" ", padLeft), msg, strings.Repeat(" ", padRight)) + } + + testMsgs := []string{ + fmt.Sprintf("Testing Agent Version: %s (%s)", cfg.Versions.Test, concreteTestVersion), + fmt.Sprintf("Stable Agent Version: %s", cfg.Versions.Stable), + fmt.Sprintf("Testing Server Version: %s", cfg.Versions.ViamServerTest), + fmt.Sprintf("Stable Server Version: %s", cfg.Versions.ViamServerStable), + } + consoleWidth := len(slices.MaxFunc(testMsgs, func(a, b string) int { return len(a) - len(b) })) + 8 + fmt.Println(strings.Repeat("=", consoleWidth)) + fmt.Println(strings.Repeat("=", consoleWidth)) + centerPrint(testMsgs[0], consoleWidth) + fmt.Println() + for _, m := range testMsgs[1:] { + centerPrint(m, consoleWidth) + } + fmt.Println(strings.Repeat("=", consoleWidth)) + fmt.Println(strings.Repeat("=", consoleWidth)) return ctx, nil }) @@ -1084,13 +1110,14 @@ func testSystemdAgentStartVersion(ctx context.Context, version string) (context. time.Sleep(time.Second * 2) } lastAgentVer := serialClient.GetAgentLastStartVersion() + // if we failed to get a version, keep trying if lastAgentVer.IsError() { err = lastAgentVer.Error() continue } + // if we got a version, but it's not what's expected, don't keep waiting if check := versionTest(lastAgentVer.MustGet()); check != "" { - err = errors.New(check) - continue + return ctx, errors.New(check) } return ctx, nil } From 50171d0d9a0de408a891794e2a48c95e89e6378f Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 13:22:07 -0400 Subject: [PATCH 26/36] Small fixes to test sequences --- features/serial/provision-ble.feature | 3 ++- features/serial/provision-wifi.feature | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/features/serial/provision-ble.feature b/features/serial/provision-ble.feature index d9164f7c..4ab90105 100644 --- a/features/serial/provision-ble.feature +++ b/features/serial/provision-ble.feature @@ -2,8 +2,8 @@ Feature: bluetooth provisioning Background: Given viam-agent is installed at the version under test + And the viam-agent systemd unit is running with the version under test And the viam-agent systemd unit is enabled - And the viam-agent systemd unit is running with version under test And there are no available wifi networks And viam-agent cannot reach the app Scenario: The agent enters automatic provisioning mode when expected @@ -13,6 +13,7 @@ Feature: bluetooth provisioning When viam-agent is in forced provisioning mode And the viam-agent bluetooth device is discoverable with the expected characteristics And the host shares an insecure wifi network via bluetooth + Then viam-agent can reach the app Scenario: The agent can join an unknown secure network when one is provided during bluetooth provisioning When viam-agent is in forced provisioning mode And the viam-agent bluetooth device is discoverable with the expected characteristics diff --git a/features/serial/provision-wifi.feature b/features/serial/provision-wifi.feature index 51828d18..13db4d58 100644 --- a/features/serial/provision-wifi.feature +++ b/features/serial/provision-wifi.feature @@ -2,8 +2,8 @@ Feature: wifi provisioning Background: Given viam-agent is installed at the version under test - And the viam-agent systemd unit is enabled And the viam-agent systemd unit is running with version under test + And the viam-agent systemd unit is enabled And there are no available wifi networks And viam-agent cannot reach the app Scenario: The agent enters automatic provisioning mode when expected From 76ce84e25603e46e458ff72fca648bd849ff19e6 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 13:56:59 -0400 Subject: [PATCH 27/36] Update guidance again --- agent-test-example.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/agent-test-example.toml b/agent-test-example.toml index 16b3692b..1ac3112a 100644 --- a/agent-test-example.toml +++ b/agent-test-example.toml @@ -13,10 +13,12 @@ part_id = "" [versions] # viam_agent_test can be any of the following. the corresponding binary will be installed from GCS -# a release version: e.g. "0.27.3" -# a github PR number (the PR needs the "dev-release" label): e.g. "pr.226" -# the tip of main: "dev" -# a custom binary (stable is installed from GCS, then the binary is pinned): "file://your/path/to/agent" + +# - a github PR number (the PR needs the "dev-release" label): e.g. "pr.226" +# - the tip of main: "dev" +# - current stable release: "stable" +# - a release version: e.g. "0.27.3" +# - a custom binary (stable is installed from GCS, then the binary is pinned): "file://your/path/to/agent" viam_agent_test = "" viam_agent_stable = "" viam_agent_old = "" From 1cc562706e882f2760bd0b3fdbb1dd470ed43af3 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 15:01:20 -0400 Subject: [PATCH 28/36] Add "wait" arg for some uses --- agent_serial_test.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index f3cda754..885e9cfa 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -184,7 +184,7 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { time.Sleep(time.Second * 3) // Pin back to the version under test if _, err := applyAgentVersionPin(ctx, "the version under test"); err != nil { - t.Logf("error pinning agent back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerTest, err) + t.Logf("error pinning agent back to \"%s\" during cleanup: %v", cfg.Versions.Test, err) } if _, err := applyViamServerVersionPin(ctx, "the version under test"); err != nil { t.Logf("error pinning viam-server back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerTest, err) @@ -525,7 +525,7 @@ func setField(root *structpb.Struct, value *structpb.Value, path ...string) erro // and is not an actual version specification like the one used in a viam robot config. // oldVersion is the concrete version string to use for the string "an old version". -// testVersion is the concrete version string to use for the string "test". +// testVersion is the concrete version string to use for the string "the version under test". func translateVersion(version, oldVersion, testVersion string) string { switch version { case "an old version": @@ -1037,7 +1037,7 @@ func installAgent(ctx context.Context, version string) (context.Context, error) // First check, if agent is running, and running on the correct version if agentStatus["SubState"] == "running" { - _, err := testAgentRunningWithVersion(ctx, version) + _, err := testSystemdAgentStartVersion(ctx, version, false) if err == nil { return ctx, nil } @@ -1092,14 +1092,14 @@ func testAgentNotFound(ctx context.Context) (context.Context, error) { } func testAgentRunningWithVersion(ctx context.Context, version string) (context.Context, error) { - ctx, err := testSystemdAgentStartVersion(ctx, version) + ctx, err := testSystemdAgentStartVersion(ctx, version, true) if err != nil { return ctx, err } return testAgentState(ctx, "SubState", "running") } -func testSystemdAgentStartVersion(ctx context.Context, version string) (context.Context, error) { +func testSystemdAgentStartVersion(ctx context.Context, version string, wait bool) (context.Context, error) { versionTest := versionStrToMatcher(version) logger.Infof("Check for version %s with matcher %s\n", version, versionTest) var err error @@ -1115,9 +1115,14 @@ func testSystemdAgentStartVersion(ctx context.Context, version string) (context. err = lastAgentVer.Error() continue } - // if we got a version, but it's not what's expected, don't keep waiting + // if we got a version, but it's not what's expected, keep trying if "wait" is set if check := versionTest(lastAgentVer.MustGet()); check != "" { - return ctx, errors.New(check) + if wait { + err = errors.New(check) + continue + } else { + return ctx, errors.New(check) + } } return ctx, nil } From 0d4bc7fd22534c2588a5bdf00df04269edc60cc9 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 15:01:52 -0400 Subject: [PATCH 29/36] Fix tests to support test version specification --- features/serial/agent-pin-file.feature | 3 +-- features/serial/agent-pin-url.feature | 3 +-- features/serial/install.feature | 1 - features/serial/provision-wifi.feature | 2 +- features/serial/uninstall.feature | 1 - features/serial/viamserver-pin-file.feature | 3 +-- features/serial/viamserver-pin-url.feature | 3 +-- features/serial/viamserver-pin-version.feature | 3 +-- 8 files changed, 6 insertions(+), 13 deletions(-) diff --git a/features/serial/agent-pin-file.feature b/features/serial/agent-pin-file.feature index 3beef640..2f8a8176 100644 --- a/features/serial/agent-pin-file.feature +++ b/features/serial/agent-pin-file.feature @@ -2,8 +2,7 @@ Feature: Pin viam-agent to an old version via a local file Background: Given viam-agent is installed at the version under test - And viam-agent is pinned to stable - And the viam-agent systemd unit is running with stable + And the viam-agent systemd unit is running with the version under test Scenario: Pin viam-agent to an old version via a local file Given an old viam-agent binary is present on the device diff --git a/features/serial/agent-pin-url.feature b/features/serial/agent-pin-url.feature index e30e9198..df660fa5 100644 --- a/features/serial/agent-pin-url.feature +++ b/features/serial/agent-pin-url.feature @@ -2,8 +2,7 @@ Feature: Pin viam-agent to an old version via a URL Background: Given viam-agent is installed at the version under test - And viam-agent is pinned to stable - And the viam-agent systemd unit is running with stable + And the viam-agent systemd unit is running with the version under test Scenario: Pin viam-agent to an old version via a URL When viam-agent is pinned to a url diff --git a/features/serial/install.feature b/features/serial/install.feature index 4c56a8cc..2e58000d 100644 --- a/features/serial/install.feature +++ b/features/serial/install.feature @@ -3,7 +3,6 @@ Feature: install viam-agent Given viam-agent is not installed Scenario: Install the viam-agent version under test When viam-agent is installed at the version under test - Then the viam-agent systemd unit is running with the version under test And the viam-agent systemd unit is enabled And the journald config is live And the wifi power save config is live diff --git a/features/serial/provision-wifi.feature b/features/serial/provision-wifi.feature index 13db4d58..1aa8db70 100644 --- a/features/serial/provision-wifi.feature +++ b/features/serial/provision-wifi.feature @@ -2,7 +2,7 @@ Feature: wifi provisioning Background: Given viam-agent is installed at the version under test - And the viam-agent systemd unit is running with version under test + And the viam-agent systemd unit is running with the version under test And the viam-agent systemd unit is enabled And there are no available wifi networks And viam-agent cannot reach the app diff --git a/features/serial/uninstall.feature b/features/serial/uninstall.feature index 0f129cc2..dc23445b 100644 --- a/features/serial/uninstall.feature +++ b/features/serial/uninstall.feature @@ -1,7 +1,6 @@ Feature: uninstall viam-agent Background: Given viam-agent is installed at the version under test - And the viam-agent systemd unit is running with the version under test And the viam-agent systemd unit is enabled Scenario: Uninstall viam-agent When viam-agent is uninstalled diff --git a/features/serial/viamserver-pin-file.feature b/features/serial/viamserver-pin-file.feature index e69124f9..7a06626c 100644 --- a/features/serial/viamserver-pin-file.feature +++ b/features/serial/viamserver-pin-file.feature @@ -2,9 +2,8 @@ Feature: Pin viam-server to an old version via a local file Background: Given viam-agent is installed at the version under test - And viam-agent is pinned to stable + And the viam-agent systemd unit is running with the version under test And viam-server is pinned to stable - And the viam-agent systemd unit is running with stable Scenario: Pin viam-server to an old version via a local file Given an old viam-server binary is present on the device diff --git a/features/serial/viamserver-pin-url.feature b/features/serial/viamserver-pin-url.feature index 6e38cddb..1b66b0be 100644 --- a/features/serial/viamserver-pin-url.feature +++ b/features/serial/viamserver-pin-url.feature @@ -2,9 +2,8 @@ Feature: Pin viam-server to an old version via a URL Background: Given viam-agent is installed at the version under test - And viam-agent is pinned to stable + And the viam-agent systemd unit is running with the version under test And viam-server is pinned to stable - And the viam-agent systemd unit is running with stable Scenario: Pin viam-server to an old version via a URL When viam-server is pinned to a url diff --git a/features/serial/viamserver-pin-version.feature b/features/serial/viamserver-pin-version.feature index 86f6ce1f..d30c9dcd 100644 --- a/features/serial/viamserver-pin-version.feature +++ b/features/serial/viamserver-pin-version.feature @@ -2,9 +2,8 @@ Feature: Pin viam-server to an older version Background: Given viam-agent is installed at the version under test - And viam-agent is pinned to stable + And the viam-agent systemd unit is running with the version under test And viam-server is pinned to stable - And the viam-agent systemd unit is running with stable Scenario: Pin viam-server to an older version When viam-server is pinned to an old version From 46d363c39f2ea6d98e126d2e0e7b0b9d461a0793 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 15:07:21 -0400 Subject: [PATCH 30/36] Bump suggested stable/old agent versions --- agent-test-example.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent-test-example.toml b/agent-test-example.toml index 1ac3112a..9021bddd 100644 --- a/agent-test-example.toml +++ b/agent-test-example.toml @@ -20,8 +20,8 @@ part_id = "" # - a release version: e.g. "0.27.3" # - a custom binary (stable is installed from GCS, then the binary is pinned): "file://your/path/to/agent" viam_agent_test = "" -viam_agent_stable = "" -viam_agent_old = "" +viam_agent_stable = "" +viam_agent_old = "" # viam_server_test does not support file pinning or PR numbers yet. # recommended to keep viam_server_test at "stable" viam_server_test = "" From 7e29efb2941678812c3e32bb246fe4dea5d384cb Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 16:32:04 -0400 Subject: [PATCH 31/36] Fix some bugs, make upgrade test make sense --- agent_serial_test.go | 30 +++++++++++++++++++++++------- features/serial/upgrade.feature | 11 ++++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 885e9cfa..bdc712f3 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -1055,10 +1055,16 @@ func installAgent(ctx context.Context, version string) (context.Context, error) "FORCE=1 VIAM_API_KEY_ID=%s VIAM_API_KEY=%s VIAM_PART_ID=%s", robotKeys.ApiKey.Id, robotKeys.ApiKey.Key, cfg.PartID, ) - logger.Infof("Install version: %s\n", concreteTestVersion) + + concrete, err := resolveVersionSpec(ctx, version) + if err != nil { + return ctx, err + } + + logger.Infof("Install version: %s\n", concrete) // Don't use the concrete test version if it's a file pin, because gcsURL can't handle file pins - if !strings.HasPrefix(concreteTestVersion, "file://") { - cmd += fmt.Sprintf(" AGENT_CUSTOM_URL=%s", gcsURL("viam-agent", concreteTestVersion)) + if !strings.HasPrefix(concrete, "file://") { + cmd += fmt.Sprintf(" AGENT_CUSTOM_URL=%s", gcsURL("viam-agent", concrete)) } cmd += " sh" err = serialClient.RunScript(installScript, cmd).Error() @@ -1067,9 +1073,9 @@ func installAgent(ctx context.Context, version string) (context.Context, error) // assuming the binary is present on the device // problem: binaries print their version as "custom Git Revision: sha" - if strings.HasPrefix(concreteTestVersion, "file://") { - logger.Infof("Version under test is a file pin: %s", concreteTestVersion) - ctx, err := applyVersionPin(ctx, concreteTestVersion, "agent", "version_control", "agent") + if strings.HasPrefix(concrete, "file://") { + logger.Infof("Version under test is a file pin: %s", concrete) + ctx, err := applyVersionPin(ctx, concrete, "agent", "version_control", "agent") return ctx, err } return ctx, err @@ -1142,7 +1148,17 @@ func testAgentState(ctx context.Context, key, expectedVal string) (context.Conte } func translateToAppVersion(version string) string { - return translateVersion(version, cfg.Versions.Old, cfg.Versions.Test) + appVersion := translateVersion(version, cfg.Versions.Old, cfg.Versions.Test) + + // for now, special handling for "the version under test": + // the only way to translate all the different version specs into something the app can + // understand is by pinning directly to the corresponding URL + + // so intercept and return that instead (this is done here so "viam-agent" can be passed) + if version == "the version under test" { + appVersion = gcsURL("viam-agent", concreteTestVersion) + } + return appVersion } func versionStrToMatcher(version string) func(string) string { diff --git a/features/serial/upgrade.feature b/features/serial/upgrade.feature index 90c4093f..6721a560 100644 --- a/features/serial/upgrade.feature +++ b/features/serial/upgrade.feature @@ -1,8 +1,9 @@ -Feature: Upgrade viam-agent +Feature: Upgrade viam-agent from stable to the version under test Background: - Given viam-agent is installed at the version under test + Given viam-agent is installed at stable + And viam-agent is pinned to stable And the viam-agent systemd unit is running with stable - Scenario: Pin viam agent to stable - When viam-agent is pinned to dev - Then the viam-agent systemd unit is running with dev + Scenario: Pin viam agent to the version under test + When viam-agent is pinned to the version under test + Then the viam-agent systemd unit is running with the version under test And the viam-agent systemd unit is enabled From 35ceabe9cf495c31b594d5f02a22841a00022835 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Thu, 14 May 2026 17:03:55 -0400 Subject: [PATCH 32/36] Fix version translation, downgrade test --- agent_serial_test.go | 3 ++- features/serial/downgrade.feature | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index bdc712f3..af6834e6 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -1056,7 +1056,8 @@ func installAgent(ctx context.Context, version string) (context.Context, error) robotKeys.ApiKey.Id, robotKeys.ApiKey.Key, cfg.PartID, ) - concrete, err := resolveVersionSpec(ctx, version) + appVersion := translateVersion(version, cfg.Versions.Old, cfg.Versions.Test) + concrete, err := resolveVersionSpec(ctx, appVersion) if err != nil { return ctx, err } diff --git a/features/serial/downgrade.feature b/features/serial/downgrade.feature index 66f2611c..ed56dd40 100644 --- a/features/serial/downgrade.feature +++ b/features/serial/downgrade.feature @@ -1,7 +1,8 @@ -Feature: Downgrade viam-agent +Feature: Downgrade viam-agent from the version under test to stable Background: Given viam-agent is installed at the version under test - And the viam-agent systemd unit is running with stable + And viam-agent is pinned to the version under test + And the viam-agent systemd unit is running with the version under test Scenario: Pin viam agent to an old version When viam-agent is pinned to an old version Then the viam-agent systemd unit is running with an old version From 660f4f089010f26622048a2fca499c1426760452 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Fri, 15 May 2026 12:26:14 -0400 Subject: [PATCH 33/36] Remove "version under test" for viam-server --- agent-test-example.toml | 3 --- agent_serial_test.go | 10 ++++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/agent-test-example.toml b/agent-test-example.toml index 9021bddd..f4579b79 100644 --- a/agent-test-example.toml +++ b/agent-test-example.toml @@ -22,9 +22,6 @@ part_id = "" viam_agent_test = "" viam_agent_stable = "" viam_agent_old = "" -# viam_server_test does not support file pinning or PR numbers yet. -# recommended to keep viam_server_test at "stable" -viam_server_test = "" viam_server_stable = "" viam_server_old = "" diff --git a/agent_serial_test.go b/agent_serial_test.go index af6834e6..7d8aa5b6 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -66,7 +66,6 @@ type versionsCfg struct { Test string `toml:"viam_agent_test"` Stable string `toml:"viam_agent_stable"` Old string `toml:"viam_agent_old"` - ViamServerTest string `toml:"viam_server_test"` ViamServerStable string `toml:"viam_server_stable"` ViamServerOld string `toml:"viam_server_old"` } @@ -186,8 +185,8 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { if _, err := applyAgentVersionPin(ctx, "the version under test"); err != nil { t.Logf("error pinning agent back to \"%s\" during cleanup: %v", cfg.Versions.Test, err) } - if _, err := applyViamServerVersionPin(ctx, "the version under test"); err != nil { - t.Logf("error pinning viam-server back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerTest, err) + if _, err := applyViamServerVersionPin(ctx, "stable"); err != nil { + t.Logf("error pinning viam-server back to \"%s\" during cleanup: %v", cfg.Versions.ViamServerStable, err) } if err := serialClient.Close(); err != nil { t.Logf("error closing serial client during cleanup: %v", err) @@ -231,7 +230,6 @@ func InitializeScenario(ctx *godog.ScenarioContext) { testMsgs := []string{ fmt.Sprintf("Testing Agent Version: %s (%s)", cfg.Versions.Test, concreteTestVersion), fmt.Sprintf("Stable Agent Version: %s", cfg.Versions.Stable), - fmt.Sprintf("Testing Server Version: %s", cfg.Versions.ViamServerTest), fmt.Sprintf("Stable Server Version: %s", cfg.Versions.ViamServerStable), } consoleWidth := len(slices.MaxFunc(testMsgs, func(a, b string) int { return len(a) - len(b) })) + 8 @@ -1221,11 +1219,11 @@ func testViamServerRunningWithVersion(ctx context.Context, version string) (cont } func translateVersionViamServer(version string) string { - return translateVersion(version, cfg.Versions.ViamServerOld, cfg.Versions.ViamServerTest) + return translateVersion(version, cfg.Versions.ViamServerOld, "") } func versionStrToMatcherViamServer(version string) func(string) string { - return versionStrToMatcherBase(version, cfg.Versions.ViamServerOld, cfg.Versions.ViamServerStable, cfg.Versions.ViamServerTest) + return versionStrToMatcherBase(version, cfg.Versions.ViamServerOld, cfg.Versions.ViamServerStable, "") } func applyViamServerVersionPin(ctx context.Context, version string) (context.Context, error) { From dbae0e6086aaf68014a6b878ba93cd9201b0aff7 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Fri, 15 May 2026 13:29:33 -0400 Subject: [PATCH 34/36] Re-add ensureonline to pre steps --- agent_serial_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agent_serial_test.go b/agent_serial_test.go index 7d8aa5b6..fe06c990 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -219,6 +219,10 @@ func InitializeScenario(ctx *godog.ScenarioContext) { } } + if err := serialClient.EnsureOnline(cfg.Wifi.SSID, cfg.Wifi.Password); err != nil { + return ctx, err + } + centerPrint := func(msg string, width int) { padLenTotal := width - len(msg) padLeft := padLenTotal / 2 From 2c8a8154d13ddef1cc4fcd3419199f3a9317539b Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 27 May 2026 11:57:29 -0400 Subject: [PATCH 35/36] Address pr comments --- agent_serial_test.go | 28 +++++++++++-------- .../agent-reject-viam-server-binary.feature | 6 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index fe06c990..42bf592d 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -170,6 +170,20 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { // Setup failed, panic panic(err) } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + // now that everything is set up, log the test conditions + if cfg.Versions.Test != "" { + concrete, err := resolveVersionSpec(ctx, cfg.Versions.Test) + concreteTestVersion = concrete + if err != nil { + panic(fmt.Errorf("resolving install version %q: %w", cfg.Versions.Test, err)) + } + logger.Infof("Version under test: %s\n", concreteTestVersion) + } else { + panic(fmt.Errorf("viam_agent_test in agent-test.toml cannot be empty string")) + } }) tsc.AfterSuite(func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) @@ -202,16 +216,6 @@ func InitializeScenario(ctx *godog.ScenarioContext) { // scenario starts with a fresh systemd InvocationID. This ensures that // journal-based checks cannot match log lines produced by a previous scenario. ctx.Before(func(ctx context.Context, _ *godog.Scenario) (context.Context, error) { - if cfg.Versions.Test != "" { - concrete, err := resolveVersionSpec(ctx, cfg.Versions.Test) - concreteTestVersion = concrete - logger.Infof("Version under test: %s\n", concreteTestVersion) - if err != nil { - panic(fmt.Errorf("resolving install version %q: %w", cfg.Versions.Test, err)) - } - } else { - panic(fmt.Errorf("viam_agent_test in agent-test.toml cannot be empty string")) - } status := serialClient.GetAgentStatus() if status.IsOk() && status.MustGet()["SubState"] == "running" { if err := serialClient.RestartAgent().Error(); err != nil { @@ -1071,6 +1075,9 @@ func installAgent(ctx context.Context, version string) (context.Context, error) } cmd += " sh" err = serialClient.RunScript(installScript, cmd).Error() + if err != nil { + return ctx, err + } // After install, if the version under test is a file pin, pin to the file // assuming the binary is present on the device @@ -1110,7 +1117,6 @@ func testAgentRunningWithVersion(ctx context.Context, version string) (context.C func testSystemdAgentStartVersion(ctx context.Context, version string, wait bool) (context.Context, error) { versionTest := versionStrToMatcher(version) - logger.Infof("Check for version %s with matcher %s\n", version, versionTest) var err error // Agent needs time to fetch the new config, possibly download the new // version, and restart. diff --git a/features/serial/agent-reject-viam-server-binary.feature b/features/serial/agent-reject-viam-server-binary.feature index ec018f6e..a55bffa2 100644 --- a/features/serial/agent-reject-viam-server-binary.feature +++ b/features/serial/agent-reject-viam-server-binary.feature @@ -2,10 +2,10 @@ Feature: Pinning viam-agent to a viam-server binary is rejected Background: Given viam-agent is installed at the version under test - And viam-agent is pinned to stable - And the viam-agent systemd unit is running with stable + And viam-agent is pinned to the version under test + And the viam-agent systemd unit is running with the version under test Scenario: Pinning viam-agent to a viam-server binary is rejected When viam-agent is pinned to a viam-server binary Then viam-agent rejected the invalid binary - And the viam-agent systemd unit is running with stable + And the viam-agent systemd unit is running with the version under test From 4b0c00a938b4dba878f4d666e8fed3b2e68c9876 Mon Sep 17 00:00:00 2001 From: Evan Dorsky Date: Wed, 27 May 2026 13:55:03 -0400 Subject: [PATCH 36/36] More PR comments --- agent_serial_test.go | 82 +++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/agent_serial_test.go b/agent_serial_test.go index 42bf592d..264ccc98 100644 --- a/agent_serial_test.go +++ b/agent_serial_test.go @@ -184,6 +184,30 @@ func InitializeSuite(t *testing.T) func(*godog.TestSuiteContext) { } else { panic(fmt.Errorf("viam_agent_test in agent-test.toml cannot be empty string")) } + + centerPrint := func(msg string, width int) { + padLenTotal := width - len(msg) + padLeft := padLenTotal / 2 + padRight := padLenTotal - padLeft + + fmt.Printf("%s%s%s\n", strings.Repeat(" ", padLeft), msg, strings.Repeat(" ", padRight)) + } + + testMsgs := []string{ + fmt.Sprintf("Testing Agent Version: %s (%s)", cfg.Versions.Test, concreteTestVersion), + fmt.Sprintf("Stable Agent Version: %s", cfg.Versions.Stable), + fmt.Sprintf("Stable Server Version: %s", cfg.Versions.ViamServerStable), + } + consoleWidth := len(slices.MaxFunc(testMsgs, func(a, b string) int { return len(a) - len(b) })) + 8 + fmt.Println(strings.Repeat("=", consoleWidth)) + fmt.Println(strings.Repeat("=", consoleWidth)) + centerPrint(testMsgs[0], consoleWidth) + fmt.Println() + for _, m := range testMsgs[1:] { + centerPrint(m, consoleWidth) + } + fmt.Println(strings.Repeat("=", consoleWidth)) + fmt.Println(strings.Repeat("=", consoleWidth)) }) tsc.AfterSuite(func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) @@ -227,29 +251,6 @@ func InitializeScenario(ctx *godog.ScenarioContext) { return ctx, err } - centerPrint := func(msg string, width int) { - padLenTotal := width - len(msg) - padLeft := padLenTotal / 2 - padRight := padLenTotal - padLeft - - fmt.Printf("%s%s%s\n", strings.Repeat(" ", padLeft), msg, strings.Repeat(" ", padRight)) - } - - testMsgs := []string{ - fmt.Sprintf("Testing Agent Version: %s (%s)", cfg.Versions.Test, concreteTestVersion), - fmt.Sprintf("Stable Agent Version: %s", cfg.Versions.Stable), - fmt.Sprintf("Stable Server Version: %s", cfg.Versions.ViamServerStable), - } - consoleWidth := len(slices.MaxFunc(testMsgs, func(a, b string) int { return len(a) - len(b) })) + 8 - fmt.Println(strings.Repeat("=", consoleWidth)) - fmt.Println(strings.Repeat("=", consoleWidth)) - centerPrint(testMsgs[0], consoleWidth) - fmt.Println() - for _, m := range testMsgs[1:] { - centerPrint(m, consoleWidth) - } - fmt.Println(strings.Repeat("=", consoleWidth)) - fmt.Println(strings.Repeat("=", consoleWidth)) return ctx, nil }) @@ -287,7 +288,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) { // Agent upgrade/downgrade steps (version/URL/file) ctx.Step(fmt.Sprintf(`the viam-agent systemd unit is running with %s$`, versionGroup), testAgentRunningWithVersion) - ctx.Step(fmt.Sprintf(`the viam-agent systemd unit started with %s`, versionGroup), testSystemdAgentStartVersion) + ctx.Step(fmt.Sprintf(`the viam-agent systemd unit started with %s`, versionGroup), waitForSystemdAgentVersion) ctx.Step(fmt.Sprintf(`viam-agent is pinned to %s`, versionGroup), applyAgentVersionPin) ctx.Step(`viam-agent is pinned to a url$`, applyAgentURLPin) ctx.Step(`viam-agent is pinned to a file$`, applyAgentFilePin) @@ -1043,7 +1044,7 @@ func installAgent(ctx context.Context, version string) (context.Context, error) // First check, if agent is running, and running on the correct version if agentStatus["SubState"] == "running" { - _, err := testSystemdAgentStartVersion(ctx, version, false) + _, err := checkSystemdAgentVersion(ctx, version) if err == nil { return ctx, nil } @@ -1108,15 +1109,27 @@ func testAgentNotFound(ctx context.Context) (context.Context, error) { } func testAgentRunningWithVersion(ctx context.Context, version string) (context.Context, error) { - ctx, err := testSystemdAgentStartVersion(ctx, version, true) + ctx, err := waitForSystemdAgentVersion(ctx, version) if err != nil { return ctx, err } return testAgentState(ctx, "SubState", "running") } -func testSystemdAgentStartVersion(ctx context.Context, version string, wait bool) (context.Context, error) { +// returns nil err on match, err otherwise +func checkSystemdAgentVersion(ctx context.Context, version string) (context.Context, error) { versionTest := versionStrToMatcher(version) + lastAgentVer := serialClient.GetAgentLastStartVersion() + if lastAgentVer.IsError() { + return ctx, lastAgentVer.Error() + } + if check := versionTest(lastAgentVer.MustGet()); check != "" { + return ctx, errors.New(check) + } + return ctx, nil +} + +func waitForSystemdAgentVersion(ctx context.Context, version string) (context.Context, error) { var err error // Agent needs time to fetch the new config, possibly download the new // version, and restart. @@ -1124,21 +1137,10 @@ func testSystemdAgentStartVersion(ctx context.Context, version string, wait bool if i > 0 { time.Sleep(time.Second * 2) } - lastAgentVer := serialClient.GetAgentLastStartVersion() - // if we failed to get a version, keep trying - if lastAgentVer.IsError() { - err = lastAgentVer.Error() + // if we didn't get the expected version, try again + if _, err = checkSystemdAgentVersion(ctx, version); err != nil { continue } - // if we got a version, but it's not what's expected, keep trying if "wait" is set - if check := versionTest(lastAgentVer.MustGet()); check != "" { - if wait { - err = errors.New(check) - continue - } else { - return ctx, errors.New(check) - } - } return ctx, nil } return ctx, err