264 lines
12 KiB
TypeScript
264 lines
12 KiB
TypeScript
import { afterEach, expect, test } from "bun:test";
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync, mkdtempSync, mkdirSync, readFileSync, 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-postprocess.mjs");
|
|
|
|
afterEach(() => {
|
|
while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true });
|
|
});
|
|
|
|
test("patches YAML-owned Code Agent and Kafka runtime settings", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "runtime-gitops-postprocess-"));
|
|
roots.push(root);
|
|
const runtimeDir = join(root, "runtime");
|
|
mkdirSync(runtimeDir);
|
|
writeFileSync(join(root, "overlay.json"), JSON.stringify(overlay("runtime")));
|
|
writeFileSync(join(runtimeDir, "cloud-api.yaml"), deploymentYaml("hwlab-cloud-api"));
|
|
writeFileSync(join(runtimeDir, "cloud-web.yaml"), `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: unchanged\n---\n${deploymentYaml("hwlab-cloud-web")}`);
|
|
writeFileSync(join(runtimeDir, "list.yaml"), `apiVersion: v1\nkind: List\nitems:\n${listItem(deploymentYaml("hwlab-cloud-api"))}\n`);
|
|
writeFileSync(join(runtimeDir, "json-workload.yaml"), `${JSON.stringify(Bun.YAML.parse(deploymentYaml("hwlab-cloud-api")), null, 2)}\n`);
|
|
|
|
const result = spawnSync(process.execPath, [script], {
|
|
cwd: root,
|
|
env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") },
|
|
encoding: "utf8",
|
|
});
|
|
|
|
if (result.status !== 0) throw new Error(result.stderr);
|
|
expect(result.stderr).toContain('"codeAgentRuntimeChanged":true');
|
|
|
|
const api = Bun.YAML.parse(readFileSync(join(runtimeDir, "cloud-api.yaml"), "utf8")) as any;
|
|
expect(envValues(api)).toMatchObject({
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v02",
|
|
AGENTRUN_MGR_URL: "http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080",
|
|
HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE: "agentrun-v02",
|
|
HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE: "agentrun-v02",
|
|
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-v02-tool-github-pr",
|
|
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-v02-tool-unidesk-ssh",
|
|
HWLAB_KAFKA_ENABLED: "true",
|
|
HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED: "true",
|
|
HWLAB_KAFKA_BOOTSTRAP_SERVERS: "kafka.platform-infra.svc.cluster.local:9092",
|
|
HWLAB_KAFKA_STDIO_TOPIC: "agentrun.stdio.v1",
|
|
HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC: "agentrun.event.v1",
|
|
HWLAB_KAFKA_EVENT_TOPIC: "hwlab.event.v1",
|
|
HWLAB_KAFKA_CLIENT_ID: "hwlab-v03",
|
|
HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "true",
|
|
HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "false",
|
|
HWLAB_KAFKA_PROJECTOR_GROUP_ID: "hwlab-projector",
|
|
HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID: "hwlab-events",
|
|
HWLAB_WORKBENCH_EMPTY_SESSION_GC_ENABLED: "false",
|
|
HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED: "false",
|
|
HWLAB_WORKBENCH_KAFKA_REFRESH_INDEX_ENABLED: "true",
|
|
HWLAB_WORKBENCH_KAFKA_REFRESH_INDEX_MAX_EVENTS: "100000",
|
|
HWLAB_WORKBENCH_KAFKA_REFRESH_INDEX_MAX_EVENTS_PER_SESSION: "10000",
|
|
HWLAB_WORKBENCH_KAFKA_REFRESH_INDEX_BOOTSTRAP_TIMEOUT_MS: "30000",
|
|
HWLAB_WORKBENCH_KAFKA_REFRESH_INDEX_REBUILD_INTERVAL_MS: "300000",
|
|
HWLAB_WORKBENCH_KAFKA_REFRESH_INDEX_REBUILD_COOLDOWN_MS: "30000",
|
|
});
|
|
expect(envSecret(api, "AGENTRUN_API_KEY")).toEqual({ name: "hwlab-v03-master-server-admin-api-key", key: "api-key" });
|
|
|
|
const multiDocs = readFileSync(join(runtimeDir, "cloud-web.yaml"), "utf8").split(/^---$/mu).map((document) => Bun.YAML.parse(document) as any);
|
|
expect(multiDocs[0].kind).toBe("ConfigMap");
|
|
expect(envValues(multiDocs[1])).toMatchObject({
|
|
HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "true",
|
|
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "true",
|
|
HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "false",
|
|
HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED: "false",
|
|
HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_ENABLED: "false",
|
|
HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_ENTRIES: "1",
|
|
HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_RETAINED_BYTES: "1",
|
|
HWLAB_WORKBENCH_TRACE_AUTO_EXPAND_RUNNING: "true",
|
|
HWLAB_WORKBENCH_TRACE_AUTO_COLLAPSE_TERMINAL: "true",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_ZONE: "Asia/Shanghai",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LOCALE: "zh-CN",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LABEL: "北京时间",
|
|
});
|
|
|
|
const list = Bun.YAML.parse(readFileSync(join(runtimeDir, "list.yaml"), "utf8")) as any;
|
|
expect(list.kind).toBe("List");
|
|
expect(envValues(list.items[0]).AGENTRUN_MGR_URL).toBe("http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080");
|
|
|
|
const jsonText = readFileSync(join(runtimeDir, "json-workload.yaml"), "utf8");
|
|
expect(jsonText.startsWith("{\n")).toBe(true);
|
|
expect(envValues(JSON.parse(jsonText)).HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE).toBe("agentrun-v02");
|
|
|
|
const afterFirstRun = snapshot(runtimeDir);
|
|
const second = spawnSync(process.execPath, [script], {
|
|
cwd: root,
|
|
env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") },
|
|
encoding: "utf8",
|
|
});
|
|
if (second.status !== 0) throw new Error(second.stderr);
|
|
expect(second.stderr).toContain('"codeAgentRuntimeChanged":false');
|
|
expect(snapshot(runtimeDir)).toEqual(afterFirstRun);
|
|
});
|
|
|
|
test("replaces retired FRPC resources with the YAML-owned NodePort service", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "runtime-gitops-postprocess-node-port-"));
|
|
roots.push(root);
|
|
const runtimeDir = join(root, "runtime");
|
|
mkdirSync(runtimeDir);
|
|
writeFileSync(join(root, "overlay.json"), JSON.stringify({
|
|
runtimePath: "runtime",
|
|
runtimeNamespace: "hwlab-v03",
|
|
environment: "development",
|
|
publicExposure: {
|
|
enabled: true,
|
|
mode: "node-port-shared-edge",
|
|
serviceName: "hwlab-cloud-web-public",
|
|
selector: { "app.kubernetes.io/name": "hwlab-cloud-web" },
|
|
port: 80,
|
|
targetPort: 8080,
|
|
nodePort: 32009,
|
|
},
|
|
}));
|
|
writeFileSync(join(runtimeDir, "node-frpc.yaml"), "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: hwlab-v03-frpc\n");
|
|
writeFileSync(join(runtimeDir, "workloads.yaml"), deploymentYaml("hwlab-cloud-web"));
|
|
writeFileSync(join(runtimeDir, "kustomization.yaml"), "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n - workloads.yaml\n - node-frpc.yaml\n");
|
|
|
|
const first = run(root);
|
|
expect(first.stderr).toContain('"frpcRemoved":true');
|
|
expect(existsSync(join(runtimeDir, "node-frpc.yaml"))).toBe(false);
|
|
const service = Bun.YAML.parse(readFileSync(join(runtimeDir, "node-public-service.yaml"), "utf8")) as any;
|
|
expect(service).toMatchObject({
|
|
kind: "Service",
|
|
metadata: { name: "hwlab-cloud-web-public", namespace: "hwlab-v03" },
|
|
spec: { type: "NodePort", ports: [{ port: 80, targetPort: 8080, nodePort: 32009 }] },
|
|
});
|
|
const kustomization = Bun.YAML.parse(readFileSync(join(runtimeDir, "kustomization.yaml"), "utf8")) as any;
|
|
expect(kustomization.resources).toEqual(["workloads.yaml", "node-public-service.yaml"]);
|
|
|
|
const afterFirstRun = snapshotFiles(runtimeDir, ["workloads.yaml", "node-public-service.yaml", "kustomization.yaml"]);
|
|
const second = run(root);
|
|
expect(second.stderr).toContain('"changed":false');
|
|
expect(snapshotFiles(runtimeDir, ["workloads.yaml", "node-public-service.yaml", "kustomization.yaml"])).toEqual(afterFirstRun);
|
|
});
|
|
|
|
test("removes every retired public exposure resource when exposure is disabled", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "runtime-gitops-postprocess-no-public-"));
|
|
roots.push(root);
|
|
const runtimeDir = join(root, "runtime");
|
|
mkdirSync(runtimeDir);
|
|
writeFileSync(join(root, "overlay.json"), JSON.stringify({
|
|
runtimePath: "runtime",
|
|
publicExposure: { enabled: false },
|
|
}));
|
|
writeFileSync(join(runtimeDir, "node-frpc.yaml"), "kind: Deployment\n");
|
|
writeFileSync(join(runtimeDir, "node-public-service.yaml"), "kind: Service\n");
|
|
writeFileSync(join(runtimeDir, "kustomization.yaml"), "resources:\n - node-frpc.yaml\n - node-public-service.yaml\n");
|
|
|
|
run(root);
|
|
|
|
expect(existsSync(join(runtimeDir, "node-frpc.yaml"))).toBe(false);
|
|
expect(existsSync(join(runtimeDir, "node-public-service.yaml"))).toBe(false);
|
|
const kustomization = Bun.YAML.parse(readFileSync(join(runtimeDir, "kustomization.yaml"), "utf8")) as any;
|
|
expect(kustomization.resources).toEqual([]);
|
|
});
|
|
|
|
function run(root: string) {
|
|
const result = spawnSync(process.execPath, [script], {
|
|
cwd: root,
|
|
env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") },
|
|
encoding: "utf8",
|
|
});
|
|
if (result.status !== 0) throw new Error(result.stderr);
|
|
return result;
|
|
}
|
|
|
|
function overlay(runtimePath: string) {
|
|
return {
|
|
runtimePath,
|
|
codeAgentRuntime: {
|
|
enabled: true,
|
|
adapter: "agentrun-v02",
|
|
managerUrl: "http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080",
|
|
apiKeySecretName: "hwlab-v03-master-server-admin-api-key",
|
|
apiKeySecretKey: "api-key",
|
|
runnerNamespace: "agentrun-v02",
|
|
secretNamespace: "agentrun-v02",
|
|
toolSecretNames: {
|
|
githubPr: "agentrun-v02-tool-github-pr",
|
|
unideskSsh: "agentrun-v02-tool-unidesk-ssh",
|
|
},
|
|
defaultProviderProfile: "gpt.pika",
|
|
codexStdioSupervisor: "repo-owned",
|
|
workbench: {
|
|
emptySessionGc: { enabled: false },
|
|
webRuntimeConfig: {
|
|
displayTime: { timeZone: "Asia/Shanghai", locale: "zh-CN", label: "北京时间" },
|
|
workbench: {
|
|
debugCapabilities: {
|
|
isolatedKafka: false,
|
|
rawHwlabEventWindow: { enabled: false, maxEntries: 1, maxRetainedBytes: 1 },
|
|
},
|
|
traceTimeline: { autoExpandRunning: true, autoCollapseTerminal: true },
|
|
},
|
|
},
|
|
},
|
|
kafkaEventBridge: {
|
|
enabled: true,
|
|
bootstrapServers: "kafka.platform-infra.svc.cluster.local:9092",
|
|
stdioTopic: "agentrun.stdio.v1",
|
|
agentRunEventTopic: "agentrun.event.v1",
|
|
hwlabEventTopic: "hwlab.event.v1",
|
|
clientId: "hwlab-v03",
|
|
features: {
|
|
directPublish: true,
|
|
liveKafkaSse: true,
|
|
kafkaRefreshReplay: true,
|
|
transactionalProjector: false,
|
|
projectionOutboxRelay: false,
|
|
projectionRealtime: false,
|
|
},
|
|
directPublishConsumerGroupId: "hwlab-direct",
|
|
transactionalProjectorConsumerGroupId: "hwlab-projector",
|
|
hwlabEventConsumerGroupId: "hwlab-events",
|
|
refreshReplay: {
|
|
groupIdPrefix: "hwlab-refresh",
|
|
timeoutMs: 30000,
|
|
scanLimit: 1000,
|
|
matchedEventLimit: 100,
|
|
liveBufferLimit: 100,
|
|
sessionIndex: {
|
|
enabled: true,
|
|
maxEvents: 100000,
|
|
maxEventsPerSession: 10000,
|
|
bootstrapTimeoutMs: 30000,
|
|
rebuildIntervalMs: 300000,
|
|
rebuildCooldownMs: 30000,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function deploymentYaml(name: string) {
|
|
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 - name: EXISTING\n value: preserved\n`;
|
|
}
|
|
|
|
function listItem(value: string) {
|
|
const [first, ...rest] = value.trimEnd().split("\n");
|
|
return [` - ${first}`, ...rest.map((line) => ` ${line}`)].join("\n");
|
|
}
|
|
|
|
function envValues(workload: any) {
|
|
return Object.fromEntries(workload.spec.template.spec.containers[0].env.map((item: any) => [item.name, item.value]));
|
|
}
|
|
|
|
function envSecret(workload: any, name: string) {
|
|
return workload.spec.template.spec.containers[0].env.find((item: any) => item.name === name)?.valueFrom?.secretKeyRef;
|
|
}
|
|
|
|
function snapshot(runtimeDir: string) {
|
|
return ["cloud-api.yaml", "cloud-web.yaml", "list.yaml", "json-workload.yaml"].map((name) => readFileSync(join(runtimeDir, name), "utf8"));
|
|
}
|
|
|
|
function snapshotFiles(runtimeDir: string, names: string[]) {
|
|
return names.map((name) => readFileSync(join(runtimeDir, name), "utf8"));
|
|
}
|