From 7f2fb13c53c0f2cd85cbb91df098612bdb4e56c3 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 28 Aug 2026 01:22:59 +0200 Subject: [PATCH 1/6] Define Artemis workspace configuration --- src/cli/workspace.toit | 211 ++++++++++++++++++++++++++++++++++ src/shared/server-config.toit | 12 +- tests/workspace-test.toit | 162 ++++++++++++++++++++++++++ 3 files changed, 383 insertions(+), 2 deletions(-) create mode 100644 src/cli/workspace.toit create mode 100644 tests/workspace-test.toit diff --git a/src/cli/workspace.toit b/src/cli/workspace.toit new file mode 100644 index 00000000..128c7833 --- /dev/null +++ b/src/cli/workspace.toit @@ -0,0 +1,211 @@ +// Copyright (C) 2026 Toitware ApS. All rights reserved. + +import encoding.base64 +import fs +import host.file + +import .server-config +import .utils show read-yaml write-yaml-to-file + +ARTEMIS-FILE ::= "artemis.yaml" +WORKSPACE-VERSION ::= 1 + +FLEET-BACKEND ::= "fleet" +BROKER-BACKEND ::= "broker" +PODS-BACKEND ::= "pods" +ARTIFACTS-BACKEND ::= "artifacts" + +class WorkspaceException: + message/string + + constructor .message: + + stringify -> string: + return message + +workspace-error_ message/string: + throw (WorkspaceException message) + +/** Describes the implementation selected for one Artemis interface. */ +abstract class BackendConfig: + name/string + + constructor.from-sub_ .name: + + /** Parses a backend configuration and resolves its server reference. */ + static from-map name/string encoded/any servers/Map -> BackendConfig: + if encoded is not Map: + workspace-error_ "Backend '$name' must be a map." + + type := encoded.get "type" + if type == "file": + directory := encoded.get "directory" + if directory is not string or directory.is-empty: + workspace-error_ "File backend '$name' must have a non-empty 'directory'." + return FileBackendConfig name --directory=directory + + if type == "http": + server-name := encoded.get "server" + if server-name is not string or server-name.is-empty: + workspace-error_ "HTTP backend '$name' must reference a server." + if not servers.contains server-name: + workspace-error_ "HTTP backend '$name' references unknown server '$server-name'." + + endpoint := encoded.get "endpoint" + if endpoint is not string or not endpoint.starts-with "/": + workspace-error_ "HTTP backend '$name' must have an absolute-path 'endpoint'." + return HttpBackendConfig name + --server-config=servers[server-name] + --endpoint=endpoint + + workspace-error_ "Backend '$name' has unknown type '$type'." + unreachable + + /** Serializes this backend configuration. */ + abstract to-map -> Map + +/** Stores an interface in a directory relative to the workspace file. */ +class FileBackendConfig extends BackendConfig: + directory/string + + constructor name/string --.directory: + super.from-sub_ name + + to-map -> Map: + return { + "type": "file", + "directory": directory, + } + +/** Accesses an interface through a named server and relative endpoint. */ +class HttpBackendConfig extends BackendConfig: + server-config/ServerConfig + endpoint/string + + constructor name/string --.server-config --.endpoint: + super.from-sub_ name + + server-name -> string: + return server-config.name + + to-map -> Map: + return { + "type": "http", + "server": server-name, + "endpoint": endpoint, + } + +/** +Configuration and local context used by Artemis. + +Named servers own reusable connection and authentication settings. Backend + configurations select independently which server and implementation to use. +*/ +class Workspace: + path/string + servers/Map + backends/Map + + constructor --.path --.servers --.backends: + + /** Loads an `artemis.yaml` from $root-or-path. */ + static load root-or-path/string -> Workspace: + path := root-or-path + if file.is-directory path: path = fs.join path ARTEMIS-FILE + if not file.is-file path: + workspace-error_ "Workspace '$root-or-path' does not contain an $ARTEMIS-FILE file." + + encoded := null + exception := catch: encoded = read-yaml path + if exception: + workspace-error_ "Workspace file '$path' is not valid YAML: $exception" + return from-map encoded --path=path + + /** Parses an encoded workspace configuration. */ + static from-map encoded/any --path/string=ARTEMIS-FILE -> Workspace: + if encoded is not Map: + workspace-error_ "Workspace file '$path' must contain a map." + + version := encoded.get "version" + if version != WORKSPACE-VERSION: + workspace-error_ "Workspace file '$path' has unsupported version '$version'." + + encoded-servers := encoded.get "servers" + if encoded-servers is not Map: + workspace-error_ "Workspace file '$path' must contain a 'servers' map." + servers := encoded-servers.map: | name encoded-server | + if name is not string or name.is-empty: + workspace-error_ "Workspace file '$path' contains an invalid server name." + if encoded-server is not Map: + workspace-error_ "Server '$name' must be a map." + if encoded-server.contains "scope": + workspace-error_ "Server '$name' cannot contain a fleet scope." + if encoded-server.contains "poll_interval" or encoded-server.contains "device_headers": + workspace-error_ "Server '$name' cannot contain embedded device configuration." + ServerConfig.from-json name encoded-server + --der-deserializer=: base64.decode it + + encoded-backends := encoded.get "backends" + if encoded-backends is not Map: + workspace-error_ "Workspace file '$path' must contain a 'backends' map." + backends := encoded-backends.map: | name encoded-backend | + if name is not string or name.is-empty: + workspace-error_ "Workspace file '$path' contains an invalid backend name." + BackendConfig.from-map name encoded-backend servers + + return Workspace --path=path --servers=servers --backends=backends + + /** Returns the configuration for the backend named $name. */ + backend name/string -> BackendConfig: + result := backends.get name + if not result: workspace-error_ "Workspace does not configure a '$name' backend." + return result + + /** Returns the configured fleet backend. */ + fleet -> BackendConfig: + return backend FLEET-BACKEND + + /** Returns the configured broker backend. */ + broker -> BackendConfig: + return backend BROKER-BACKEND + + /** Returns the configured pod backend. */ + pods -> BackendConfig: + return backend PODS-BACKEND + + /** Returns the configured artifact backend. */ + artifacts -> BackendConfig: + return backend ARTIFACTS-BACKEND + + /** Resolves a relative workspace path against the workspace directory. */ + resolve path/string -> string: + if fs.is-absolute path: return path + return fs.join (fs.dirname this.path) path + + /** Serializes this workspace configuration. */ + to-map -> Map: + encoded-servers := {:} + servers.keys.sort.do: | name/string | + server-config/ServerConfig := servers[name] + encoded := server-config.to-json --base64 --der-serializer=: unreachable + // Polling and device headers are part of embedded device configuration, + // not reusable CLI server connections. + encoded.remove "poll_interval" + encoded.remove "device_headers" + encoded.remove "scope" + encoded-servers[name] = encoded + + encoded-backends := {:} + backends.keys.sort.do: | name/string | + backend-config/BackendConfig := backends[name] + encoded-backends[name] = backend-config.to-map + + return { + "version": WORKSPACE-VERSION, + "servers": encoded-servers, + "backends": encoded-backends, + } + + /** Writes this workspace to its configured $path. */ + write -> none: + write-yaml-to-file path to-map diff --git a/src/shared/server-config.toit b/src/shared/server-config.toit index beff34a7..64b6ddad 100644 --- a/src/shared/server-config.toit +++ b/src/shared/server-config.toit @@ -134,11 +134,15 @@ class ServerConfigSupabase extends ServerConfig implements supabase.ServerConfig url = "$scheme://$(json["host"])" scope-value := json.get "scope" scope/Scope? := scope-value and (Scope scope-value) + poll-interval-value := json.get "poll_interval" + poll-interval := poll-interval-value + ? Duration --us=poll-interval-value + : BrokerConfig.DEFAULT-POLL-INTERVAL return ServerConfigSupabase name --url=url --anon=json["anon"] --root-certificate-ders=roots - --poll-interval=Duration --us=json["poll_interval"] + --poll-interval=poll-interval --scope=scope constructor name/string @@ -220,12 +224,16 @@ class ServerConfigHttp extends ServerConfig: url = "$scheme://$(config["host"])$port-suffix$(config["path"])" scope-value := config.get "scope" scope/Scope? := scope-value and (Scope scope-value) + poll-interval-value := config.get "poll_interval" + poll-interval := poll-interval-value + ? Duration --us=poll-interval-value + : BrokerConfig.DEFAULT-POLL-INTERVAL return ServerConfigHttp name --url=url --root-certificate-ders=roots --device-headers=config.get "device_headers" --admin-headers=config.get "admin_headers" - --poll-interval=Duration --us=config["poll_interval"] + --poll-interval=poll-interval --scope=scope constructor name/string diff --git a/tests/workspace-test.toit b/tests/workspace-test.toit new file mode 100644 index 00000000..d75084fe --- /dev/null +++ b/tests/workspace-test.toit @@ -0,0 +1,162 @@ +// Copyright (C) 2026 Toitware ApS. All rights reserved. + +import artemis.cli.workspace show + FileBackendConfig + HttpBackendConfig + Workspace + WorkspaceException +import artemis.shared.broker-config show BrokerConfig +import artemis.shared.server-config show ServerConfigSupabase +import expect show * +import host.directory +import host.file + +main: + test-server-indirection + test-round-trip + test-yaml-file + test-validation + +test-server-indirection: + workspace := Workspace.from-map { + "version": 1, + "servers": { + "production": { + "type": "supabase", + "url": "https://example.supabase.co/", + "anon": "anon-key", + }, + }, + "backends": { + "fleet": { + "type": "file", + "directory": "fleet", + }, + "broker": { + "type": "http", + "server": "production", + "endpoint": "/functions/v2/broker", + }, + "pods": { + "type": "http", + "server": "production", + "endpoint": "/functions/v2/pods", + }, + "artifacts": { + "type": "http", + "server": "production", + "endpoint": "/functions/v2/artifacts", + }, + }, + } --path="/work/artemis.yaml" + + fleet := workspace.fleet as FileBackendConfig + broker := workspace.broker as HttpBackendConfig + pods := workspace.pods as HttpBackendConfig + artifacts := workspace.artifacts as HttpBackendConfig + + expect-equals "/work/fleet" (workspace.resolve fleet.directory) + expect-identical broker.server-config pods.server-config + expect-identical broker.server-config artifacts.server-config + expect broker.server-config is ServerConfigSupabase + expect-equals "https://example.supabase.co" + (broker.server-config as ServerConfigSupabase).url + expect-equals BrokerConfig.DEFAULT-POLL-INTERVAL + (broker.server-config as ServerConfigSupabase).poll-interval + +test-round-trip: + encoded := { + "version": 1, + "servers": { + "local": { + "type": "toit-http", + "url": "http://localhost:4998", + "admin_headers": {"Authorization": "Bearer token"}, + }, + }, + "backends": { + "broker": { + "type": "http", + "server": "local", + "endpoint": "/broker", + }, + }, + } + + workspace := Workspace.from-map encoded + decoded := Workspace.from-map workspace.to-map + backend := decoded.broker as HttpBackendConfig + expect-equals "local" backend.server-name + expect-equals "/broker" backend.endpoint + server-map := backend.server-config.to-json + --base64 + --der-serializer=: unreachable + expect-equals "Bearer token" server-map["admin_headers"]["Authorization"] + +test-yaml-file: + tmp := directory.mkdtemp "/tmp/artemis-workspace-test-" + try: + path := "$tmp/artemis.yaml" + workspace := Workspace.from-map { + "version": 1, + "servers": {:}, + "backends": { + "fleet": { + "type": "file", + "directory": "fleet", + }, + }, + } --path=path + workspace.write + + expect (file.is-file path) + loaded := Workspace.load tmp + fleet := loaded.fleet as FileBackendConfig + expect-equals "$tmp/fleet" (loaded.resolve fleet.directory) + finally: + directory.rmdir --recursive tmp + +test-validation: + expect-workspace-error "HTTP backend 'broker' references unknown server 'missing'.": + Workspace.from-map { + "version": 1, + "servers": {:}, + "backends": { + "broker": { + "type": "http", + "server": "missing", + "endpoint": "/broker", + }, + }, + } + + expect-workspace-error "Server 'production' cannot contain a fleet scope.": + Workspace.from-map { + "version": 1, + "servers": { + "production": { + "type": "toit-http", + "url": "https://example.com", + "scope": "organization", + }, + }, + "backends": {:}, + } + + expect-workspace-error "Server 'production' cannot contain embedded device configuration.": + Workspace.from-map { + "version": 1, + "servers": { + "production": { + "type": "toit-http", + "url": "https://example.com", + "poll_interval": 20_000_000, + }, + }, + "backends": {:}, + } + +expect-workspace-error message/string [block]: + exception := catch: block.call + expect exception is WorkspaceException + expect-equals message (exception as WorkspaceException).message From fc35b10c8ed03c6d613643bb581ee8e887a27e11 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 28 Aug 2026 01:59:21 +0200 Subject: [PATCH 2/6] Address workspace review comments --- public/schemas/workspace/v1.json | 111 +++++++++++++++++++++++++++++++ src/cli/workspace.toit | 58 ++++++++-------- src/shared/server-config.toit | 29 ++++++-- tests/server-config-test.toit | 7 ++ tests/workspace-test.toit | 37 ++++++----- 5 files changed, 191 insertions(+), 51 deletions(-) create mode 100644 public/schemas/workspace/v1.json diff --git a/public/schemas/workspace/v1.json b/public/schemas/workspace/v1.json new file mode 100644 index 00000000..3e8feb67 --- /dev/null +++ b/public/schemas/workspace/v1.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://toit.io/schemas/artemis/workspace/v1.json", + "title": "Artemis workspace", + "description": "Configures reusable servers and backend strategies for an Artemis workspace.", + "type": "object", + "additionalProperties": false, + "required": [ + "$schema", + "servers", + "backends" + ], + "properties": { + "$schema": { + "const": "https://toit.io/schemas/artemis/workspace/v1.json" + }, + "servers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { "$ref": "#/$defs/SupabaseServer" }, + { "$ref": "#/$defs/HttpServer" } + ] + } + }, + "backends": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { "$ref": "#/$defs/FileBackend" }, + { "$ref": "#/$defs/HttpBackend" } + ] + } + } + }, + "$defs": { + "SupabaseServer": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "url", + "anon" + ], + "properties": { + "type": { "const": "supabase" }, + "url": { "type": "string", "format": "uri" }, + "anon": { "type": "string" }, + "root_certificate_ders64": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "HttpServer": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "url" + ], + "properties": { + "type": { "const": "toit-http" }, + "url": { "type": "string", "format": "uri" }, + "root_certificate_ders64": { + "type": "array", + "items": { "type": "string" } + }, + "admin_headers": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + }, + "FileBackend": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "directory" + ], + "properties": { + "type": { "const": "file" }, + "directory": { + "type": "string", + "minLength": 1 + } + } + }, + "HttpBackend": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "server", + "endpoint" + ], + "properties": { + "type": { "const": "http" }, + "server": { + "type": "string", + "minLength": 1 + }, + "endpoint": { + "type": "string", + "pattern": "^/" + } + } + } + } +} diff --git a/src/cli/workspace.toit b/src/cli/workspace.toit index 128c7833..6c49b696 100644 --- a/src/cli/workspace.toit +++ b/src/cli/workspace.toit @@ -8,7 +8,7 @@ import .server-config import .utils show read-yaml write-yaml-to-file ARTEMIS-FILE ::= "artemis.yaml" -WORKSPACE-VERSION ::= 1 +WORKSPACE-SCHEMA ::= "https://toit.io/schemas/artemis/workspace/v1.json" FLEET-BACKEND ::= "fleet" BROKER-BACKEND ::= "broker" @@ -39,24 +39,10 @@ abstract class BackendConfig: type := encoded.get "type" if type == "file": - directory := encoded.get "directory" - if directory is not string or directory.is-empty: - workspace-error_ "File backend '$name' must have a non-empty 'directory'." - return FileBackendConfig name --directory=directory + return FileBackendConfig.from-map name encoded if type == "http": - server-name := encoded.get "server" - if server-name is not string or server-name.is-empty: - workspace-error_ "HTTP backend '$name' must reference a server." - if not servers.contains server-name: - workspace-error_ "HTTP backend '$name' references unknown server '$server-name'." - - endpoint := encoded.get "endpoint" - if endpoint is not string or not endpoint.starts-with "/": - workspace-error_ "HTTP backend '$name' must have an absolute-path 'endpoint'." - return HttpBackendConfig name - --server-config=servers[server-name] - --endpoint=endpoint + return HttpBackendConfig.from-map name encoded servers workspace-error_ "Backend '$name' has unknown type '$type'." unreachable @@ -68,6 +54,12 @@ abstract class BackendConfig: class FileBackendConfig extends BackendConfig: directory/string + constructor.from-map name/string encoded/Map: + directory := encoded.get "directory" + if directory is not string or directory.is-empty: + workspace-error_ "File backend '$name' must have a non-empty 'directory'." + return FileBackendConfig name --directory=directory + constructor name/string --.directory: super.from-sub_ name @@ -82,6 +74,20 @@ class HttpBackendConfig extends BackendConfig: server-config/ServerConfig endpoint/string + constructor.from-map name/string encoded/Map servers/Map: + server-name := encoded.get "server" + if server-name is not string or server-name.is-empty: + workspace-error_ "HTTP backend '$name' must reference a server." + if not servers.contains server-name: + workspace-error_ "HTTP backend '$name' references unknown server '$server-name'." + + endpoint := encoded.get "endpoint" + if endpoint is not string or not endpoint.starts-with "/": + workspace-error_ "HTTP backend '$name' must have an absolute-path 'endpoint'." + return HttpBackendConfig name + --server-config=servers[server-name] + --endpoint=endpoint + constructor name/string --.server-config --.endpoint: super.from-sub_ name @@ -126,9 +132,9 @@ class Workspace: if encoded is not Map: workspace-error_ "Workspace file '$path' must contain a map." - version := encoded.get "version" - if version != WORKSPACE-VERSION: - workspace-error_ "Workspace file '$path' has unsupported version '$version'." + schema := encoded.get "\$schema" + if schema != WORKSPACE-SCHEMA: + workspace-error_ "Workspace file '$path' has unsupported schema '$schema'." encoded-servers := encoded.get "servers" if encoded-servers is not Map: @@ -187,13 +193,9 @@ class Workspace: encoded-servers := {:} servers.keys.sort.do: | name/string | server-config/ServerConfig := servers[name] - encoded := server-config.to-json --base64 --der-serializer=: unreachable - // Polling and device headers are part of embedded device configuration, - // not reusable CLI server connections. - encoded.remove "poll_interval" - encoded.remove "device_headers" - encoded.remove "scope" - encoded-servers[name] = encoded + encoded-servers[name] = server-config.to-workspace-json + --base64 + --der-serializer=: unreachable encoded-backends := {:} backends.keys.sort.do: | name/string | @@ -201,7 +203,7 @@ class Workspace: encoded-backends[name] = backend-config.to-map return { - "version": WORKSPACE-VERSION, + "\$schema": WORKSPACE-SCHEMA, "servers": encoded-servers, "backends": encoded-backends, } diff --git a/src/shared/server-config.toit b/src/shared/server-config.toit index 64b6ddad..03524ac5 100644 --- a/src/shared/server-config.toit +++ b/src/shared/server-config.toit @@ -63,6 +63,13 @@ abstract class ServerConfig: */ abstract to-json [--der-serializer] --base64/bool=false -> Map + /** + Serializes the reusable connection settings for an Artemis workspace. + + Device behavior and fleet scope are intentionally omitted. + */ + abstract to-workspace-json [--der-serializer] --base64/bool=false -> Map + /** Creates the broker configuration used by the Artemis service. */ @@ -162,16 +169,22 @@ class ServerConfigSupabase extends ServerConfig implements supabase.ServerConfig type -> string: return "supabase" to-json [--der-serializer] --base64/bool=false -> Map: + result := to-workspace-json + --der-serializer=der-serializer + --base64=base64 + result["poll_interval"] = poll-interval.in-us + if scope: result["scope"] = scope.to-json + return result + + to-workspace-json [--der-serializer] --base64/bool=false -> Map: result := { "type": type, "url": url, "anon": anon, - "poll_interval": poll-interval.in-us, } add-root-certificates_ result root-certificate-ders --der-serializer=der-serializer --base64=base64 - if scope: result["scope"] = scope.to-json return result to-broker-config -> BrokerConfig: @@ -253,17 +266,23 @@ class ServerConfigHttp extends ServerConfig: type -> string: return "toit-http" to-json [--der-serializer] --base64/bool=false -> Map: + result := to-workspace-json + --der-serializer=der-serializer + --base64=base64 + result["poll_interval"] = poll-interval.in-us + if device-headers: result["device_headers"] = device-headers + if scope: result["scope"] = scope.to-json + return result + + to-workspace-json [--der-serializer] --base64/bool=false -> Map: result := { "type": type, "url": url, - "poll_interval": poll-interval.in-us, } add-root-certificates_ result root-certificate-ders --der-serializer=der-serializer --base64=base64 - if device-headers: result["device_headers"] = device-headers if admin-headers: result["admin_headers"] = admin-headers - if scope: result["scope"] = scope.to-json return result to-broker-config -> BrokerConfig: diff --git a/tests/server-config-test.toit b/tests/server-config-test.toit index aafaff5d..7cc37d7d 100644 --- a/tests/server-config-test.toit +++ b/tests/server-config-test.toit @@ -21,6 +21,9 @@ test-supabase-templates: expect-equals "http://localhost:54321" cli-json["url"] expect-not (cli-json.contains "host") expect-not (cli-json.contains "use_tls") + workspace-json := config.to-workspace-json --base64 --der-serializer=: unreachable + expect-not (workspace-json.contains "poll_interval") + expect-not (workspace-json.contains "scope") encoded := config.to-service-json --base64 --der-serializer=: unreachable expect-not (encoded.contains "type") expect-equals @@ -57,6 +60,10 @@ test-http-templates: expect-not (cli-json.contains "port") expect-not (cli-json.contains "path") expect-not (cli-json.contains "use_tls") + workspace-json := config.to-workspace-json --base64 --der-serializer=: unreachable + expect-not (workspace-json.contains "poll_interval") + expect-not (workspace-json.contains "device_headers") + expect-not (workspace-json.contains "scope") encoded := config.to-service-json --base64 --der-serializer=: unreachable expect-equals "http://localhost:1234/api/device/{device-id}/goal" diff --git a/tests/workspace-test.toit b/tests/workspace-test.toit index d75084fe..ffb3dcf6 100644 --- a/tests/workspace-test.toit +++ b/tests/workspace-test.toit @@ -5,11 +5,11 @@ import artemis.cli.workspace show HttpBackendConfig Workspace WorkspaceException -import artemis.shared.broker-config show BrokerConfig + WORKSPACE-SCHEMA import artemis.shared.server-config show ServerConfigSupabase import expect show * -import host.directory import host.file +import .utils show with-tmp-directory main: test-server-indirection @@ -18,8 +18,8 @@ main: test-validation test-server-indirection: - workspace := Workspace.from-map { - "version": 1, + workspace := Workspace.from-map --path="/work/artemis.yaml" { + "\$schema": WORKSPACE-SCHEMA, "servers": { "production": { "type": "supabase", @@ -48,7 +48,7 @@ test-server-indirection: "endpoint": "/functions/v2/artifacts", }, }, - } --path="/work/artemis.yaml" + } fleet := workspace.fleet as FileBackendConfig broker := workspace.broker as HttpBackendConfig @@ -61,12 +61,10 @@ test-server-indirection: expect broker.server-config is ServerConfigSupabase expect-equals "https://example.supabase.co" (broker.server-config as ServerConfigSupabase).url - expect-equals BrokerConfig.DEFAULT-POLL-INTERVAL - (broker.server-config as ServerConfigSupabase).poll-interval test-round-trip: encoded := { - "version": 1, + "\$schema": WORKSPACE-SCHEMA, "servers": { "local": { "type": "toit-http", @@ -94,11 +92,10 @@ test-round-trip: expect-equals "Bearer token" server-map["admin_headers"]["Authorization"] test-yaml-file: - tmp := directory.mkdtemp "/tmp/artemis-workspace-test-" - try: + with-tmp-directory: | tmp/string | path := "$tmp/artemis.yaml" - workspace := Workspace.from-map { - "version": 1, + workspace := Workspace.from-map --path=path { + "\$schema": WORKSPACE-SCHEMA, "servers": {:}, "backends": { "fleet": { @@ -106,20 +103,24 @@ test-yaml-file: "directory": "fleet", }, }, - } --path=path + } workspace.write expect (file.is-file path) loaded := Workspace.load tmp fleet := loaded.fleet as FileBackendConfig expect-equals "$tmp/fleet" (loaded.resolve fleet.directory) - finally: - directory.rmdir --recursive tmp test-validation: + expect-workspace-error "Workspace file 'artemis.yaml' has unsupported schema 'null'.": + Workspace.from-map { + "servers": {:}, + "backends": {:}, + } + expect-workspace-error "HTTP backend 'broker' references unknown server 'missing'.": Workspace.from-map { - "version": 1, + "\$schema": WORKSPACE-SCHEMA, "servers": {:}, "backends": { "broker": { @@ -132,7 +133,7 @@ test-validation: expect-workspace-error "Server 'production' cannot contain a fleet scope.": Workspace.from-map { - "version": 1, + "\$schema": WORKSPACE-SCHEMA, "servers": { "production": { "type": "toit-http", @@ -145,7 +146,7 @@ test-validation: expect-workspace-error "Server 'production' cannot contain embedded device configuration.": Workspace.from-map { - "version": 1, + "\$schema": WORKSPACE-SCHEMA, "servers": { "production": { "type": "toit-http", From bf2969a76d2260bdb91d12dc3deee01e0ab7be02 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 28 Aug 2026 02:22:51 +0200 Subject: [PATCH 3/6] Use host temporary-directory helper --- tests/package.lock | 4 ++-- tests/package.yaml | 2 +- tests/workspace-test.toit | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/package.lock b/tests/package.lock index 7d890064..8c527d69 100644 --- a/tests/package.lock +++ b/tests/package.lock @@ -56,9 +56,9 @@ packages: url: github.com/toitlang/pkg-fs version: 2.3.1 github.com/toitlang/pkg-host-1: - hash: ae83f761db80166a20bb38498edd009916b72563 + hash: d7a30b86c73cf8d9cd5dd0211031c76b68706738 url: github.com/toitlang/pkg-host - version: 1.16.2 + version: 1.19.0 github.com/toitlang/pkg-http-2: hash: dbe7effdb578273b940b2cd789ecd0ca0a588d85 url: github.com/toitlang/pkg-http diff --git a/tests/package.yaml b/tests/package.yaml index f636f88a..95c6c4e0 100644 --- a/tests/package.yaml +++ b/tests/package.yaml @@ -17,7 +17,7 @@ dependencies: version: ^2.3.1 host: url: github.com/toitlang/pkg-host - version: ^1.16.2 + version: ^1.19.0 http: url: github.com/toitlang/pkg-http version: ^2.15.0 diff --git a/tests/workspace-test.toit b/tests/workspace-test.toit index ffb3dcf6..6401359c 100644 --- a/tests/workspace-test.toit +++ b/tests/workspace-test.toit @@ -8,8 +8,8 @@ import artemis.cli.workspace show WORKSPACE-SCHEMA import artemis.shared.server-config show ServerConfigSupabase import expect show * +import host import host.file -import .utils show with-tmp-directory main: test-server-indirection @@ -92,7 +92,7 @@ test-round-trip: expect-equals "Bearer token" server-map["admin_headers"]["Authorization"] test-yaml-file: - with-tmp-directory: | tmp/string | + host.with-tmp-directory: | tmp/string | path := "$tmp/artemis.yaml" workspace := Workspace.from-map --path=path { "\$schema": WORKSPACE-SCHEMA, From e64e9a11faa40c9a0542ba508606fc9dc3307f58 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 28 Aug 2026 02:33:07 +0200 Subject: [PATCH 4/6] Preserve CLI config serialization order --- src/shared/server-config.toit | 18 ++++++++++++++---- tests/server-config-test.toit | 8 +++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/shared/server-config.toit b/src/shared/server-config.toit index 03524ac5..f2fa0595 100644 --- a/src/shared/server-config.toit +++ b/src/shared/server-config.toit @@ -169,10 +169,15 @@ class ServerConfigSupabase extends ServerConfig implements supabase.ServerConfig type -> string: return "supabase" to-json [--der-serializer] --base64/bool=false -> Map: - result := to-workspace-json + result := { + "type": type, + "url": url, + "anon": anon, + "poll_interval": poll-interval.in-us, + } + add-root-certificates_ result root-certificate-ders --der-serializer=der-serializer --base64=base64 - result["poll_interval"] = poll-interval.in-us if scope: result["scope"] = scope.to-json return result @@ -266,11 +271,16 @@ class ServerConfigHttp extends ServerConfig: type -> string: return "toit-http" to-json [--der-serializer] --base64/bool=false -> Map: - result := to-workspace-json + result := { + "type": type, + "url": url, + "poll_interval": poll-interval.in-us, + } + add-root-certificates_ result root-certificate-ders --der-serializer=der-serializer --base64=base64 - result["poll_interval"] = poll-interval.in-us if device-headers: result["device_headers"] = device-headers + if admin-headers: result["admin_headers"] = admin-headers if scope: result["scope"] = scope.to-json return result diff --git a/tests/server-config-test.toit b/tests/server-config-test.toit index 7cc37d7d..670663ef 100644 --- a/tests/server-config-test.toit +++ b/tests/server-config-test.toit @@ -18,10 +18,12 @@ test-supabase-templates: --url="http://localhost:54321/" --anon="anon" cli-json := config.to-json --base64 --der-serializer=: unreachable + expect-equals ["type", "url", "anon", "poll_interval"] cli-json.keys expect-equals "http://localhost:54321" cli-json["url"] expect-not (cli-json.contains "host") expect-not (cli-json.contains "use_tls") workspace-json := config.to-workspace-json --base64 --der-serializer=: unreachable + expect-equals ["type", "url", "anon"] workspace-json.keys expect-not (workspace-json.contains "poll_interval") expect-not (workspace-json.contains "scope") encoded := config.to-service-json --base64 --der-serializer=: unreachable @@ -53,14 +55,18 @@ test-http-templates: --url="http://localhost:1234/api/" --root-certificate-ders=null --device-headers={"X-Device": "true"} - --admin-headers=null + --admin-headers={"X-Admin": "true"} cli-json := config.to-json --base64 --der-serializer=: unreachable + expect-equals + ["type", "url", "poll_interval", "device_headers", "admin_headers"] + cli-json.keys expect-equals "http://localhost:1234/api" cli-json["url"] expect-not (cli-json.contains "host") expect-not (cli-json.contains "port") expect-not (cli-json.contains "path") expect-not (cli-json.contains "use_tls") workspace-json := config.to-workspace-json --base64 --der-serializer=: unreachable + expect-equals ["type", "url", "admin_headers"] workspace-json.keys expect-not (workspace-json.contains "poll_interval") expect-not (workspace-json.contains "device_headers") expect-not (workspace-json.contains "scope") From f0b08a73b4eb240952ef426e0dd1a1dce6cc265d Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 28 Aug 2026 02:38:38 +0200 Subject: [PATCH 5/6] Make workspace path test platform-independent --- tests/workspace-test.toit | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/workspace-test.toit b/tests/workspace-test.toit index 6401359c..7f2c578f 100644 --- a/tests/workspace-test.toit +++ b/tests/workspace-test.toit @@ -8,6 +8,7 @@ import artemis.cli.workspace show WORKSPACE-SCHEMA import artemis.shared.server-config show ServerConfigSupabase import expect show * +import fs import host import host.file @@ -55,7 +56,8 @@ test-server-indirection: pods := workspace.pods as HttpBackendConfig artifacts := workspace.artifacts as HttpBackendConfig - expect-equals "/work/fleet" (workspace.resolve fleet.directory) + expected-fleet-path := fs.join (fs.dirname workspace.path) fleet.directory + expect-equals expected-fleet-path (workspace.resolve fleet.directory) expect-identical broker.server-config pods.server-config expect-identical broker.server-config artifacts.server-config expect broker.server-config is ServerConfigSupabase From 58dad09cf6d1f92b1018758acd5259ba9a6b9da0 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 28 Aug 2026 02:46:48 +0200 Subject: [PATCH 6/6] Use platform path join in workspace file test --- tests/workspace-test.toit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/workspace-test.toit b/tests/workspace-test.toit index 7f2c578f..7738e677 100644 --- a/tests/workspace-test.toit +++ b/tests/workspace-test.toit @@ -111,7 +111,7 @@ test-yaml-file: expect (file.is-file path) loaded := Workspace.load tmp fleet := loaded.fleet as FileBackendConfig - expect-equals "$tmp/fleet" (loaded.resolve fleet.directory) + expect-equals (fs.join tmp "fleet") (loaded.resolve fleet.directory) test-validation: expect-workspace-error "Workspace file 'artemis.yaml' has unsupported schema 'null'.":