Files
pikasTech-unidesk/scripts/ci-publish-user-service-preflight-contract-test.ts
T

121 lines
7.2 KiB
TypeScript

import { readConfig } from "./src/config";
import { runCiPublishUserServiceDryRunPreflight, type PublishPreflightTransport } from "./src/ci";
type JsonRecord = Record<string, unknown>;
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}
function asRecord(value: unknown, label: string): JsonRecord {
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, { value });
return value as JsonRecord;
}
const commit = "0123456789abcdef0123456789abcdef01234567";
const config = readConfig();
const infraBlockedTransport: PublishPreflightTransport = {
coreFetch: async (path) => ({
ok: false,
status: 503,
body: {
ok: false,
failureKind: "target-stack-not-running",
runnerDisposition: "infra-blocked",
degradedReason: "backend-core-container-missing",
message: `backend-core unavailable for ${path}`,
},
}),
dispatchHostSsh: async (command, waitMs, remoteTimeoutMs) => ({
ok: false,
taskId: null,
status: "infra-blocked",
stdout: "",
stderr: `backend-core bridge unavailable while dispatching readonly SSH task (${waitMs}/${remoteTimeoutMs})`,
exitCode: null,
raw: {
ok: false,
failureKind: "target-stack-not-running",
runnerDisposition: "infra-blocked",
command,
},
}),
commandCwd: "/workspace/unidesk",
artifactRegistryCommand: (probe) => [process.execPath, "scripts/cli.ts", "ssh", probe.providerId, "argv", "bash", "-lc", probe.script],
};
async function main(): Promise<void> {
const result = await runCiPublishUserServiceDryRunPreflight(config, [
"publish-user-service",
"--service",
"frontend",
"--commit",
commit,
"--dry-run",
], infraBlockedTransport);
const record = asRecord(result, "preflight");
const source = asRecord(record.source, "source");
const channels = Array.isArray(record.channels) ? record.channels.map((item) => asRecord(item, "channel")) : [];
const controlChannels = Array.isArray(record.controlChannels) ? record.controlChannels.map((item) => asRecord(item, "control channel")) : [];
const registry = asRecord(record.registry, "registry");
const controlledPublish = asRecord(record.controlledPublish, "controlledPublish");
const backendCore = asRecord(channels.find((item) => item.channel === "backend-core-api")?.detail, "backendCore detail");
const backendCoreTransport = asRecord(backendCore.detail, "backendCore transport payload");
const backendCoreBody = asRecord(backendCoreTransport.body, "backendCore body payload");
const providerDispatch = asRecord(channels.find((item) => item.channel === "provider-dispatch")?.detail, "providerDispatch detail");
const missingChannels = Array.isArray(record.missingChannels) ? record.missingChannels as string[] : [];
const missingControlChannels = Array.isArray(record.missingControlChannels) ? record.missingControlChannels as string[] : [];
assertCondition(record.ok === false, "infra-blocked preflight should fail", record);
assertCondition(record.mode === "dry-run-preflight", "dry-run preflight mode should be reported", record);
assertCondition(record.runnerDisposition === "infra-blocked", "runnerDisposition should be infra-blocked", record);
assertCondition(Array.isArray(record.missingChannels), "missingChannels should be an array", record);
assertCondition(Array.isArray(record.missingControlChannels), "missingControlChannels should be an array", record);
assertCondition(missingChannels.includes("backend-core-api"), "backend-core-api should be missing", record);
assertCondition(missingChannels.includes("database"), "database should be missing", record);
assertCondition(missingChannels.includes("provider-dispatch"), "provider-dispatch should be missing", record);
assertCondition(missingChannels.includes("provider-host-ssh"), "provider-host-ssh should be missing", record);
assertCondition(missingChannels.includes("artifact-registry"), "artifact-registry should be missing", record);
assertCondition(missingControlChannels.join(",") === "backend-core,database,provider,registry", "missingControlChannels should name runner-facing domains", record);
assertCondition(controlChannels.length === 4, "controlChannels should report four runner-facing domains", controlChannels);
assertCondition(controlChannels.every((item) => item.ok === false), "infra-blocked transport should fail every control channel", controlChannels);
assertCondition(
(controlChannels.find((item) => item.channel === "provider")?.probes as unknown[]).join(",") === "provider-dispatch,provider-host-ssh",
"provider control channel should map provider dispatch and host ssh probes",
controlChannels,
);
assertCondition(!JSON.stringify(record).includes("No such container: unidesk-database"), "raw container error should not leak", record);
assertCondition(backendCoreBody.failureKind === "target-stack-not-running", "backend-core detail should classify target-stack-not-running", backendCoreBody);
assertCondition(providerDispatch.status === "infra-blocked", "provider dispatch should be infra-blocked", providerDispatch);
assertCondition(registry.ok === false, "registry channel should fail without backend-core bridge", registry);
assertCondition(Array.isArray(channels) && channels.length >= 5, "expected five channel probes", channels);
assertCondition(source.mode === "planned-only", "source should remain planned-only", source);
assertCondition(source.repoFetchUrl === "git@github.com:pikasTech/unidesk.git", "source repo should use CI catalog ssh form", source);
assertCondition(asRecord(record.artifactSummary, "artifactSummary").imageRef === `127.0.0.1:5000/unidesk/frontend:${commit}`, "artifact ref should remain commit-pinned", record.artifactSummary);
assertCondition(controlledPublish.environment === "D601", "controlledPublish should name the controlled environment", controlledPublish);
assertCondition(controlledPublish.namespace === "unidesk-ci", "controlledPublish should name the Tekton namespace", controlledPublish);
assertCondition(controlledPublish.pipeline === "unidesk-user-service-artifact-publish", "controlledPublish should name the user-service pipeline", controlledPublish);
assertCondition(String(controlledPublish.command ?? "").includes("--wait-ms 1200000"), "controlledPublish should provide the real publish command shape", controlledPublish);
assertCondition(String(record.boundary ?? "").includes("read-only"), "boundary should state preflight is read-only", record);
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"dry-run preflight returns infra-blocked when backend-core/database/provider channels are absent",
"missing channel list names the absent channels",
"artifact summary remains commit-pinned and read-only",
"missingControlChannels maps detailed probes to backend-core/database/provider/registry",
"controlledPublish identifies D601 unidesk-ci as the only place for the real publish",
],
missingChannels: record.missingChannels,
missingControlChannels: record.missingControlChannels,
registry,
}, null, 2)}\n`);
}
if (import.meta.main) {
await main();
}