diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayProtocolGeneratedModelsTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayProtocolGeneratedModelsTests.swift new file mode 100644 index 000000000000..0b662155e8b6 --- /dev/null +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayProtocolGeneratedModelsTests.swift @@ -0,0 +1,71 @@ +import Foundation +import OpenClawProtocol +import Testing + +struct GatewayProtocolGeneratedModelsTests { + @Test + func `generated frames decode legacy minimums and additive fields`() throws { + let request = try JSONDecoder().decode( + GatewayFrame.self, + from: Data(#"{"type":"req","id":"old-req","method":"health"}"#.utf8)) + let additiveRequest = try JSONDecoder().decode( + GatewayFrame.self, + from: Data( + #"{"type":"req","id":"new-req","method":"health","traceparent":"00-0123456789abcdef0123456789abcdef-0123456789abcdef-01","futureField":true}"# + .utf8)) + let response = try JSONDecoder().decode( + GatewayFrame.self, + from: Data(#"{"type":"res","id":"old-req","ok":true}"#.utf8)) + let additiveResponse = try JSONDecoder().decode( + GatewayFrame.self, + from: Data( + #"{"type":"res","id":"new-req","ok":true,"payload":{"healthy":true},"futureField":true}"#.utf8)) + let event = try JSONDecoder().decode( + GatewayFrame.self, + from: Data(#"{"type":"event","event":"tick"}"#.utf8)) + let additiveEvent = try JSONDecoder().decode( + GatewayFrame.self, + from: Data( + #"{"type":"event","event":"tick","payload":{"ts":1},"seq":2,"futureField":true}"#.utf8)) + + guard case let .req(oldRequest) = request, + case let .req(newRequest) = additiveRequest, + case let .res(oldResponse) = response, + case let .res(newResponse) = additiveResponse, + case let .event(oldEvent) = event, + case let .event(newEvent) = additiveEvent + else { + Issue.record("Expected generated request, response, and event frame cases") + return + } + + #expect(oldRequest.params == nil) + #expect(oldRequest.traceparent == nil) + #expect(newRequest.traceparent == "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01") + #expect(oldResponse.payload == nil) + #expect(newResponse.payload != nil) + #expect(oldEvent.seq == nil) + #expect(newEvent.seq == 2) + } + + @Test + func `generated connect model decodes old and additive handshakes`() throws { + let legacy = try JSONDecoder().decode( + ConnectParams.self, + from: Data( + #"{"minProtocol":4,"maxProtocol":4,"client":{"id":"test","version":"old","platform":"ios","mode":"test"}}"# + .utf8)) + let additive = try JSONDecoder().decode( + ConnectParams.self, + from: Data( + #"{"minProtocol":4,"maxProtocol":4,"client":{"id":"test","version":"new","platform":"ios","mode":"test","futureClientField":true},"role":"operator","scopes":["operator.read"],"locale":"en-US","futureField":true}"# + .utf8)) + + #expect(legacy.role == nil) + #expect(legacy.scopes == nil) + #expect(legacy.locale == nil) + #expect(additive.role == "operator") + #expect(additive.scopes == ["operator.read"]) + #expect(additive.locale == "en-US") + } +} diff --git a/qa/scenarios/runtime/gateway-protocol-artifacts.yaml b/qa/scenarios/runtime/gateway-protocol-artifacts.yaml new file mode 100644 index 000000000000..f50535e7ee72 --- /dev/null +++ b/qa/scenarios/runtime/gateway-protocol-artifacts.yaml @@ -0,0 +1,38 @@ +title: Gateway published protocol artifacts + +scenario: + id: gateway-protocol-artifacts + surface: runtime + category: gateway.protocol-compatibility + coverage: + primary: + - gateway.published-protocol-schema + - gateway.json-schema-export + - gateway.swift-client-models + - gateway.backward-compatible-evolution + objective: Verify the published Gateway protocol package and generated Swift models stay aligned with the canonical protocol registry and additive compatibility contract. + successCriteria: + - The real `@openclaw/gateway-protocol` package build is packed and unpacked before inspection. + - The unpacked package imports its built validators and schema registry successfully. + - The shipped `protocol.schema.json` exactly matches the canonical TypeBox registry and includes connect, request, response, and event definitions. + - The canonical protocol drift checks, including generated Swift model drift, pass. + - Generated Swift models compile and decode legacy-minimal frames plus additive optional and unknown fields. + docsRefs: + - docs/gateway/protocol.md + - docs/concepts/qa-e2e-automation.md + - docs/reference/test.md + codeRefs: + - packages/gateway-protocol/package.json + - packages/gateway-protocol/src/schema/protocol-schemas.ts + - scripts/protocol-gen.ts + - scripts/protocol-gen-swift.ts + - test/e2e/qa-lab/runtime/gateway-protocol-artifacts.ts + - apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayProtocolGeneratedModelsTests.swift + execution: + kind: script + path: test/e2e/qa-lab/runtime/gateway-protocol-artifacts.ts + summary: Packs and imports the real Gateway protocol npm artifact, checks canonical JSON Schema and generated-model drift, then runs targeted Swift compatibility decoding. + timeoutMs: 1800000 + args: + - --artifact-base + - ${outputDir} diff --git a/test/e2e/qa-lab/runtime/gateway-protocol-artifacts.test.ts b/test/e2e/qa-lab/runtime/gateway-protocol-artifacts.test.ts new file mode 100644 index 000000000000..679c4766c240 --- /dev/null +++ b/test/e2e/qa-lab/runtime/gateway-protocol-artifacts.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from "vitest"; +import { + assertPublishedProtocolSchema, + buildCanonicalProtocolSchema, + buildInstalledProtocolInspectionScript, + buildPortableSwiftAnyCodableSource, + buildSwiftProtocolCompatibilityHarness, + parseGatewayProtocolArtifactOptions, +} from "./gateway-protocol-artifacts.js"; + +describe("Gateway protocol artifact producer", () => { + it("parses one artifact directory", () => { + expect( + parseGatewayProtocolArtifactOptions(["--artifact-base", ".artifacts/protocol"], "/repo"), + ).toEqual({ + artifactBase: "/repo/.artifacts/protocol", + repoRoot: "/repo", + }); + expect(() => parseGatewayProtocolArtifactOptions([])).toThrow("--artifact-base is required"); + expect(() => + parseGatewayProtocolArtifactOptions(["--artifact-base", "one", "--artifact-base", "two"]), + ).toThrow("--artifact-base was provided more than once"); + }); + + it("builds the same schema envelope as the protocol generator", () => { + const request = { type: "object", properties: { type: { const: "req" } } }; + const canonical = buildCanonicalProtocolSchema( + { + ConnectParams: { type: "object" }, + EventFrame: { type: "object" }, + RequestFrame: request, + ResponseFrame: { type: "object" }, + }, + [{ name: "health", scope: "operator.read", since: "2026.7" }], + ); + + expect(canonical.definitions.RequestFrame).toEqual(request); + expect(canonical.methods.health).toEqual({ scope: "operator.read", since: "2026.7" }); + expect(canonical.discriminator.mapping.req).toBe("#/definitions/RequestFrame"); + }); + + it("requires the published schema and built registry to match canonical definitions", () => { + const definitions = { + ConnectParams: { type: "object" }, + EventFrame: { type: "object" }, + RequestFrame: { type: "object" }, + ResponseFrame: { type: "object" }, + }; + const canonical = buildCanonicalProtocolSchema(definitions, []); + + expect(() => + assertPublishedProtocolSchema({ + builtSchemas: definitions, + canonical, + published: canonical, + }), + ).not.toThrow(); + expect(() => + assertPublishedProtocolSchema({ + builtSchemas: definitions, + canonical, + published: { + ...canonical, + definitions: { + ...definitions, + EventFrame: { type: "string" }, + }, + }, + }), + ).toThrow("published protocol.schema.json differs"); + }); + + it("loads installed artifacts through their public package specifiers", () => { + const script = buildInstalledProtocolInspectionScript(); + + expect(script).toContain('from "@openclaw/gateway-protocol"'); + expect(script).toContain('from "@openclaw/gateway-protocol/schema"'); + expect(script).not.toContain("dist/index.mjs"); + expect(script).not.toContain("dist/schema.mjs"); + }); + + it("builds a standalone Swift compatibility harness for the generated model artifact", () => { + const harness = buildSwiftProtocolCompatibilityHarness(); + + expect(harness).toContain("@main"); + expect(harness).toContain("GatewayFrame.self"); + expect(harness).toContain("ConnectParams.self"); + expect(harness).toContain('"futureField":true'); + }); + + it("makes the AnyCodable dependency portable for standalone Linux compilation", () => { + expect(buildPortableSwiftAnyCodableSource("import Foundation\n")).toBe( + "import CoreFoundation\nimport Foundation\n", + ); + expect(buildPortableSwiftAnyCodableSource("import CoreFoundation\n")).toBe( + "import CoreFoundation\n", + ); + }); +}); diff --git a/test/e2e/qa-lab/runtime/gateway-protocol-artifacts.ts b/test/e2e/qa-lab/runtime/gateway-protocol-artifacts.ts new file mode 100644 index 000000000000..4bab72e93a9b --- /dev/null +++ b/test/e2e/qa-lab/runtime/gateway-protocol-artifacts.ts @@ -0,0 +1,601 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { x as extractTar } from "tar"; +import { + QA_EVIDENCE_FILENAME, + type QaEvidenceSummaryJson, +} from "../../../../extensions/qa-lab/api.js"; +import { ProtocolSchemas } from "../../../../packages/gateway-protocol/src/schema/protocol-schemas.js"; +import { listCoreGatewayMethodMetadata } from "../../../../src/gateway/methods/core-descriptors.js"; +import { createQaScriptEvidenceWriter } from "./script-evidence.js"; + +const SOURCE_PATH = "test/e2e/qa-lab/runtime/gateway-protocol-artifacts.ts"; +const REQUIRED_DEFINITIONS = [ + "ConnectParams", + "RequestFrame", + "ResponseFrame", + "EventFrame", +] as const; + +type ProducerOptions = { + artifactBase: string; + repoRoot: string; +}; + +type ProtocolSchemaDocument = { + definitions: Record; + discriminator: { + mapping: Record; + propertyName: string; + }; + methods: Record; + oneOf: Array<{ $ref: string }>; +}; + +type ProtocolArtifactSummary = { + consumer: { + installed: boolean; + packageSpecifier: string; + schemaSpecifier: string; + }; + definitions: string[]; + package: { + name: string; + sha256: string; + tarball: string; + version: string; + }; + validators: { + connect: boolean; + request: boolean; + }; +}; + +type InstalledProtocolInspection = { + schemas: Record; + validators: { + connect: { + errors: unknown[] | null; + valid: boolean; + }; + request: { + errors: unknown[] | null; + valid: boolean; + }; + }; +}; + +export function buildInstalledProtocolInspectionScript() { + return `import fs from "node:fs/promises"; +import { + validateConnectParams, + validateRequestFrame, +} from "@openclaw/gateway-protocol"; +import { ProtocolSchemas } from "@openclaw/gateway-protocol/schema"; + +const requestValid = validateRequestFrame({ + type: "req", + id: "artifact-request", + method: "health", +}); +const connectValid = validateConnectParams({ + minProtocol: 4, + maxProtocol: 4, + client: { + id: "test", + version: "artifact-proof", + platform: process.platform, + mode: "test", + }, +}); +await fs.writeFile( + process.argv[2], + JSON.stringify({ + schemas: ProtocolSchemas, + validators: { + connect: { + errors: validateConnectParams.errors, + valid: connectValid, + }, + request: { + errors: validateRequestFrame.errors, + valid: requestValid, + }, + }, + }), + "utf8", +); +`; +} + +export function buildSwiftProtocolCompatibilityHarness() { + return `import Foundation + +enum GatewayProtocolArtifactError: Error { + case unexpected(String) +} + +@main +struct GatewayProtocolArtifactHarness { + static func decode(_ type: T.Type, _ json: String) throws -> T { + try JSONDecoder().decode(type, from: Data(json.utf8)) + } + + static func main() throws { + let request = try decode( + GatewayFrame.self, + #"{"type":"req","id":"old-req","method":"health"}"#) + let additiveRequest = try decode( + GatewayFrame.self, + #"{"type":"req","id":"new-req","method":"health","traceparent":"00-0123456789abcdef0123456789abcdef-0123456789abcdef-01","futureField":true}"#) + let response = try decode( + GatewayFrame.self, + #"{"type":"res","id":"old-req","ok":true}"#) + let additiveResponse = try decode( + GatewayFrame.self, + #"{"type":"res","id":"new-req","ok":true,"payload":{"healthy":true},"futureField":true}"#) + let event = try decode( + GatewayFrame.self, + #"{"type":"event","event":"tick"}"#) + let additiveEvent = try decode( + GatewayFrame.self, + #"{"type":"event","event":"tick","payload":{"ts":1},"seq":2,"futureField":true}"#) + let legacyConnect = try decode( + ConnectParams.self, + #"{"minProtocol":4,"maxProtocol":4,"client":{"id":"test","version":"old","platform":"ios","mode":"test"}}"#) + let additiveConnect = try decode( + ConnectParams.self, + #"{"minProtocol":4,"maxProtocol":4,"client":{"id":"test","version":"new","platform":"ios","mode":"test","futureClientField":true},"role":"operator","scopes":["operator.read"],"locale":"en-US","futureField":true}"#) + + guard case let .req(oldRequest) = request, + case let .req(newRequest) = additiveRequest, + case let .res(oldResponse) = response, + case let .res(newResponse) = additiveResponse, + case let .event(oldEvent) = event, + case let .event(newEvent) = additiveEvent + else { + throw GatewayProtocolArtifactError.unexpected("frame discriminator") + } + guard oldRequest.params == nil, + oldRequest.traceparent == nil, + newRequest.traceparent == "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", + oldResponse.payload == nil, + newResponse.payload != nil, + oldEvent.seq == nil, + newEvent.seq == 2, + legacyConnect.role == nil, + legacyConnect.scopes == nil, + legacyConnect.locale == nil, + additiveConnect.role == "operator", + additiveConnect.scopes == ["operator.read"], + additiveConnect.locale == "en-US" + else { + throw GatewayProtocolArtifactError.unexpected("legacy or additive decode") + } + print("gateway Swift generated-model compatibility passed") + } +} +`; +} + +export function buildPortableSwiftAnyCodableSource(source: string) { + return source.includes("import CoreFoundation") ? source : `import CoreFoundation\n${source}`; +} + +function formatErrorMessage(error: unknown) { + return error instanceof Error ? error.message : String(error); +} + +export function parseGatewayProtocolArtifactOptions( + args: readonly string[], + cwd = process.cwd(), +): ProducerOptions { + let artifactBase: string | undefined; + for (let index = 0; index < args.length; index += 1) { + const option = args[index]; + const value = args[index + 1]; + if (option !== "--artifact-base") { + throw new Error(`unknown argument: ${option}`); + } + if (!value || value.startsWith("--")) { + throw new Error("--artifact-base requires a value"); + } + if (artifactBase) { + throw new Error("--artifact-base was provided more than once"); + } + artifactBase = value; + index += 1; + } + if (!artifactBase) { + throw new Error("--artifact-base is required"); + } + return { + artifactBase: path.resolve(cwd, artifactBase), + repoRoot: path.resolve(cwd), + }; +} + +export function buildCanonicalProtocolSchema( + schemas: Record = ProtocolSchemas, + methodMetadata = listCoreGatewayMethodMetadata(), +): ProtocolSchemaDocument { + return JSON.parse( + JSON.stringify({ + $schema: "http://json-schema.org/draft-07/schema#", + $id: "https://openclaw.ai/protocol.schema.json", + title: "OpenClaw Gateway Protocol", + description: "Handshake, request/response, and event frames for the Gateway WebSocket.", + oneOf: [ + { $ref: "#/definitions/RequestFrame" }, + { $ref: "#/definitions/ResponseFrame" }, + { $ref: "#/definitions/EventFrame" }, + ], + discriminator: { + propertyName: "type", + mapping: { + req: "#/definitions/RequestFrame", + res: "#/definitions/ResponseFrame", + event: "#/definitions/EventFrame", + }, + }, + methods: Object.fromEntries( + methodMetadata.map(({ name, scope, since }) => [name, { since, scope }]), + ), + definitions: schemas, + }), + ) as ProtocolSchemaDocument; +} + +export function assertPublishedProtocolSchema(params: { + builtSchemas: Record; + canonical: ProtocolSchemaDocument; + published: ProtocolSchemaDocument; +}) { + assert.deepEqual( + JSON.parse(JSON.stringify(params.builtSchemas)), + params.canonical.definitions, + "built package schema registry differs from the canonical TypeBox registry", + ); + assert.deepEqual( + params.published, + params.canonical, + "published protocol.schema.json differs from the canonical TypeBox registry", + ); + for (const definition of REQUIRED_DEFINITIONS) { + assert.ok( + Object.hasOwn(params.published.definitions, definition), + `published protocol schema is missing ${definition}`, + ); + } + assert.deepEqual( + params.published.oneOf.map((entry) => entry.$ref), + ["#/definitions/RequestFrame", "#/definitions/ResponseFrame", "#/definitions/EventFrame"], + ); +} + +async function runCommand(params: { + args: string[]; + appendLog: (chunk: unknown) => void; + command: string; + cwd: string; +}) { + await new Promise((resolve, reject) => { + const child = spawn(params.command, params.args, { + cwd: params.cwd, + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + child.on("error", reject); + child.stdout.on("data", (chunk: Buffer) => { + process.stdout.write(chunk); + params.appendLog(chunk); + }); + child.stderr.on("data", (chunk: Buffer) => { + process.stderr.write(chunk); + params.appendLog(chunk); + }); + child.on("exit", (code, signal) => { + if (code === 0 && !signal) { + resolve(); + return; + } + reject( + new Error( + `${params.command} ${params.args.join(" ")} failed: code=${String(code)} signal=${String(signal)}`, + ), + ); + }); + }); +} + +async function withPreservedPackageOutputs(repoRoot: string, run: () => Promise): Promise { + const packageRoot = path.join(repoRoot, "packages", "gateway-protocol"); + const targets = [path.join(packageRoot, "dist"), path.join(packageRoot, "protocol.schema.json")]; + const backupRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-protocol-pack-")); + const backups: Array<{ backup: string; target: string }> = []; + try { + for (const [index, target] of targets.entries()) { + try { + await fs.access(target); + } catch { + continue; + } + const backup = path.join(backupRoot, String(index)); + await fs.rename(target, backup); + backups.push({ backup, target }); + } + return await run(); + } finally { + for (const target of targets) { + await fs.rm(target, { force: true, recursive: true }); + } + for (const { backup, target } of backups) { + await fs.rename(backup, target); + } + await fs.rm(backupRoot, { force: true, recursive: true }); + } +} + +async function sha256(filePath: string) { + const contents = await fs.readFile(filePath); + return createHash("sha256").update(contents).digest("hex"); +} + +async function packAndInspectProtocol(params: { + appendLog: (chunk: unknown) => void; + artifactBase: string; + repoRoot: string; +}): Promise { + const packageRoot = path.join(params.repoRoot, "packages", "gateway-protocol"); + const packDir = path.join(params.artifactBase, "package"); + const extractDir = path.join(params.artifactBase, "unpacked"); + await fs.rm(packDir, { force: true, recursive: true }); + await fs.rm(extractDir, { force: true, recursive: true }); + await fs.mkdir(packDir, { recursive: true }); + await fs.mkdir(extractDir, { recursive: true }); + + await withPreservedPackageOutputs(params.repoRoot, async () => { + await runCommand({ + args: ["--dir", packageRoot, "pack", "--pack-destination", packDir], + appendLog: params.appendLog, + command: "pnpm", + cwd: params.repoRoot, + }); + }); + + const tarballs = (await fs.readdir(packDir)).filter((entry) => entry.endsWith(".tgz")); + assert.equal( + tarballs.length, + 1, + `expected one packed protocol tarball, found ${tarballs.length}`, + ); + const tarball = tarballs[0]!; + const tarballPath = path.join(packDir, tarball); + await extractTar({ cwd: extractDir, file: tarballPath }); + + const unpackedRoot = path.join(extractDir, "package"); + const packageJson = JSON.parse( + await fs.readFile(path.join(unpackedRoot, "package.json"), "utf8"), + ) as { name?: string; version?: string }; + assert.equal(packageJson.name, "@openclaw/gateway-protocol"); + assert.ok(packageJson.version, "packed protocol package version is missing"); + + const published = JSON.parse( + await fs.readFile(path.join(unpackedRoot, "protocol.schema.json"), "utf8"), + ) as ProtocolSchemaDocument; + const consumerRoot = await fs.mkdtemp( + path.join(os.tmpdir(), "openclaw-gateway-protocol-consumer-"), + ); + const consumerInspectionPath = path.join(consumerRoot, "inspection.json"); + const inspection = await (async () => { + try { + await fs.writeFile( + path.join(consumerRoot, "package.json"), + `${JSON.stringify({ + name: "openclaw-gateway-protocol-artifact-consumer", + private: true, + type: "module", + })}\n`, + "utf8", + ); + await fs.writeFile( + path.join(consumerRoot, "inspect.mjs"), + buildInstalledProtocolInspectionScript(), + "utf8", + ); + await runCommand({ + args: ["--dir", consumerRoot, "add", "--ignore-scripts", "--lockfile=false", tarballPath], + appendLog: params.appendLog, + command: "pnpm", + cwd: params.repoRoot, + }); + await runCommand({ + args: ["inspect.mjs", consumerInspectionPath], + appendLog: params.appendLog, + command: "node", + cwd: consumerRoot, + }); + return JSON.parse( + await fs.readFile(consumerInspectionPath, "utf8"), + ) as InstalledProtocolInspection; + } finally { + await fs.rm(consumerRoot, { force: true, recursive: true }); + } + })(); + const canonical = buildCanonicalProtocolSchema(); + assertPublishedProtocolSchema({ + builtSchemas: inspection.schemas, + canonical, + published, + }); + + assert.ok( + inspection.validators.request.valid, + `installed request validator rejected a minimal frame: ${JSON.stringify(inspection.validators.request.errors)}`, + ); + assert.ok( + inspection.validators.connect.valid, + `installed connect validator rejected a minimal payload: ${JSON.stringify(inspection.validators.connect.errors)}`, + ); + + return { + consumer: { + installed: true, + packageSpecifier: "@openclaw/gateway-protocol", + schemaSpecifier: "@openclaw/gateway-protocol/schema", + }, + definitions: REQUIRED_DEFINITIONS.filter((definition) => + Object.hasOwn(published.definitions, definition), + ), + package: { + name: packageJson.name, + sha256: await sha256(tarballPath), + tarball, + version: packageJson.version, + }, + validators: { + connect: inspection.validators.connect.valid, + request: inspection.validators.request.valid, + }, + }; +} + +async function compileAndRunSwiftProtocolModels(params: { + appendLog: (chunk: unknown) => void; + artifactBase: string; + repoRoot: string; +}) { + const swiftArtifactDir = path.join(params.artifactBase, "swift"); + const anyCodablePath = path.join(swiftArtifactDir, "AnyCodable.swift"); + const harnessPath = path.join(swiftArtifactDir, "GatewayProtocolArtifactHarness.swift"); + const executablePath = path.join(swiftArtifactDir, "gateway-protocol-artifact-harness"); + await fs.rm(swiftArtifactDir, { force: true, recursive: true }); + await fs.mkdir(swiftArtifactDir, { recursive: true }); + const anyCodableSource = await fs.readFile( + path.join(params.repoRoot, "apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift"), + "utf8", + ); + // Linux Foundation does not re-export the CoreFoundation type-ID helpers. + await fs.writeFile(anyCodablePath, buildPortableSwiftAnyCodableSource(anyCodableSource), "utf8"); + await fs.writeFile(harnessPath, buildSwiftProtocolCompatibilityHarness(), "utf8"); + await runCommand({ + args: [ + anyCodablePath, + path.join( + params.repoRoot, + "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift", + ), + harnessPath, + "-o", + executablePath, + ], + appendLog: params.appendLog, + command: "swiftc", + cwd: params.repoRoot, + }); + await runCommand({ + args: [], + appendLog: params.appendLog, + command: executablePath, + cwd: params.repoRoot, + }); +} + +async function runGatewayProtocolArtifactsProducer( + options: ProducerOptions, +): Promise { + const writer = createQaScriptEvidenceWriter({ + artifactBase: options.artifactBase, + logFileName: "gateway-protocol-artifacts.log", + primaryModel: "gateway/protocol-artifacts", + providerMode: "mock-openai", + repoRoot: options.repoRoot, + target: { + codeRefs: [ + SOURCE_PATH, + "packages/gateway-protocol/package.json", + "packages/gateway-protocol/src/schema/protocol-schemas.ts", + "scripts/protocol-gen.ts", + "scripts/protocol-gen-swift.ts", + "apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayProtocolGeneratedModelsTests.swift", + ], + docsRefs: ["docs/gateway/protocol.md", "docs/reference/test.md"], + id: "gateway-protocol-artifacts", + sourcePath: SOURCE_PATH, + title: "Gateway published protocol artifacts", + }, + }); + const startedAt = Date.now(); + try { + await runCommand({ + args: ["protocol:check"], + appendLog: writer.appendLog, + command: "pnpm", + cwd: options.repoRoot, + }); + const summary = await packAndInspectProtocol({ + appendLog: writer.appendLog, + artifactBase: options.artifactBase, + repoRoot: options.repoRoot, + }); + await compileAndRunSwiftProtocolModels({ + appendLog: writer.appendLog, + artifactBase: options.artifactBase, + repoRoot: options.repoRoot, + }); + const summaryPath = path.join(options.artifactBase, "protocol-artifact-summary.json"); + await fs.writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8"); + return await writer.write({ + artifacts: [ + { kind: "package", filePath: path.join("package", summary.package.tarball) }, + { kind: "schema", filePath: path.join("unpacked", "package", "protocol.schema.json") }, + { kind: "summary", filePath: "protocol-artifact-summary.json" }, + ], + details: [ + `package=${summary.package.name}@${summary.package.version}`, + `sha256=${summary.package.sha256}`, + `consumer-install=${summary.consumer.installed ? "pass" : "fail"}`, + `definitions=${summary.definitions.join(",")}`, + "protocol-check=pass", + "swift-generated-model-compile=pass", + "swift-generated-model-decode=pass", + ].join("; "), + durationMs: Math.max(1, Date.now() - startedAt), + status: "pass", + }); + } catch (error) { + const details = formatErrorMessage(error); + writer.appendLog(`\nfail: ${details}\n`); + return await writer.write({ + details, + durationMs: Math.max(1, Date.now() - startedAt), + status: "fail", + }); + } +} + +async function main(argv: readonly string[]) { + const evidence = await runGatewayProtocolArtifactsProducer( + parseGatewayProtocolArtifactOptions(argv), + ); + const status = evidence.entries[0]?.result.status; + console.log(`Gateway protocol artifact evidence: ${QA_EVIDENCE_FILENAME}`); + console.log(`Gateway protocol artifact status: ${status}`); + return status === "pass" ? 0 : 1; +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + main(process.argv.slice(2)) + .then((exitCode) => { + process.exitCode = exitCode; + }) + .catch((error: unknown) => { + console.error(formatErrorMessage(error)); + process.exitCode = 1; + }); +}