457 lines
15 KiB
JavaScript
457 lines
15 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
buildKubectlCommandPrefix,
|
|
compareRuntimeIdentityEnv,
|
|
decideDevLegacySimulatorDeploymentCleanup,
|
|
decideDevTemplateJobReplacement,
|
|
formatKubeconfigAccessFailure,
|
|
inspectCodeAgentProviderDesiredState,
|
|
inspectCodeAgentProviderLiveDeployment,
|
|
parseArgs,
|
|
redactSensitiveText,
|
|
resolveApplySourceCommit,
|
|
resolveDevKubeconfigSelection
|
|
} from "./dev-deploy-apply.mjs";
|
|
import {
|
|
devCdTransactionGuardFailure,
|
|
hasDevCdTransactionEnv,
|
|
requireDevCdTransactionForSideEffect
|
|
} from "./dev-cd-transaction-guard.mjs";
|
|
|
|
const codeAgentBaseUrl = "http://172.26.26.227:17680/v1/responses";
|
|
|
|
function codeAgentDeploy(env = {}) {
|
|
return {
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-api",
|
|
env: {
|
|
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-5.5",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: codeAgentBaseUrl,
|
|
OPENAI_API_KEY: "secretRef:hwlab-code-agent-provider/openai-api-key",
|
|
...env
|
|
}
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function codeAgentWorkloads(envOverrides = []) {
|
|
return {
|
|
kind: "List",
|
|
items: [
|
|
{
|
|
kind: "Deployment",
|
|
metadata: {
|
|
name: "hwlab-cloud-api",
|
|
namespace: "hwlab-dev",
|
|
labels: {
|
|
"hwlab.pikastech.local/service-id": "hwlab-cloud-api"
|
|
}
|
|
},
|
|
spec: {
|
|
template: {
|
|
spec: {
|
|
containers: [
|
|
{
|
|
name: "hwlab-cloud-api",
|
|
env: [
|
|
{ name: "HWLAB_CODE_AGENT_PROVIDER", value: "openai" },
|
|
{ name: "HWLAB_CODE_AGENT_MODEL", value: "gpt-5.5" },
|
|
{ name: "HWLAB_CODE_AGENT_OPENAI_BASE_URL", value: codeAgentBaseUrl },
|
|
{
|
|
name: "OPENAI_API_KEY",
|
|
valueFrom: {
|
|
secretKeyRef: {
|
|
name: "hwlab-code-agent-provider",
|
|
key: "openai-api-key"
|
|
}
|
|
}
|
|
},
|
|
...envOverrides
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
test("allowlisted suspended template Job image change plans replacement", () => {
|
|
const decision = decideDevTemplateJobReplacement({
|
|
jobName: "hwlab-agent-worker-template",
|
|
jobNamespace: "hwlab-dev",
|
|
desiredSuspended: true,
|
|
liveExists: true,
|
|
liveSuspended: true,
|
|
oldImages: [
|
|
{
|
|
name: "hwlab-agent-worker",
|
|
serviceId: "hwlab-agent-worker",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-agent-worker:old1234"
|
|
}
|
|
],
|
|
newImages: [
|
|
{
|
|
name: "hwlab-agent-worker",
|
|
serviceId: "hwlab-agent-worker",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-agent-worker:new5678"
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.equal(decision.replace, true);
|
|
assert.equal(decision.result, "planned");
|
|
assert.equal(decision.namespace, "hwlab-dev");
|
|
assert.equal(decision.jobName, "hwlab-agent-worker-template");
|
|
assert.equal(decision.oldImage, "127.0.0.1:5000/hwlab/hwlab-agent-worker:old1234");
|
|
assert.equal(decision.newImage, "127.0.0.1:5000/hwlab/hwlab-agent-worker:new5678");
|
|
});
|
|
|
|
test("DEV CD transaction guard rejects direct legacy side-effect calls", () => {
|
|
assert.equal(hasDevCdTransactionEnv({}), false);
|
|
assert.equal(hasDevCdTransactionEnv({ HWLAB_CD_TRANSACTION_ID: "tx-123" }), true);
|
|
|
|
const failure = requireDevCdTransactionForSideEffect({
|
|
env: {},
|
|
script: "scripts/dev-deploy-apply.mjs",
|
|
mode: "--apply"
|
|
});
|
|
assert.deepEqual(failure, devCdTransactionGuardFailure({
|
|
script: "scripts/dev-deploy-apply.mjs",
|
|
mode: "--apply"
|
|
}));
|
|
assert.equal(failure.error, "cd-transaction-required");
|
|
assert.equal(failure.mutationAttempted, false);
|
|
assert.match(failure.entrypoint, /scripts\/dev-cd-apply\.mjs/u);
|
|
|
|
assert.equal(
|
|
requireDevCdTransactionForSideEffect({
|
|
env: { HWLAB_CD_TRANSACTION_ID: "tx-123" },
|
|
script: "scripts/dev-deploy-apply.mjs",
|
|
mode: "--apply"
|
|
}),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("matching allowlisted suspended template Job image does not replace", () => {
|
|
const image = "127.0.0.1:5000/hwlab/hwlab-cli:73b379f";
|
|
const decision = decideDevTemplateJobReplacement({
|
|
jobName: "hwlab-cli-template",
|
|
jobNamespace: "hwlab-dev",
|
|
desiredSuspended: true,
|
|
liveExists: true,
|
|
liveSuspended: true,
|
|
oldImages: [{ name: "hwlab-cli", serviceId: "hwlab-cli", image }],
|
|
newImages: [{ name: "hwlab-cli", serviceId: "hwlab-cli", image }]
|
|
});
|
|
|
|
assert.equal(decision.replace, false);
|
|
assert.equal(decision.result, "not_needed");
|
|
});
|
|
|
|
test("replacement policy refuses non-allowlisted or active Jobs", () => {
|
|
const ignored = decideDevTemplateJobReplacement({
|
|
jobName: "some-other-job",
|
|
jobNamespace: "hwlab-dev",
|
|
desiredSuspended: true,
|
|
liveExists: true,
|
|
liveSuspended: true,
|
|
oldImages: [{ name: "worker", serviceId: "worker", image: "image:old" }],
|
|
newImages: [{ name: "worker", serviceId: "worker", image: "image:new" }]
|
|
});
|
|
assert.equal(ignored.replace, false);
|
|
assert.equal(ignored.result, "ignored");
|
|
|
|
const active = decideDevTemplateJobReplacement({
|
|
jobName: "hwlab-cli-template",
|
|
jobNamespace: "hwlab-dev",
|
|
desiredSuspended: true,
|
|
liveExists: true,
|
|
liveSuspended: false,
|
|
oldImages: [{ name: "hwlab-cli", serviceId: "hwlab-cli", image: "image:old" }],
|
|
newImages: [{ name: "hwlab-cli", serviceId: "hwlab-cli", image: "image:new" }]
|
|
});
|
|
assert.equal(active.replace, false);
|
|
assert.equal(active.result, "blocked");
|
|
});
|
|
|
|
test("allowlisted stale simulator Deployment plans cleanup only with StatefulSet replacement", () => {
|
|
const decision = decideDevLegacySimulatorDeploymentCleanup({
|
|
deploymentName: "hwlab-box-simu",
|
|
deploymentNamespace: "hwlab-dev",
|
|
desiredReplacementExists: true,
|
|
desiredReplacementKind: "StatefulSet",
|
|
liveExists: true,
|
|
oldImages: [
|
|
{
|
|
name: "hwlab-box-simu",
|
|
serviceId: "hwlab-box-simu",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-box-simu:old1234"
|
|
}
|
|
],
|
|
newImages: [
|
|
{
|
|
name: "hwlab-box-simu",
|
|
serviceId: "hwlab-box-simu",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-box-simu:new5678"
|
|
}
|
|
]
|
|
});
|
|
|
|
assert.equal(decision.cleanup, true);
|
|
assert.equal(decision.result, "planned");
|
|
assert.equal(decision.namespace, "hwlab-dev");
|
|
assert.equal(decision.deploymentName, "hwlab-box-simu");
|
|
assert.equal(decision.serviceId, "hwlab-box-simu");
|
|
assert.equal(decision.desiredReplacementKind, "StatefulSet");
|
|
assert.equal(decision.oldImage, "127.0.0.1:5000/hwlab/hwlab-box-simu:old1234");
|
|
assert.equal(decision.newImage, "127.0.0.1:5000/hwlab/hwlab-box-simu:new5678");
|
|
});
|
|
|
|
test("legacy simulator cleanup refuses non-DEV or missing replacement cleanup", () => {
|
|
const wrongNamespace = decideDevLegacySimulatorDeploymentCleanup({
|
|
deploymentName: "hwlab-gateway-simu",
|
|
deploymentNamespace: "hwlab-prod",
|
|
desiredReplacementExists: true,
|
|
desiredReplacementKind: "StatefulSet",
|
|
liveExists: true
|
|
});
|
|
assert.equal(wrongNamespace.cleanup, false);
|
|
assert.equal(wrongNamespace.result, "blocked");
|
|
|
|
const missingReplacement = decideDevLegacySimulatorDeploymentCleanup({
|
|
deploymentName: "hwlab-gateway-simu",
|
|
deploymentNamespace: "hwlab-dev",
|
|
desiredReplacementExists: false,
|
|
desiredReplacementKind: null,
|
|
liveExists: true
|
|
});
|
|
assert.equal(missingReplacement.cleanup, false);
|
|
assert.equal(missingReplacement.result, "blocked");
|
|
|
|
const ignored = decideDevLegacySimulatorDeploymentCleanup({
|
|
deploymentName: "hwlab-cloud-api",
|
|
deploymentNamespace: "hwlab-dev",
|
|
desiredReplacementExists: true,
|
|
desiredReplacementKind: "StatefulSet",
|
|
liveExists: true
|
|
});
|
|
assert.equal(ignored.cleanup, false);
|
|
assert.equal(ignored.result, "ignored");
|
|
});
|
|
|
|
test("apply source commit follows deploy/catalog artifact identity", () => {
|
|
assert.equal(
|
|
resolveApplySourceCommit({ commitId: "73b379f" }, { commitId: "73b379f" }, "cb35ada68606"),
|
|
"73b379f"
|
|
);
|
|
assert.equal(
|
|
resolveApplySourceCommit({ commitId: "73b379f" }, { commitId: "deadbee" }, "cb35ada68606"),
|
|
"cb35ada68606"
|
|
);
|
|
});
|
|
|
|
test("runtime identity env comparator accepts matching commit image and tag", () => {
|
|
const comparison = compareRuntimeIdentityEnv(
|
|
{
|
|
HWLAB_COMMIT_ID: "7de6edd",
|
|
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd",
|
|
HWLAB_IMAGE_TAG: "7de6edd"
|
|
},
|
|
{
|
|
HWLAB_COMMIT_ID: "7de6edd2c41fb50dcb007353780338be8832b27e",
|
|
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd",
|
|
HWLAB_IMAGE_TAG: "7de6edd"
|
|
}
|
|
);
|
|
|
|
assert.equal(comparison.status, "match");
|
|
assert.equal(comparison.matchesDesired, true);
|
|
assert.deepEqual(comparison.drift, []);
|
|
});
|
|
|
|
test("runtime identity env comparator detects stale cloud-web env drift", () => {
|
|
const comparison = compareRuntimeIdentityEnv(
|
|
{
|
|
HWLAB_COMMIT_ID: "c7de474",
|
|
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-cloud-web:c7de474",
|
|
HWLAB_IMAGE_TAG: "c7de474"
|
|
},
|
|
{
|
|
HWLAB_COMMIT_ID: "7de6edd",
|
|
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd",
|
|
HWLAB_IMAGE_TAG: "7de6edd"
|
|
}
|
|
);
|
|
|
|
assert.equal(comparison.status, "drift");
|
|
assert.equal(comparison.matchesDesired, false);
|
|
assert.deepEqual(
|
|
comparison.drift.map((field) => field.name),
|
|
["HWLAB_COMMIT_ID", "HWLAB_IMAGE", "HWLAB_IMAGE_TAG"]
|
|
);
|
|
});
|
|
|
|
test("Code Agent provider desired state preserves Secret ref and egress base URL", () => {
|
|
const inspection = inspectCodeAgentProviderDesiredState(codeAgentDeploy(), codeAgentWorkloads());
|
|
|
|
assert.equal(inspection.ready, true);
|
|
assert.equal(inspection.status, "pass");
|
|
assert.equal(inspection.secretRef.present, true);
|
|
assert.equal(inspection.secretMaterialRead, false);
|
|
assert.equal(inspection.egress.deployMatchesDevProxy, true);
|
|
assert.equal(inspection.egress.workloadMatchesDevProxy, true);
|
|
});
|
|
|
|
test("Code Agent provider desired state blocks direct public OpenAI base URL", () => {
|
|
const inspection = inspectCodeAgentProviderDesiredState(
|
|
codeAgentDeploy({ HWLAB_CODE_AGENT_OPENAI_BASE_URL: "https://api.openai.com/v1/responses" }),
|
|
codeAgentWorkloads([
|
|
{ name: "HWLAB_CODE_AGENT_OPENAI_BASE_URL", value: "https://api.openai.com/v1/responses" }
|
|
])
|
|
);
|
|
|
|
assert.equal(inspection.ready, false);
|
|
assert.equal(inspection.status, "blocked");
|
|
assert.equal(inspection.egress.directPublicOpenAi, true);
|
|
assert.ok(inspection.missingEgressContract.some((entry) => entry.includes("api.openai.com")));
|
|
});
|
|
|
|
test("Code Agent live Deployment inspection reads only env metadata", () => {
|
|
const deployment = codeAgentWorkloads().items[0];
|
|
const inspection = inspectCodeAgentProviderLiveDeployment(deployment);
|
|
|
|
assert.equal(inspection.ready, true);
|
|
assert.equal(inspection.secretRef.present, true);
|
|
assert.equal(inspection.secretValuesRead, false);
|
|
assert.equal(inspection.kubernetesSecretDataRead, false);
|
|
assert.equal(inspection.validationScope, "deployment-env-and-secretKeyRef-metadata-only");
|
|
});
|
|
|
|
test("DEV kubeconfig selection prefers flag then HWLAB_DEV_KUBECONFIG then KUBECONFIG then D601 default", () => {
|
|
assert.deepEqual(
|
|
resolveDevKubeconfigSelection({
|
|
flagValue: "/tmp/from-flag",
|
|
env: {
|
|
HWLAB_DEV_KUBECONFIG: "/tmp/from-hwlab",
|
|
KUBECONFIG: "/tmp/from-kubeconfig"
|
|
}
|
|
}),
|
|
{
|
|
kubeconfig: "/tmp/from-flag",
|
|
source: "flag:--kubeconfig",
|
|
paths: ["/tmp/from-flag"]
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(
|
|
resolveDevKubeconfigSelection({
|
|
env: {
|
|
HWLAB_DEV_KUBECONFIG: "/tmp/from-hwlab",
|
|
KUBECONFIG: "/tmp/from-kubeconfig"
|
|
}
|
|
}),
|
|
{
|
|
kubeconfig: "/tmp/from-hwlab",
|
|
source: "env:HWLAB_DEV_KUBECONFIG",
|
|
paths: ["/tmp/from-hwlab"]
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(
|
|
resolveDevKubeconfigSelection({
|
|
env: {
|
|
KUBECONFIG: "/tmp/from-kubeconfig"
|
|
}
|
|
}),
|
|
{
|
|
kubeconfig: "/tmp/from-kubeconfig",
|
|
source: "env:KUBECONFIG",
|
|
paths: ["/tmp/from-kubeconfig"]
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(
|
|
resolveDevKubeconfigSelection({ env: {} }),
|
|
{
|
|
kubeconfig: "/etc/rancher/k3s/k3s.yaml",
|
|
source: "default:d601-k3s",
|
|
paths: ["/etc/rancher/k3s/k3s.yaml"]
|
|
}
|
|
);
|
|
});
|
|
|
|
test("DEV kubeconfig selection keeps multi-path KUBECONFIG readable checks explicit", () => {
|
|
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
const selection = resolveDevKubeconfigSelection({
|
|
env: {
|
|
KUBECONFIG: [`/tmp/one`, `/tmp/two`].join(delimiter)
|
|
}
|
|
});
|
|
|
|
assert.equal(selection.kubeconfig, [`/tmp/one`, `/tmp/two`].join(delimiter));
|
|
assert.equal(selection.source, "env:KUBECONFIG");
|
|
assert.deepEqual(selection.paths, ["/tmp/one", "/tmp/two"]);
|
|
});
|
|
|
|
test("kubectl command prefix quotes kubeconfig paths without exposing contents", () => {
|
|
assert.equal(
|
|
buildKubectlCommandPrefix("/var/lib/unidesk/code-queue/kubeconfig"),
|
|
"KUBECONFIG=/var/lib/unidesk/code-queue/kubeconfig kubectl"
|
|
);
|
|
assert.equal(
|
|
buildKubectlCommandPrefix("/tmp/path with space/kubeconfig"),
|
|
"KUBECONFIG='/tmp/path with space/kubeconfig' kubectl"
|
|
);
|
|
});
|
|
|
|
test("kubeconfig access failure names source and path without secret material", () => {
|
|
const message = formatKubeconfigAccessFailure(
|
|
{
|
|
source: "env:HWLAB_DEV_KUBECONFIG"
|
|
},
|
|
"/missing/kubeconfig"
|
|
);
|
|
|
|
assert.match(message, /env:HWLAB_DEV_KUBECONFIG/u);
|
|
assert.match(message, /\/missing\/kubeconfig/u);
|
|
assert.match(message, /Secret values are never printed/u);
|
|
assert.equal(message.includes("client-key-data:"), false);
|
|
});
|
|
|
|
test("parseArgs reports a missing kubeconfig path as a blocking error", () => {
|
|
const args = parseArgs(["--apply", "--kubeconfig"]);
|
|
assert.equal(args.apply, true);
|
|
assert.equal(args.kubeconfigSpecified, true);
|
|
assert.deepEqual(args.errors, ["--kubeconfig requires a non-empty path value"]);
|
|
});
|
|
|
|
test("redaction removes common token and password material from kubectl output", () => {
|
|
const output = redactSensitiveText(
|
|
[
|
|
"Authorization: Bearer abc.def-123",
|
|
"token: abcdef123456",
|
|
"password=plain-secret",
|
|
"client-key-data: LS0tPRIVATE",
|
|
"postgresql://hwlab:db-secret@example.invalid:5432/hwlab"
|
|
].join("\n")
|
|
);
|
|
|
|
assert.equal(output.includes("abc.def-123"), false);
|
|
assert.equal(output.includes("plain-secret"), false);
|
|
assert.equal(output.includes("LS0tPRIVATE"), false);
|
|
assert.equal(output.includes("db-secret"), false);
|
|
assert.match(output, /Bearer <redacted>/u);
|
|
assert.match(output, /password=<redacted>/u);
|
|
assert.match(output, /client-key-data: <redacted>/u);
|
|
assert.match(output, /postgresql:\/\/hwlab:<redacted>@example.invalid/u);
|
|
});
|