diff --git a/deploy/install/test_sandbox_setup.py b/deploy/install/test_sandbox_setup.py index 9d1d7b5b7..3ecdec5de 100644 --- a/deploy/install/test_sandbox_setup.py +++ b/deploy/install/test_sandbox_setup.py @@ -12,17 +12,15 @@ class SandboxSetupTests(unittest.TestCase): - def test_e2b_endpoint_pair(self): - for api_url, domain in ((None, None), ("https://sandbox-test.sandbase.ai", "sandbox-test.sandbase.ai")): - self.assertTrue(sandbox_setup.e2b_endpoint(api_url, domain)) - for api_url, domain in (("https://sandbox-test.sandbase.ai", None), - ("http://sandbox-test.sandbase.ai", "sandbox-test.sandbase.ai"), - ("https://sandbox-test.sandbase.ai/path", "sandbox-test.sandbase.ai"), - ("https://localhost", "sandbox-test.sandbase.ai"), - ("https://127.0.0.1", "sandbox-test.sandbase.ai"), - ("https://unrelated.example", "sandbox-test.sandbase.ai"), - ("https://good.example", "-bad.example")): - self.assertFalse(sandbox_setup.e2b_endpoint(api_url, domain)) + def test_shared_e2b_selectors(self): + fixture = Path(__file__).resolve().parents[2] / "services/core/internal/sandbox/e2b/testdata/configuration-selectors.json" + cases = json.loads(fixture.read_text()) + for case in cases["endpoints"]: + with self.subTest(endpoint=case["name"]): + self.assertEqual(sandbox_setup.e2b_endpoint(case["api_url"], case["domain"]), case["valid"]) + for case in cases["templates"]: + with self.subTest(template=case["name"]): + self.assertEqual(sandbox_setup.e2b_template(case["value"]), case["valid"]) def setUp(self): base = Path.home() / ".oac/tests/sandbox-setup" @@ -70,13 +68,6 @@ def send(req): self.assertEqual(writes, [dict(selection, expected_generation=generation)]) self.assertNotIn("expected_generation", selection) - def test_e2b_template_is_an_exact_build(self): - build = "0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11" - for value in ("base:" + build, "my_template-2:" + build): - self.assertTrue(sandbox_setup.e2b_template(value), value) - for value in ("base", "base:", ":" + build, "base:" + build.upper(), "base:" + build.replace("-", ""), - "base:00000000-0000-0000-0000-000000000000", "ba.se:" + build, "x" * 129 + ":" + build): - self.assertFalse(sandbox_setup.e2b_template(value), value) if __name__ == "__main__": diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index 4f781c39e..1cf4b37d8 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -198,6 +198,8 @@ An administrator [Session archive](../contracts/agents-api/admin-api.md#session- ## Validate the integration +E2B template and endpoint validators in the installer and Go adapter consume the shared [selector fixtures](../services/core/internal/sandbox/e2b/testdata/configuration-selectors.json). Extend these cases with any validation change so both entry points accept the same selectors. + Run `make check-sandbox-provider-contract` while developing. It runs the shared [`contracttest`](../services/core/internal/sandbox/contracttest) suite through real adapter boundaries with controlled native failures, plus the adapter and node transport tests; `make check` includes the same packages. Call the public failure runner with native-side fixtures instead of a fake `SandboxProvider`, and keep tests for foreign ownership, unknown mutation results, cancellation, no automatic replay, failed cleanup and reference-bound settlement. Node tests separately cover disconnect and reconnect fencing and cleanup after a lost Create response. Helper protocols and the [sandbox node protocol](../contracts/agents-api/node-generation-protocol.md) require an exact version match; direct in-process interfaces have no separate wire version. diff --git a/services/core/internal/sandbox/e2b/endpoint_test.go b/services/core/internal/sandbox/e2b/endpoint_test.go index c7bd1e15f..4d441a904 100644 --- a/services/core/internal/sandbox/e2b/endpoint_test.go +++ b/services/core/internal/sandbox/e2b/endpoint_test.go @@ -1,37 +1,56 @@ package e2b import ( - "strings" + "encoding/json" + "os" "testing" ) -func TestNormalizeEndpoint(t *testing.T) { - for _, tc := range []struct { - apiURL, domain string - valid bool - }{ - {"", "", true}, - {"https://sandbox-test.sandbase.ai", "sandbox-test.sandbase.ai", true}, - {"https://api.e2b.app", "e2b.app", true}, - {"https://api.sandbox-test.sandbase.ai", "sandbox-test.sandbase.ai", true}, - {"https://unrelated.example", "sandbox-test.sandbase.ai", false}, - {"https://sandbox-test.sandbase.ai", "", false}, - {"", "sandbox-test.sandbase.ai", false}, - {"http://sandbox-test.sandbase.ai", "sandbox-test.sandbase.ai", false}, - {"https://sandbox-test.sandbase.ai/path", "sandbox-test.sandbase.ai", false}, - {"https://sandbox-test.sandbase.ai:443", "sandbox-test.sandbase.ai", false}, - {"https://127.0.0.1", "sandbox-test.sandbase.ai", false}, - {"https://localhost", "sandbox-test.sandbase.ai", false}, - {"https://-bad.example", "sandbox-test.sandbase.ai", false}, - {"https://good.example", "bad..example", false}, - {"https://" + strings.Repeat("a.", 125) + "example", "good.example", false}, - } { - apiURL, domain, err := NormalizeEndpoint(tc.apiURL, tc.domain) - if (err == nil) != tc.valid { - t.Errorf("NormalizeEndpoint(%q, %q): unexpected error %v", tc.apiURL, tc.domain, err) +// The installer consumes this same fixture; native selectors are owned here. +func TestSharedConfigurationSelectors(t *testing.T) { + raw, err := os.ReadFile("testdata/configuration-selectors.json") + if err != nil { + t.Fatal(err) + } + var cases struct { + Endpoints []struct { + Name string + APIURL string `json:"api_url"` + Domain string + Valid bool } - if tc.apiURL == "" && tc.domain == "" && (apiURL != OfficialAPIURL || domain != OfficialDomain) { - t.Errorf("official defaults: %q %q", apiURL, domain) + Templates []struct { + Name string + Value string + Valid bool } } + if err := json.Unmarshal(raw, &cases); err != nil { + t.Fatal(err) + } + for _, tc := range cases.Endpoints { + t.Run("endpoint/"+tc.Name, func(t *testing.T) { + apiURL, domain, err := NormalizeEndpoint(tc.APIURL, tc.Domain) + if (err == nil) != tc.Valid { + t.Fatalf("valid=%v: %v", tc.Valid, err) + } + if tc.Valid { + expectedURL, expectedDomain := tc.APIURL, tc.Domain + if expectedURL == "" && expectedDomain == "" { + expectedURL, expectedDomain = OfficialAPIURL, OfficialDomain + } + if apiURL != expectedURL || domain != expectedDomain { + t.Fatalf("unexpected normalized selectors: %q %q", apiURL, domain) + } + } + }) + } + for _, tc := range cases.Templates { + t.Run("template/"+tc.Name, func(t *testing.T) { + err := ValidateConfiguration(&DeploymentConfiguration{APIKey: "fixture-key", Template: tc.Value}) + if (err == nil) != tc.Valid { + t.Fatalf("valid=%v: %v", tc.Valid, err) + } + }) + } } diff --git a/services/core/internal/sandbox/e2b/testdata/configuration-selectors.json b/services/core/internal/sandbox/e2b/testdata/configuration-selectors.json new file mode 100644 index 000000000..b8a84107e --- /dev/null +++ b/services/core/internal/sandbox/e2b/testdata/configuration-selectors.json @@ -0,0 +1,250 @@ +{ + "endpoints": [ + { + "name": "official", + "api_url": null, + "domain": null, + "valid": true + }, + { + "name": "explicit_official", + "api_url": "https://api.e2b.app", + "domain": "e2b.app", + "valid": true + }, + { + "name": "custom_root", + "api_url": "https://sand.example", + "domain": "sand.example", + "valid": true + }, + { + "name": "custom_subdomain", + "api_url": "https://api.sand.example", + "domain": "sand.example", + "valid": true + }, + { + "name": "one_missing_url", + "api_url": null, + "domain": "sand.example", + "valid": false + }, + { + "name": "one_missing_domain", + "api_url": "https://api.sand.example", + "domain": null, + "valid": false + }, + { + "name": "http", + "api_url": "http://api.sand.example", + "domain": "sand.example", + "valid": false + }, + { + "name": "path", + "api_url": "https://api.sand.example/", + "domain": "sand.example", + "valid": false + }, + { + "name": "query", + "api_url": "https://api.sand.example?x=1", + "domain": "sand.example", + "valid": false + }, + { + "name": "empty_query", + "api_url": "https://api.sand.example?", + "domain": "sand.example", + "valid": false + }, + { + "name": "fragment", + "api_url": "https://api.sand.example#x", + "domain": "sand.example", + "valid": false + }, + { + "name": "userinfo", + "api_url": "https://user@api.sand.example", + "domain": "sand.example", + "valid": false + }, + { + "name": "port", + "api_url": "https://api.sand.example:443", + "domain": "sand.example", + "valid": false + }, + { + "name": "uppercase", + "api_url": "https://API.sand.example", + "domain": "sand.example", + "valid": false + }, + { + "name": "foreign", + "api_url": "https://sand.example.evil.test", + "domain": "sand.example", + "valid": false + }, + { + "name": "leading_space", + "api_url": " https://api.sand.example", + "domain": "sand.example", + "valid": false + }, + { + "name": "newline", + "api_url": "https://api.sand.example\n", + "domain": "sand.example", + "valid": false + }, + { + "name": "localhost", + "api_url": "https://localhost", + "domain": "localhost", + "valid": false + }, + { + "name": "local_suffix", + "api_url": "https://sand.local", + "domain": "sand.local", + "valid": false + }, + { + "name": "localhost_suffix", + "api_url": "https://sand.localhost", + "domain": "sand.localhost", + "valid": false + }, + { + "name": "ipv4", + "api_url": "https://127.0.0.1", + "domain": "127.0.0.1", + "valid": false + }, + { + "name": "ipv6", + "api_url": "https://::1", + "domain": "::1", + "valid": false + }, + { + "name": "empty_label", + "api_url": "https://sand..example", + "domain": "sand..example", + "valid": false + }, + { + "name": "leading_dash", + "api_url": "https://-sand.example", + "domain": "-sand.example", + "valid": false + }, + { + "name": "trailing_dash", + "api_url": "https://sand-.example", + "domain": "sand-.example", + "valid": false + }, + { + "name": "underscore", + "api_url": "https://sand_box.example", + "domain": "sand_box.example", + "valid": false + }, + { + "name": "trailing_dot", + "api_url": "https://sand.example.", + "domain": "sand.example.", + "valid": false + }, + { + "name": "label_length", + "api_url": "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.example", + "domain": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.example", + "valid": false + }, + { + "name": "domain_length", + "api_url": "https://a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.example", + "domain": "a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.example", + "valid": false + } + ], + "templates": [ + { + "name": "simple", + "value": "base:0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": true + }, + { + "name": "characters", + "value": "My_template-2:0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": true + }, + { + "name": "maximum_length", + "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": true + }, + { + "name": "unpinned", + "value": "base", + "valid": false + }, + { + "name": "no_build", + "value": "base:", + "valid": false + }, + { + "name": "empty_template", + "value": ":0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": false + }, + { + "name": "uppercase_uuid", + "value": "base:0F6C1E8E-7D3A-4B8E-9A51-2B7F7F0C9D11", + "valid": false + }, + { + "name": "compact_uuid", + "value": "base:0f6c1e8e7d3a4b8e9a512b7f7f0c9d11", + "valid": false + }, + { + "name": "zero_uuid", + "value": "base:00000000-0000-0000-0000-000000000000", + "valid": false + }, + { + "name": "dot", + "value": "ba.se:0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": false + }, + { + "name": "too_long", + "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": false + }, + { + "name": "extra_colon", + "value": "base:0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11:extra", + "valid": false + }, + { + "name": "space", + "value": "base :0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": false + }, + { + "name": "unicode", + "value": "\u6a21\u677f:0f6c1e8e-7d3a-4b8e-9a51-2b7f7f0c9d11", + "valid": false + } + ] +}