From 66e3c8a8485541d79fed5622320d9e3088fc23d5 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 02:25:06 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99=20runtime=20GitOps=20?= =?UTF-8?q?codeAgentRuntime=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime-gitops-scripts-configmap.mjs | 1 + .../runtime-gitops-scripts-configmap.test.ts | 46 ++++++++ .../native/hwlab/runtime-gitops-verify.mjs | 83 +++++++++++++ .../hwlab/runtime-gitops-verify.test.ts | 111 ++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 scripts/native/hwlab/runtime-gitops-scripts-configmap.test.ts create mode 100644 scripts/native/hwlab/runtime-gitops-verify.test.ts diff --git a/scripts/native/hwlab/runtime-gitops-scripts-configmap.mjs b/scripts/native/hwlab/runtime-gitops-scripts-configmap.mjs index 36c67d91..b46a8925 100644 --- a/scripts/native/hwlab/runtime-gitops-scripts-configmap.mjs +++ b/scripts/native/hwlab/runtime-gitops-scripts-configmap.mjs @@ -6,6 +6,7 @@ export function runtimeGitopsScriptsConfigMap({ overlay, scriptsDir, namespace, "runtime-gitops-overlay.json": `${JSON.stringify({ runtimePath: requiredString(overlay.runtimePath, "overlay.runtimePath"), observability: objectOr(overlay.observability), + codeAgentRuntime: objectOr(overlay.codeAgentRuntime), })}\n`, }; for (const name of ["runtime-gitops-observability.mjs", "runtime-gitops-postprocess.mjs", "runtime-gitops-verify.mjs"]) { diff --git a/scripts/native/hwlab/runtime-gitops-scripts-configmap.test.ts b/scripts/native/hwlab/runtime-gitops-scripts-configmap.test.ts new file mode 100644 index 00000000..0dc8b141 --- /dev/null +++ b/scripts/native/hwlab/runtime-gitops-scripts-configmap.test.ts @@ -0,0 +1,46 @@ +import { afterEach, expect, test } from "bun:test"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { runtimeGitopsScriptsConfigMap } from "./runtime-gitops-scripts-configmap.mjs"; + +const roots: string[] = []; + +afterEach(() => { + while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true }); +}); + +test("preserves codeAgentRuntime in the owning runtime GitOps overlay", () => { + const scriptsDir = mkdtempSync(join(tmpdir(), "runtime-gitops-configmap-")); + roots.push(scriptsDir); + for (const name of ["runtime-gitops-observability.mjs", "runtime-gitops-postprocess.mjs", "runtime-gitops-verify.mjs"]) { + writeFileSync(join(scriptsDir, name), `${name}\n`); + } + const codeAgentRuntime = { + enabled: true, + kafkaEventBridge: { + enabled: true, + features: { + directPublish: false, + liveKafkaSse: false, + kafkaRefreshReplay: false, + transactionalProjector: true, + projectionOutboxRelay: true, + projectionRealtime: true, + }, + }, + }; + + const configMap = runtimeGitopsScriptsConfigMap({ + overlay: { runtimePath: "runtime", observability: { prometheusOperator: false }, codeAgentRuntime }, + scriptsDir, + namespace: "hwlab", + configMapName: "runtime-gitops-scripts", + }); + + expect(JSON.parse(configMap.data["runtime-gitops-overlay.json"])).toEqual({ + runtimePath: "runtime", + observability: { prometheusOperator: false }, + codeAgentRuntime, + }); +}); diff --git a/scripts/native/hwlab/runtime-gitops-verify.mjs b/scripts/native/hwlab/runtime-gitops-verify.mjs index 97235a90..5acdc3c8 100644 --- a/scripts/native/hwlab/runtime-gitops-verify.mjs +++ b/scripts/native/hwlab/runtime-gitops-verify.mjs @@ -2,8 +2,12 @@ // SPEC: PJ2026-01060703 CI/CD branch follower runtime GitOps verify. // Responsibility: fail publish before Argo when rendered runtime GitOps violates UniDesk overlay gates. import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; +const requireFromScript = createRequire(import.meta.url); +const requireFromCwd = createRequire(path.join(process.cwd(), "package.json")); +const YAML = requireYaml(); const repoDir = process.cwd(); const overlay = readOverlay(); const runtimePath = requiredOverlayString("runtimePath"); @@ -20,6 +24,11 @@ if (overlay?.observability?.prometheusOperator === false) { const refs = findPrometheusOperatorResources(); if (refs.length > 0) fail("prometheus-operator-resource-present", { runtimePath, refs: refs.slice(0, 12), refCount: refs.length }); } +if (overlay?.codeAgentRuntime?.enabled && overlay.codeAgentRuntime.kafkaEventBridge?.enabled) { + checks.push("code-agent-runtime-kafka-event-bridge-enabled"); + verifyTransactionalProjectionAuthority(overlay.codeAgentRuntime.kafkaEventBridge); + verifyCodeAgentRuntimeWorkloads(); +} console.error(JSON.stringify({ event: "unidesk-runtime-gitops-verify", ok: true, runtimePath, checks })); @@ -34,6 +43,63 @@ function findPrometheusOperatorResources() { return refs; } +function verifyTransactionalProjectionAuthority(kafka) { + const expected = { + directPublish: false, + liveKafkaSse: false, + kafkaRefreshReplay: false, + transactionalProjector: true, + projectionOutboxRelay: true, + projectionRealtime: true, + }; + const actual = Object.fromEntries(Object.keys(expected).map((name) => [name, kafka.features?.[name]])); + const invalid = Object.entries(expected).filter(([name, value]) => actual[name] !== value).map(([name]) => name); + if (invalid.length > 0) fail("kafka-event-bridge-authority-invalid", { invalid, expected, actual }); +} + +function verifyCodeAgentRuntimeWorkloads() { + const expectedByWorkload = { + "hwlab-cloud-api": { + HWLAB_KAFKA_ENABLED: "true", + HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED: "true", + HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "false", + HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false", + HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false", + HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "true", + HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED: "true", + HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "true", + }, + "hwlab-cloud-web": { + HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false", + HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false", + HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "true", + }, + }; + const found = new Set(); + for (const file of listYamlFiles(runtimeDir)) { + const rel = path.relative(repoDir, file); + for (const document of parseStructuredDocuments(readFileSync(file, "utf8"), file)) { + const items = isKubernetesList(document) ? document.items : [document]; + for (const item of items) { + const workloadName = String(item?.metadata?.labels?.["app.kubernetes.io/name"] ?? item?.metadata?.name ?? ""); + const expected = expectedByWorkload[workloadName]; + if (!expected) continue; + const container = item?.spec?.template?.spec?.containers?.find((candidate) => candidate?.name === workloadName); + if (!container) fail("code-agent-runtime-container-missing", { file: rel, workload: workloadName, container: workloadName }); + if (!Array.isArray(container.env)) fail("code-agent-runtime-env-missing", { file: rel, workload: workloadName, container: workloadName }); + const actual = Object.fromEntries(container.env.filter((item) => typeof item?.name === "string").map((item) => [item.name, item.value])); + const invalid = Object.entries(expected) + .filter(([name, value]) => actual[name] !== value) + .map(([name, value]) => ({ name, expected: value, actual: actual[name] ?? null })); + if (invalid.length > 0) fail("code-agent-runtime-capability-env-invalid", { file: rel, workload: workloadName, container: workloadName, invalid }); + found.add(workloadName); + } + } + } + const missing = Object.keys(expectedByWorkload).filter((workloadName) => !found.has(workloadName)); + if (missing.length > 0) fail("code-agent-runtime-workload-missing", { runtimePath, missing }); +} + function splitYamlDocuments(text) { return text.split(/^---[ \t]*(?:#.*)?$/mu).map((doc) => doc.trim()).filter(Boolean); } @@ -87,6 +153,15 @@ function parseJsonDocument(text) { } } +function parseStructuredDocuments(text, file) { + const json = parseJsonDocument(text); + if (json !== null) return [json]; + const documents = YAML.parseAllDocuments(text); + const errors = documents.flatMap((document) => document.errors); + if (errors.length > 0) fail("runtime-yaml-invalid", { file: path.relative(repoDir, file), errors: errors.map((error) => error.message) }); + return documents.map((document) => document.toJS()).filter((document) => document !== null); +} + function listYamlFiles(root) { const out = []; for (const name of readdirSync(root)) { @@ -109,6 +184,14 @@ function readOverlay() { return JSON.parse(Buffer.from(encoded, "base64").toString("utf8")); } +function requireYaml() { + try { + return requireFromScript("yaml"); + } catch { + return requireFromCwd("yaml"); + } +} + function requiredOverlayString(name) { const value = overlay[name]; if (typeof value !== "string" || value.length === 0) throw new Error(`overlay.${name} is required`); diff --git a/scripts/native/hwlab/runtime-gitops-verify.test.ts b/scripts/native/hwlab/runtime-gitops-verify.test.ts new file mode 100644 index 00000000..bbdf2b75 --- /dev/null +++ b/scripts/native/hwlab/runtime-gitops-verify.test.ts @@ -0,0 +1,111 @@ +import { afterEach, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const roots: string[] = []; +const script = resolve(import.meta.dir, "runtime-gitops-verify.mjs"); + +afterEach(() => { + while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true }); +}); + +test("accepts the complete transactional projection capability env", () => { + const fixture = createFixture(); + const result = runVerify(fixture.root); + expect(result.status).toBe(0); + expect(result.stderr).toContain('"code-agent-runtime-kafka-event-bridge-enabled"'); +}); + +test("fails closed when a required workload capability env is missing", () => { + const fixture = createFixture({ omitWebEnv: "HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED" }); + const result = runVerify(fixture.root); + expect(result.status).toBe(48); + expect(result.stderr).toContain('"reason":"code-agent-runtime-capability-env-invalid"'); + expect(result.stderr).toContain('"name":"HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED"'); +}); + +test("fails closed when a required workload was not matched", () => { + const fixture = createFixture({ omitWebWorkload: true }); + const result = runVerify(fixture.root); + expect(result.status).toBe(48); + expect(result.stderr).toContain('"reason":"code-agent-runtime-workload-missing"'); + expect(result.stderr).toContain('"hwlab-cloud-web"'); +}); + +test("fails closed when the transactional projection authority is incomplete", () => { + const fixture = createFixture({ featureOverrides: { projectionOutboxRelay: false } }); + const result = runVerify(fixture.root); + expect(result.status).toBe(48); + expect(result.stderr).toContain('"reason":"kafka-event-bridge-authority-invalid"'); + expect(result.stderr).toContain('"projectionOutboxRelay"'); +}); + +function createFixture(options: { omitWebEnv?: string; omitWebWorkload?: boolean; featureOverrides?: Record } = {}) { + const root = mkdtempSync(join(tmpdir(), "runtime-gitops-verify-")); + roots.push(root); + const runtimeDir = join(root, "runtime"); + mkdirSync(runtimeDir); + writeFileSync(join(root, "overlay.json"), JSON.stringify(overlay(options.featureOverrides))); + writeFileSync(join(runtimeDir, "cloud-api.yaml"), deploymentYaml("hwlab-cloud-api", cloudApiEnv())); + if (!options.omitWebWorkload) { + writeFileSync(join(runtimeDir, "cloud-web.yaml"), deploymentYaml("hwlab-cloud-web", cloudWebEnv().filter(([name]) => name !== options.omitWebEnv))); + } + return { root }; +} + +function runVerify(root: string) { + return spawnSync(process.execPath, [script], { + cwd: root, + env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") }, + encoding: "utf8", + }); +} + +function overlay(featureOverrides: Record = {}) { + return { + runtimePath: "runtime", + codeAgentRuntime: { + enabled: true, + kafkaEventBridge: { + enabled: true, + features: { + directPublish: false, + liveKafkaSse: false, + kafkaRefreshReplay: false, + transactionalProjector: true, + projectionOutboxRelay: true, + projectionRealtime: true, + ...featureOverrides, + }, + }, + }, + }; +} + +function cloudApiEnv(): Array<[string, string]> { + return [ + ["HWLAB_KAFKA_ENABLED", "true"], + ["HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED", "true"], + ["HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED", "false"], + ["HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", "false"], + ["HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", "false"], + ["HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED", "true"], + ["HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED", "true"], + ["HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", "true"], + ]; +} + +function cloudWebEnv(): Array<[string, string]> { + return [ + ["HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", "false"], + ["HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", "false"], + ["HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", "true"], + ]; +} + +function deploymentYaml(name: string, env: Array<[string, string]>) { + const envYaml = env.map(([envName, value]) => ` - name: ${envName}\n value: "${value}"`).join("\n"); + return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: ${name}\n labels:\n app.kubernetes.io/name: ${name}\nspec:\n template:\n spec:\n containers:\n - name: ${name}\n env:\n${envYaml}\n`; +}