Files
pikasTech-unidesk/scripts/claudeqq-artifact-event-contract-test.ts
T
2026-05-23 19:22:07 +08:00

144 lines
8.4 KiB
TypeScript

import { readFileSync } from "node:fs";
import { rootPath } from "./src/config";
import { runArtifactRegistryCommand } from "./src/artifact-registry";
const serviceId = "claudeqq";
const desiredCommit = "203b1f46684c91340ecbbd8a74502bd55e4f2011";
const sourceRepo = "https://gitee.com/lyon1998/agent_skills";
const dockerfile = "claudeqq/Dockerfile";
const eventPaths = ["/api/events/recent", "/api/events/subscriptions"];
type JsonRecord = Record<string, unknown>;
function assertCondition(condition: boolean, message: string): void {
if (!condition) throw new Error(message);
}
function asRecord(value: unknown, path: string): JsonRecord {
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${path} must be an object`);
return value as JsonRecord;
}
function asArray(value: unknown, path: string): unknown[] {
assertCondition(Array.isArray(value), `${path} must be an array`);
return value as unknown[];
}
function stringField(value: unknown, path: string): string {
assertCondition(typeof value === "string" && value.length > 0, `${path} must be a non-empty string`);
return value as string;
}
function serviceById(environment: JsonRecord, id: string, path: string): JsonRecord {
const services = asArray(environment.services, `${path}.services`).map((item, index) => asRecord(item, `${path}.services[${index}]`));
const service = services.find((item) => item.id === id);
assertCondition(service !== undefined, `${path}.services must include ${id}`);
return service!;
}
function assertDeployJson(): void {
const deploy = asRecord(JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")), "deploy.json");
const environments = asRecord(deploy.environments, "deploy.json.environments");
for (const environment of ["dev", "prod"] as const) {
const service = serviceById(asRecord(environments[environment], `deploy.json.environments.${environment}`), serviceId, `deploy.json.environments.${environment}`);
assertCondition(service.repo === sourceRepo, `${environment} deploy.json claudeqq repo must match source repo`);
assertCondition(service.commitId === desiredCommit, `${environment} deploy.json claudeqq commit must match desired commit`);
}
}
function assertArtifactCatalog(): void {
const catalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")), "CI.json");
const artifacts = asArray(catalog.artifacts, "CI.json.artifacts").map((item, index) => asRecord(item, `CI.json.artifacts[${index}]`));
const artifact = artifacts.find((item) => item.serviceId === serviceId);
assertCondition(artifact !== undefined, "CI.json must include claudeqq artifact producer");
assertCondition(artifact!.kind === "source-build", "claudeqq artifact must be source-build");
assertCondition(artifact!.status === "supported", "claudeqq artifact must be supported");
assertCondition(artifact!.producer === "ci publish-user-service", "claudeqq artifact producer must be ci publish-user-service");
const source = asRecord(artifact!.source, "CI.json claudeqq source");
assertCondition(source.repo === sourceRepo, "claudeqq artifact source repo must match agent_skills");
assertCondition(source.dockerfile === dockerfile, "claudeqq artifact dockerfile must be claudeqq/Dockerfile");
const image = asRecord(artifact!.image, "CI.json claudeqq image");
assertCondition(image.repository === "unidesk/claudeqq", "claudeqq artifact image repository must be unidesk/claudeqq");
}
function assertAdapterSourceContract(): void {
const adapter = readFileSync(rootPath("src/components/microservices/claudeqq/adapter.js"), "utf8");
const imageDockerfile = readFileSync(rootPath("src/components/microservices/claudeqq/Dockerfile"), "utf8");
for (const eventPath of eventPaths) {
assertCondition(adapter.includes(eventPath), `adapter must expose ${eventPath}`);
}
assertCondition(adapter.includes("claudeqq-event-api-v1"), "adapter health metadata must name the event API contract");
assertCondition(adapter.includes("adapter-readonly-fallback"), "adapter must include read-only event fallback");
assertCondition(adapter.includes("CLAUDEQQ_HOST: upstreamHost"), "adapter must bind the upstream server to the private upstream host");
assertCondition(adapter.includes("CLAUDEQQ_PORT: String(upstreamPort)"), "adapter must bind the upstream server to the private upstream port");
assertCondition(adapter.includes("POST /api/events/subscriptions"), "adapter health metadata must document event subscription mutation paths");
assertCondition(imageDockerfile.includes('CMD ["node", "unidesk-adapter.cjs"]'), "claudeqq image must start the UniDesk adapter by default");
}
async function assertDryRun(environment: "dev" | "prod"): Promise<void> {
const plan = asRecord(await runArtifactRegistryCommand([
"deploy-service",
"--env",
environment,
"--service",
serviceId,
"--commit",
desiredCommit,
"--dry-run",
]), `artifact dry-run ${environment}`);
assertCondition(plan.ok === true, `${environment} dry-run must be ok`);
assertCondition(plan.supported === true, `${environment} dry-run must be supported`);
assertCondition(plan.dryRun === true, `${environment} dry-run must report dryRun=true`);
assertCondition(plan.mutation === false, `${environment} dry-run must not mutate`);
assertCondition(plan.serviceId === serviceId, `${environment} dry-run serviceId must be claudeqq`);
assertCondition(plan.commit === desiredCommit, `${environment} dry-run commit must match desired commit`);
assertCondition(plan.sourceRepo === sourceRepo, `${environment} dry-run source repo must match`);
const source = asRecord(plan.source, `${environment}.source`);
assertCondition(source.repo === sourceRepo, `${environment} dry-run source.repo must match`);
assertCondition(source.commit === desiredCommit, `${environment} dry-run source.commit must match`);
assertCondition(source.dockerfile === dockerfile, `${environment} dry-run source.dockerfile must match`);
const build = asRecord(plan.build, `${environment}.build`);
assertCondition(build.willCompile === false, `${environment} dry-run must not compile on the runtime target`);
assertCondition(build.willRunDockerBuild === false, `${environment} dry-run must not docker build on the runtime target`);
assertCondition(build.willRunDockerComposeBuild === false, `${environment} dry-run must not docker compose build on the runtime target`);
const labels = asRecord(plan.requiredLabels, `${environment}.requiredLabels`);
assertCondition(labels["unidesk.ai/service-id"] === serviceId, `${environment} labels must include service id`);
assertCondition(labels["unidesk.ai/source-repo"] === sourceRepo, `${environment} labels must include source repo`);
assertCondition(labels["unidesk.ai/source-commit"] === desiredCommit, `${environment} labels must include source commit`);
assertCondition(labels["unidesk.ai/dockerfile"] === dockerfile, `${environment} labels must include dockerfile`);
const target = asRecord(plan.target, `${environment}.target`);
assertCondition(target.kind === "d601-k3s", `${environment} target kind must be d601-k3s`);
assertCondition(target.namespace === (environment === "dev" ? "unidesk-dev" : "unidesk"), `${environment} namespace must match`);
assertCondition(target.deployment === (environment === "dev" ? "claudeqq-dev" : "claudeqq"), `${environment} deployment must match`);
assertCondition(target.service === (environment === "dev" ? "claudeqq-dev" : "claudeqq"), `${environment} service must match`);
assertCondition(target.runtimeImage === `unidesk-claudeqq:${desiredCommit}`, `${environment} runtime image must be commit-pinned`);
const validation = asArray(plan.validation, `${environment}.validation`).map((item, index) => stringField(item, `${environment}.validation[${index}]`));
assertCondition(validation.some((line) => line.includes("service health via Kubernetes API service proxy")), `${environment} dry-run must require API service proxy health`);
assertCondition(validation.some((line) => line.includes("source repo")), `${environment} dry-run must require source repo label validation`);
}
assertDeployJson();
assertArtifactCatalog();
assertAdapterSourceContract();
await assertDryRun("dev");
await assertDryRun("prod");
console.log(JSON.stringify({
ok: true,
serviceId,
desiredCommit,
eventPaths,
checks: [
"deploy.json desired dev/prod commit",
"CI.json artifact producer contract",
"adapter read-only event API fallback and health metadata",
"dev/prod artifact-registry deploy-service dry-runs",
],
}, null, 2));