import assert from "node:assert/strict"; import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "bun:test"; import type { UniDeskConfig } from "./config"; import { rootPath } from "./config"; import { runPikaoaCommand } from "./pikaoa-test-target"; import type { SshCaptureResult } from "./ssh"; test("PikaOA test target submits once and exposes bounded typed status", async () => { const root = mkdtempSync(join(tmpdir(), "pikaoa-test-target-")); const stateRoot = join(root, "state"); const fakeRoot = join(root, "fake-k8s"); const binDir = join(root, "bin"); const secretsDir = join(root, "secrets"); mkdirSync(binDir, { recursive: true }); mkdirSync(secretsDir, { recursive: true }); mkdirSync(join(fakeRoot, "namespaces"), { recursive: true }); const secretValues = ["fixture-db-password", "fixture-admin-password", "fixture-employee-password", "fixture-session-secret"]; for (const [name, value] of [ ["pikaoa-test.env", `DATABASE_URL=postgresql://pikaoa_test:${secretValues[0]}@db.example.invalid:5432/pikaoa_test?sslmode=require\nPIKAOA_SESSION_SECRET=${secretValues[3]}`], ["pikaoa-test-admin-password.txt", secretValues[1]], ["pikaoa-test-employee-password.txt", secretValues[2]], ]) writeFileSync(join(secretsDir, name), `${value}\n`, { mode: 0o600 }); const fakeKubectl = join(binDir, "kubectl"); writeFileSync(fakeKubectl, `#!/bin/sh set -u root="$FAKE_K8S_ROOT" args="$*" namespace="" previous="" for argument in "$@"; do if [ "$previous" = "-n" ]; then namespace="$argument"; fi previous="$argument" done namespace_after_resource() { previous="" for argument in "$@"; do if [ "$previous" = "namespace" ]; then printf '%s' "$argument"; return; fi previous="$argument" done } if printf '%s' "$args" | grep -Eq '(^| )apply( |$)'; then file="" previous="" for argument in "$@"; do if [ "$previous" = "-f" ]; then file="$argument"; fi previous="$argument" done if grep -q '"kind":"Namespace"' "$file"; then applied_identity="$(python3 - "$file" <<'PY' import json, pathlib, sys for line in pathlib.Path(sys.argv[1]).read_text().splitlines(): if not line or line == "---": continue item = json.loads(line) if item.get("kind") == "Namespace": print(item["metadata"]["name"] + "|" + item["metadata"]["labels"]["pikaoa.unidesk.io/instance"]) break PY )" applied_namespace="\${applied_identity%%|*}" applied_instance="\${applied_identity##*|}" printf '%s' "$applied_instance" >"$root/namespaces/$applied_namespace" fi [ ! -f "$root/slow" ] || sleep 0.2 exit 0 fi if printf '%s' "$args" | grep -q ' rollout status '; then [ ! -f "$root/slow" ] || sleep 0.4 exit 0 fi if printf '%s' "$args" | grep -q ' get job/pikaoa-migrate '; then if [ -f "$root/fail-migration-query" ]; then printf '%s\n' 'job query failed' >&2; exit 1; fi if [ -f "$root/failure-target-migration" ]; then printf '%s\n' '{"status":{"conditions":[{"type":"FailureTarget","status":"True","reason":"BackoffLimitExceeded"}]}}' elif [ -f "$root/fail-migration" ]; then printf '%s\n' '{"status":{"conditions":[{"type":"Failed","status":"True","reason":"BackoffLimitExceeded"}]}}' else printf '%s\n' '{"status":{"conditions":[{"type":"Complete","status":"True"}]}}' fi exit 0 fi if printf '%s' "$args" | grep -Eq '(^| )get namespace '; then name="$(namespace_after_resource "$@")" if [ -f "$root/fail-runtime-query" ]; then printf '%s\n' 'unable to connect to the server' >&2 exit 1 fi if [ ! -f "$root/namespaces/$name" ]; then printf 'Error from server (NotFound): namespaces "%s" not found\n' "$name" >&2 exit 1 fi instance="$(cat "$root/namespaces/$name")" if printf '%s' "$args" | grep -q 'managed-by'; then printf '%s' 'unidesk-pikaoa-test-target'; exit 0; fi if printf '%s' "$args" | grep -q 'unidesk.*target'; then printf '%s' 'TEST01'; exit 0; fi if printf '%s' "$args" | grep -q 'unidesk.*instance'; then printf '%s' "$instance"; exit 0; fi printf '{"metadata":{"name":"%s","labels":{"app.kubernetes.io/managed-by":"unidesk-pikaoa-test-target","pikaoa.unidesk.io/target":"TEST01","pikaoa.unidesk.io/instance":"%s"}}}\n' "$name" "$instance" exit 0 fi if printf '%s' "$args" | grep -q ' get deployment,job,service,persistentvolumeclaim,pod '; then if [ -f "$root/fail-runtime-resource-query" ]; then printf '%s\n' 'resource query failed' >&2 exit 1 fi printf '%s\n' '{"items":[]}' exit 0 fi if printf '%s' "$args" | grep -Eq '(^| )delete namespace '; then name="$(namespace_after_resource "$@")" rm -f "$root/namespaces/$name" exit 0 fi printf 'unsupported fake kubectl call: %s\n' "$args" >&2 exit 64 `, { mode: 0o700 }); chmodSync(fakeKubectl, 0o700); const fixturePath = join(root, "pikaoa.yaml"); const fixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8") .replace("validationOnly: true", "validationOnly: false") .replace("statusRequestTimeoutSeconds: 3", "statusRequestTimeoutSeconds: 31") .replace("logTailLines: 8", "logTailLines: 101") .replace("/tmp/unidesk-pikaoa-test-target-fixture", stateRoot); writeFileSync(fixturePath, fixture); const config = {} as UniDeskConfig; let lastCapture: SshCaptureResult | null = null; const localCapture = async (_config: UniDeskConfig, _route: string, args: string[], stdin: string): Promise => { assert.deepEqual(args, ["sh"]); const child = Bun.spawn(["sh"], { env: { ...process.env, PATH: `${binDir}:${process.env.PATH ?? ""}`, FAKE_K8S_ROOT: fakeRoot }, stdin: "pipe", stdout: "pipe", stderr: "pipe", }); child.stdin.write(stdin); child.stdin.end(); const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]); lastCapture = { stdout, stderr, exitCode }; return lastCapture; }; const args = (action: string, instance: string, withCommit = false, step = "all"): string[] => [ "test-target", action, "--config", fixturePath, "--target", "TEST01", "--instance", instance, ...(withCommit ? ["--commit", "0123456789abcdef"] : []), "--step", step, ...((action === "start" || action === "stop") ? ["--confirm"] : []), "--output", "json", ]; const projection = async (action: string, instance: string, withCommit = false, step = "all"): Promise> => { const result = await runPikaoaCommand(config, args(action, instance, withCommit, step), localCapture); return JSON.parse(result.renderedText) as Record; }; const projectionWithCommit = async (action: string, instance: string, commit: string, step = "all"): Promise> => { const commandArgs = args(action, instance, true, step); commandArgs[commandArgs.indexOf("--commit") + 1] = commit; const result = await runPikaoaCommand(config, commandArgs, localCapture); return JSON.parse(result.renderedText) as Record; }; const waitFor = async (instance: string, expectedCode: string, step = "all"): Promise> => { for (let attempt = 0; attempt < 80; attempt += 1) { const value = await projection("status", instance, false, step); const status = value.status as Record; if (status.code === expectedCode) return value; await Bun.sleep(25); } assert.fail(`status did not reach ${expectedCode}`); }; try { const planned = await projection("plan", "runtime", true); const plan = planned.plan as Record; const secret = plan.secret as Record; const sources = secret.sources as Array>; assert.equal(sources.find((source) => source.purpose === "database.connection")?.presence, true); const objects = plan.objects as Array>; assert.equal(objects.some((object) => object.kind === "StatefulSet" || object.name === "pikaoa-postgres"), false); assert.equal(objects.some((object) => object.kind === "Deployment" && object.name === "pikaoa-web"), true); assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === "pikaoa-api")?.podFsGroup, 65532); assert.deepEqual(plan.exposure, { serviceType: "NodePort", serviceName: "pikaoa-web", hostIP: "192.0.2.10", port: 32080 }); assert.deepEqual(plan.webApiUpstream, { envName: "PIKAOA_API_UPSTREAM", value: "http://pikaoa-api:8080", scope: "same-namespace" }); const missing = await projection("status", "runtime", false, "api"); assert.equal((missing.status as Record).code, "start-not-found"); writeFileSync(join(fakeRoot, "slow"), "1\n"); const submitted = await projection("start", "runtime", true); assert.equal(submitted.code, "start-submitted", JSON.stringify(lastCapture)); assert.equal(submitted.mutation, true); assert.deepEqual((submitted.target as Record).taskState, { rootPath: stateRoot, statusRequestTimeoutSeconds: 31, logTailLines: 101, }); const running = await projection("status", "runtime"); assert.equal((running.status as Record).code, "start-running"); const concurrentCommit = "fedcba9876543210"; const concurrent = await projectionWithCommit("start", "runtime", concurrentCommit); assert.equal(concurrent.code, "start-instance-conflict"); assert.equal(concurrent.mutation, false); assert.equal(concurrent.previousTaskReset, false); rmSync(join(fakeRoot, "slow")); const succeeded = await waitFor("runtime", "start-succeeded"); const succeededStatus = succeeded.status as Record; assert.equal((succeededStatus.task as Record).exitCode, 0); assert.ok((succeededStatus.logTail as unknown[]).length <= 101); writeFileSync(join(fakeRoot, "fail-runtime-query"), "1\n"); const runtimeQueryFailed = await projection("status", "runtime"); const runtimeQueryFailedStatus = runtimeQueryFailed.status as Record; assert.equal(runtimeQueryFailed.ok, false); assert.equal(runtimeQueryFailedStatus.code, "runtime-query-failed"); assert.equal(runtimeQueryFailedStatus.taskCode, "start-succeeded"); assert.equal((runtimeQueryFailedStatus.runtime as Record).code, "runtime-query-failed"); const runtimeQueryFailedText = await runPikaoaCommand(config, args("status", "runtime").slice(0, -2), localCapture); assert.match(runtimeQueryFailedText.renderedText, /status=runtime-query-failed/); rmSync(join(fakeRoot, "fail-runtime-query")); writeFileSync(join(fakeRoot, "fail-runtime-resource-query"), "1\n"); const runtimeResourceQueryFailed = await projection("status", "runtime"); const runtimeResourceQueryFailedStatus = runtimeResourceQueryFailed.status as Record; assert.equal(runtimeResourceQueryFailed.ok, false); assert.equal(runtimeResourceQueryFailedStatus.code, "runtime-resource-query-failed"); assert.equal(runtimeResourceQueryFailedStatus.taskCode, "start-succeeded"); assert.equal((runtimeResourceQueryFailedStatus.runtime as Record).code, "runtime-resource-query-failed"); rmSync(join(fakeRoot, "fail-runtime-resource-query")); const repeated = await projection("start", "runtime", true); assert.equal(repeated.code, "start-already-succeeded"); assert.equal(repeated.mutation, false); const rerunSubmitted = await projectionWithCommit("start", "runtime", concurrentCommit); assert.equal(rerunSubmitted.code, "start-submitted"); assert.equal(rerunSubmitted.previousTaskReset, true); const rerunSucceeded = await waitFor("runtime", "start-succeeded"); assert.equal(((rerunSucceeded.status as Record).task as Record).sourceCommit, concurrentCommit); writeFileSync(fixturePath, readFileSync(fixturePath, "utf8").replace(" livenessPath: /healthz\n", " livenessPath: /healthz-v2\n")); const manifestRerunSubmitted = await projectionWithCommit("start", "runtime", concurrentCommit); assert.equal(manifestRerunSubmitted.code, "start-submitted"); assert.equal(manifestRerunSubmitted.previousTaskReset, true); const manifestRerunSucceeded = await waitFor("runtime", "start-succeeded"); await Bun.sleep(5); const sameRequestAfterRenderTimeChanged = await projectionWithCommit("start", "runtime", concurrentCommit); assert.equal(sameRequestAfterRenderTimeChanged.code, "start-already-succeeded"); assert.equal(sameRequestAfterRenderTimeChanged.previousTaskReset, false); writeFileSync(join(fakeRoot, "slow"), "1\n"); const workerMissingSubmitted = await projection("start", "runtime", true, "api"); const workerPid = Number((workerMissingSubmitted.remote as Record).pid); assert.equal(Number.isSafeInteger(workerPid) && workerPid > 0, true); process.kill(workerPid, "SIGKILL"); await Bun.sleep(25); const workerMissingReset = await projectionWithCommit("start", "runtime", concurrentCommit, "api"); assert.equal(workerMissingReset.code, "start-submitted"); assert.equal(workerMissingReset.previousTaskReset, true); rmSync(join(fakeRoot, "slow")); const workerMissingSucceeded = await waitFor("runtime", "start-succeeded", "api"); writeFileSync(join(fakeRoot, "fail-migration"), "1\n"); await projection("start", "runtime", true, "migration"); const failed = await waitFor("runtime", "start-failed", "migration"); const failedTask = ((failed.status as Record).task as Record); assert.equal(failedTask.code, "migration-job-failed"); assert.equal(failedTask.exitCode, 46); rmSync(join(fakeRoot, "fail-migration")); writeFileSync(join(fakeRoot, "failure-target-migration"), "1\n"); await projectionWithCommit("start", "runtime", concurrentCommit, "migration"); const failureTarget = await waitFor("runtime", "start-failed", "migration"); const failureTargetTask = ((failureTarget.status as Record).task as Record); assert.equal(failureTargetTask.code, "migration-job-failure-target"); assert.equal(failureTargetTask.exitCode, 46); rmSync(join(fakeRoot, "failure-target-migration")); writeFileSync(join(fakeRoot, "fail-migration-query"), "1\n"); await projectionWithCommit("start", "runtime", "abcdef0123456789", "migration"); const migrationQueryFailed = await waitFor("runtime", "start-failed", "migration"); const migrationQueryFailedTask = ((migrationQueryFailed.status as Record).task as Record); assert.equal(migrationQueryFailedTask.code, "migration-job-query-failed"); assert.equal(migrationQueryFailedTask.exitCode, 46); rmSync(join(fakeRoot, "fail-migration-query")); await projectionWithCommit("start", "runtime", "1234567890abcdef", "migration"); const migrationSucceeded = await waitFor("runtime", "start-succeeded", "migration"); assert.equal((((migrationSucceeded.status as Record).task as Record).exitCode), 0); writeFileSync(join(fakeRoot, "slow"), "1\n"); await projection("start", "runtime", true, "web"); const stopped = await projection("stop", "runtime", false, "web"); assert.equal(stopped.startTaskDisposition, "cancel-requested"); assert.equal(stopped.mutation, true); await waitFor("runtime", "start-canceled", "web"); for (const name of [ "pikaoa-test.env", "pikaoa-test-admin-password.txt", "pikaoa-test-employee-password.txt", ]) rmSync(join(secretsDir, name)); const foundationPlan = await projection("plan", "foundation", false, "foundation"); assert.equal(foundationPlan.readyForMutation, true); assert.deepEqual(foundationPlan.missingFields, []); assert.deepEqual((foundationPlan.blockers as unknown[]), []); const foundationObjects = ((foundationPlan.plan as Record).objects as Array>); assert.deepEqual(foundationObjects.map((object) => object.kind), ["Namespace", "PersistentVolumeClaim"]); assert.equal(((foundationPlan.plan as Record).secret as Record).requiredForStep, false); const foundationSubmitted = await projection("start", "foundation", false, "foundation"); assert.equal(foundationSubmitted.code, "start-submitted", JSON.stringify(lastCapture)); const foundationSucceeded = await waitFor("foundation", "start-succeeded", "foundation"); const foundationRepeated = await projection("start", "foundation", false, "foundation"); assert.equal(foundationRepeated.code, "start-already-succeeded"); const foundationRerun = await projectionWithCommit("start", "foundation", concurrentCommit, "foundation"); assert.equal(foundationRerun.code, "start-submitted"); assert.equal(foundationRerun.previousTaskReset, true); const foundationRerunSucceeded = await waitFor("foundation", "start-succeeded", "foundation"); const serialized = JSON.stringify({ submitted, succeeded, concurrent, rerunSubmitted, rerunSucceeded, manifestRerunSubmitted, manifestRerunSucceeded, sameRequestAfterRenderTimeChanged, workerMissingSubmitted, workerMissingReset, workerMissingSucceeded, failed, failureTarget, migrationQueryFailed, migrationSucceeded, stopped, foundationSubmitted, foundationSucceeded, foundationRepeated, foundationRerun, foundationRerunSucceeded, }); for (const value of secretValues) assert.equal(serialized.includes(value), false); assert.equal(serialized.includes("postgresql://pikaoa_test:"), false); } finally { rmSync(root, { recursive: true, force: true }); } }, 30_000); test("validation-only fixture never invokes remote capture", async () => { let calls = 0; const forbiddenCapture = async (): Promise => { calls += 1; throw new Error("validation-only fixture attempted remote capture"); }; const result = await runPikaoaCommand({} as UniDeskConfig, [ "test-target", "status", "--config", "config/fixtures/pikaoa-test-target.yaml", "--target", "TEST01", "--instance", "validation-only", "--output", "json", ], forbiddenCapture); assert.equal(calls, 0); const payload = JSON.parse(result.renderedText) as Record; assert.equal(payload.remoteQueried, false); assert.equal((payload.status as Record).remoteQueried, false); const planResult = await runPikaoaCommand({} as UniDeskConfig, [ "test-target", "plan", "--config", "config/fixtures/pikaoa-test-target.yaml", "--target", "TEST01", "--instance", "validation-only", "--commit", "0123456789abcdef", "--output", "json", ], forbiddenCapture); const planPayload = JSON.parse(planResult.renderedText) as Record; const plan = planPayload.plan as Record; assert.deepEqual(plan.database, { configRef: "config/platform-db/postgres-pk01.yaml#exports.connectionStrings.pikaoa-test-database-url", name: "pikaoa_test", username: "pikaoa_test", schema: "pikaoa", connection: { sourceRef: "./secrets/pikaoa-test.env", sourceKey: "DATABASE_URL", targetKey: "DATABASE_URL", valuesPrinted: false, }, dsnParameterPresence: { sslmode: true, search_path: true }, valuesPrinted: false, }); assert.deepEqual(plan.runtimeConfig, { secretName: "pikaoa-test-runtime", key: "pikaoa.yaml", mountPath: "/etc/pikaoa/pikaoa.yaml", consumers: ["migration", "api", "worker"], valuesPrinted: false, }); assert.equal(planPayload.valuesPrinted, false); assert.equal(JSON.stringify(planPayload).includes("fixture-db-password"), false); assert.equal(JSON.stringify(planPayload).includes("postgres://"), false); const missingSchemaRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-schema-")); const missingSchemaPath = join(missingSchemaRoot, "pikaoa.yaml"); const missingSchemaFixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8") .replace(" schema: pikaoa\n", ""); writeFileSync(missingSchemaPath, missingSchemaFixture); try { await assert.rejects( runPikaoaCommand({} as UniDeskConfig, [ "test-target", "status", "--config", missingSchemaPath, "--target", "TEST01", "--instance", "missing-schema", "--output", "json", ], forbiddenCapture), /testRuntime\.targets\.TEST01\.database\.schema 必须是非空字符串/u, ); assert.equal(calls, 0); } finally { rmSync(missingSchemaRoot, { recursive: true, force: true }); } const unconfiguredRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-unconfigured-")); const unconfiguredPath = join(unconfiguredRoot, "pikaoa.yaml"); writeFileSync(unconfiguredPath, "version: 1\nkind: pikaoa-platform-delivery\ntestRuntime:\n defaultTargetId: null\n targets: {}\n"); try { const unconfigured = await runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", unconfiguredPath, "--output", "json"], forbiddenCapture); const unconfiguredPayload = JSON.parse(unconfigured.renderedText) as Record; assert.equal(calls, 0); assert.equal(unconfiguredPayload.configured, false); assert.equal(unconfiguredPayload.remoteQueried, false); } finally { rmSync(unconfiguredRoot, { recursive: true, force: true }); } });