719 lines
32 KiB
TypeScript
719 lines
32 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { join } from "node:path";
|
|
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
agentRunManagedRepositoryDesiredConfig,
|
|
agentRunManagedRepositoryRenderHash,
|
|
renderAgentRunManagedRepositoryReconcilerFragment,
|
|
} from "./agentrun-managed-repository-reconciler";
|
|
import {
|
|
parseAgentRunGitFetchCredentialCapability,
|
|
resolveAgentRunLaneTarget,
|
|
} from "./agentrun-lanes";
|
|
import { resolveCicdDeliveryAuthority } from "./cicd-delivery-authority";
|
|
import { renderGitMirrorStatusEnvelope } from "./agentrun/git-mirror";
|
|
import { yamlLaneGitMirrorStatusScript } from "./agentrun/secrets";
|
|
import { renderPlatformInfraGiteaDesiredFragments } from "./platform-infra-gitea-desired-fragments";
|
|
|
|
const controllerPath = new URL("../native/agentrun/managed-repository-reconciler.mjs", import.meta.url).pathname;
|
|
const askPassPath = new URL("../native/agentrun/git-askpass.mjs", import.meta.url).pathname;
|
|
|
|
describe("AgentRun YAML-owned managed repository reconciler", () => {
|
|
test("renders one narrow controller from the lane repository list", () => {
|
|
const spec = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }).spec;
|
|
const desired = agentRunManagedRepositoryDesiredConfig(spec);
|
|
expect(desired.repositories.map((item) => item.repository)).toEqual([
|
|
"pikasTech/agentrun",
|
|
"pikasTech/unidesk",
|
|
"pikasTech/agent_skills",
|
|
]);
|
|
expect(desired.repositories.every((item) => item.remote.startsWith("https://github.com/"))).toBe(true);
|
|
expect(desired.remoteAuth).toMatchObject({
|
|
authMode: "github-https-token",
|
|
host: "github.com",
|
|
proxy: { host: "127.0.0.1", port: 10808 },
|
|
});
|
|
expect(desired.lifecycle).toEqual({
|
|
undeclaredRepositoryPolicy: "retain",
|
|
managedRefs: "source-branch-only",
|
|
});
|
|
|
|
const documents = renderAgentRunManagedRepositoryReconcilerFragment("NC01")
|
|
.split(/^---\s*$/mu)
|
|
.map((item) => Bun.YAML.parse(item) as any);
|
|
expect(documents.map((item) => item.kind)).toEqual(["ServiceAccount", "ConfigMap", "Deployment"]);
|
|
expect(documents[0].automountServiceAccountToken).toBe(false);
|
|
expect(documents[1].metadata.annotations["unidesk.ai/undeclared-repository-policy"]).toBe("retain");
|
|
expect(JSON.parse(documents[1].data["config.json"]).repositories).toEqual(desired.repositories);
|
|
expect(documents[2].spec.strategy.type).toBe("Recreate");
|
|
expect(documents[2].spec.template.spec.automountServiceAccountToken).toBe(false);
|
|
expect(documents[2].spec.template.spec.hostNetwork).toBe(spec.gitMirror.repositoryReconciler.hostNetwork);
|
|
expect(documents[2].spec.template.spec.dnsPolicy).toBe(spec.gitMirror.repositoryReconciler.dnsPolicy);
|
|
expect(documents[2].spec.template.spec.containers[0].readinessProbe.exec.command).toContain("--health");
|
|
const authVolume = documents[2].spec.template.spec.volumes.find((item: any) => item.name === "git-auth");
|
|
expect(authVolume.projected.sources).toHaveLength(2);
|
|
expect(authVolume.projected.sources[1].secret).toEqual({
|
|
name: "gitea-github-sync-secrets",
|
|
items: [{ key: "github-token", path: "token", mode: 256 }],
|
|
});
|
|
expect(JSON.stringify(authVolume)).not.toContain("gitea-username");
|
|
expect(JSON.stringify(authVolume)).not.toContain("gitea-password");
|
|
expect(JSON.stringify(authVolume)).not.toContain("github-webhook-secret");
|
|
expect(documents[2].spec.template.spec.volumes.some((item: any) => item.name === "git-ssh")).toBe(false);
|
|
expect(renderAgentRunManagedRepositoryReconcilerFragment("JD01")).toBe("");
|
|
|
|
const controllerSource = readFileSync(controllerPath, "utf8");
|
|
expect(controllerSource).not.toContain("agent_skills");
|
|
expect(controllerSource).not.toContain("pikasTech/");
|
|
expect(controllerSource).not.toContain("github.com");
|
|
});
|
|
|
|
test("registers after PaC-owned admission objects without identity or separator conflicts", () => {
|
|
const spec = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }).spec;
|
|
const desired = agentRunManagedRepositoryDesiredConfig(spec);
|
|
const fragment = renderPlatformInfraGiteaDesiredFragments("NC01");
|
|
expect(fragment).not.toContain("---\n---");
|
|
const documents = fragment
|
|
.split(/^---\s*$/mu)
|
|
.map((item) => Bun.YAML.parse(item) as any);
|
|
expect(documents.map((item) => item.kind)).toEqual([
|
|
"Role",
|
|
"RoleBinding",
|
|
"ValidatingAdmissionPolicy",
|
|
"ValidatingAdmissionPolicyBinding",
|
|
"ServiceAccount",
|
|
"ConfigMap",
|
|
"Deployment",
|
|
]);
|
|
const identities = documents.map((item) => [
|
|
item.apiVersion,
|
|
item.kind,
|
|
item.metadata.namespace ?? "_cluster",
|
|
item.metadata.name,
|
|
].join("/"));
|
|
expect(new Set(identities).size).toBe(documents.length);
|
|
|
|
const managed = documents.slice(-3);
|
|
expect(managed.map((item) => item.metadata.name)).toEqual([
|
|
spec.gitMirror.repositoryReconciler.serviceAccountName,
|
|
spec.gitMirror.repositoryReconciler.configMapName,
|
|
spec.gitMirror.repositoryReconciler.deploymentName,
|
|
]);
|
|
const config = JSON.parse(managed[1].data["config.json"]);
|
|
expect(config.desiredHash).toBe(desired.desiredHash);
|
|
expect(managed[1].metadata.annotations["unidesk.ai/managed-repository-desired-sha"]).toBe(desired.desiredHash);
|
|
expect(managed[2].metadata.annotations["unidesk.ai/managed-repository-desired-sha"]).toBe(desired.desiredHash);
|
|
expect(managed[1].metadata.annotations["unidesk.ai/managed-repository-render-sha"]).toMatch(/^[0-9a-f]{64}$/u);
|
|
expect(managed[2].metadata.annotations["unidesk.ai/managed-repository-render-sha"]).toBe(
|
|
managed[1].metadata.annotations["unidesk.ai/managed-repository-render-sha"],
|
|
);
|
|
});
|
|
|
|
test("requires every repository entry to own a matching remote", () => {
|
|
const temporary = mkdtempSync("/tmp/unidesk-agentrun-repository-config-");
|
|
try {
|
|
const source = readFileSync(new URL("../../config/agentrun.yaml", import.meta.url), "utf8");
|
|
const invalid = source.replace(
|
|
"remote: https://github.com/pikasTech/agent_skills.git",
|
|
"remote: https://github.com/pikasTech/unidesk.git",
|
|
);
|
|
const configPath = join(temporary, "agentrun.yaml");
|
|
writeFileSync(configPath, invalid);
|
|
expect(() => resolveAgentRunLaneTarget(
|
|
{ node: "NC01", lane: "nc01-v02" },
|
|
{ ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: configPath },
|
|
)).toThrow("same repository entry");
|
|
} finally {
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("fails closed on non-canonical remotes and malformed credential capabilities", () => {
|
|
const source = readFileSync(new URL("../../config/agentrun.yaml", import.meta.url), "utf8");
|
|
const canonical = "https://github.com/pikasTech/agent_skills.git";
|
|
const invalidRemotes = [
|
|
"ssh://git@ssh.github.com:443/pikasTech/agent_skills.git",
|
|
"https://user@github.com/pikasTech/agent_skills.git",
|
|
"https://github.com:443/pikasTech/agent_skills.git",
|
|
"https://github.com/pikasTech/agent_skills.git?token=forbidden",
|
|
"https://github.com/pikasTech/agent_skills.git#forbidden",
|
|
"https://example.com/pikasTech/agent_skills.git",
|
|
"https://github.com/pikasTech/unidesk.git",
|
|
];
|
|
const temporary = mkdtempSync("/tmp/unidesk-agentrun-remote-contract-");
|
|
try {
|
|
for (const [index, remote] of invalidRemotes.entries()) {
|
|
const configPath = join(temporary, `invalid-${index}.yaml`);
|
|
writeFileSync(configPath, source.replace(`remote: ${canonical}`, `remote: ${remote}`));
|
|
expect(() => resolveAgentRunLaneTarget(
|
|
{ node: "NC01", lane: "nc01-v02" },
|
|
{ ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: configPath },
|
|
)).toThrow();
|
|
}
|
|
} finally {
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
|
|
const validCapability = {
|
|
apiVersion: "unidesk.ai/v1",
|
|
kind: "GitFetchCredential",
|
|
authMode: "github-https-token",
|
|
host: "github.com",
|
|
secretRef: { namespace: "devops-infra", name: "gitea-github-sync-secrets", key: "github-token" },
|
|
};
|
|
expect(parseAgentRunGitFetchCredentialCapability(validCapability)).toEqual(validCapability);
|
|
for (const invalid of [
|
|
{ ...validCapability, kind: "Secret" },
|
|
{ ...validCapability, authMode: "ssh" },
|
|
{ ...validCapability, host: "example.com" },
|
|
{ ...validCapability, secretRef: { ...validCapability.secretRef, key: "invalid/key" } },
|
|
]) expect(() => parseAgentRunGitFetchCredentialCapability(invalid)).toThrow();
|
|
});
|
|
|
|
test("renders host networking only from the explicit reconciler fields", () => {
|
|
const temporary = mkdtempSync("/tmp/unidesk-agentrun-repository-network-");
|
|
try {
|
|
const source = readFileSync(new URL("../../config/agentrun.yaml", import.meta.url), "utf8");
|
|
const nonLoopbackProxy = source.replace(
|
|
"githubProxy:\n host: 127.0.0.1\n port: 10808",
|
|
"githubProxy:\n host: proxy.internal.example\n port: 10808",
|
|
);
|
|
const nonLoopbackPath = join(temporary, "non-loopback.yaml");
|
|
writeFileSync(nonLoopbackPath, nonLoopbackProxy);
|
|
const nonLoopbackEnv = { ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: nonLoopbackPath };
|
|
const nonLoopbackDeployment = renderAgentRunManagedRepositoryReconcilerFragment("NC01", nonLoopbackEnv)
|
|
.split(/^---\s*$/mu)
|
|
.map((item) => Bun.YAML.parse(item) as any)
|
|
.find((item) => item.kind === "Deployment");
|
|
expect(nonLoopbackDeployment.spec.template.spec.hostNetwork).toBe(true);
|
|
expect(nonLoopbackDeployment.spec.template.spec.dnsPolicy).toBe("ClusterFirstWithHostNet");
|
|
|
|
const clusterNetwork = source.replace(
|
|
"hostNetwork: true\n dnsPolicy: ClusterFirstWithHostNet",
|
|
"hostNetwork: false\n dnsPolicy: ClusterFirst",
|
|
);
|
|
const clusterNetworkPath = join(temporary, "cluster-network.yaml");
|
|
writeFileSync(clusterNetworkPath, clusterNetwork);
|
|
const clusterNetworkEnv = { ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: clusterNetworkPath };
|
|
const clusterNetworkSpec = resolveAgentRunLaneTarget(
|
|
{ node: "NC01", lane: "nc01-v02" },
|
|
clusterNetworkEnv,
|
|
).spec;
|
|
const clusterNetworkDeployment = renderAgentRunManagedRepositoryReconcilerFragment("NC01", clusterNetworkEnv)
|
|
.split(/^---\s*$/mu)
|
|
.map((item) => Bun.YAML.parse(item) as any)
|
|
.find((item) => item.kind === "Deployment");
|
|
expect(clusterNetworkDeployment.spec.template.spec.hostNetwork).toBe(false);
|
|
expect(clusterNetworkDeployment.spec.template.spec.dnsPolicy).toBe("ClusterFirst");
|
|
const defaultSpec = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }).spec;
|
|
expect(agentRunManagedRepositoryRenderHash(clusterNetworkSpec)).not.toBe(agentRunManagedRepositoryRenderHash(defaultSpec));
|
|
|
|
const invalidHostNetworkPath = join(temporary, "invalid-host-network.yaml");
|
|
writeFileSync(invalidHostNetworkPath, source.replace("dnsPolicy: ClusterFirstWithHostNet", "dnsPolicy: ClusterFirst"));
|
|
expect(() => resolveAgentRunLaneTarget(
|
|
{ node: "NC01", lane: "nc01-v02" },
|
|
{ ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: invalidHostNetworkPath },
|
|
)).toThrow("must be ClusterFirstWithHostNet when");
|
|
|
|
const invalidClusterNetworkPath = join(temporary, "invalid-cluster-network.yaml");
|
|
writeFileSync(invalidClusterNetworkPath, source.replace("hostNetwork: true", "hostNetwork: false"));
|
|
expect(() => resolveAgentRunLaneTarget(
|
|
{ node: "NC01", lane: "nc01-v02" },
|
|
{ ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: invalidClusterNetworkPath },
|
|
)).toThrow("must not be ClusterFirstWithHostNet when");
|
|
} finally {
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("materializes source refs, exposes health, and retains undeclared cache content", () => {
|
|
const temporary = mkdtempSync("/tmp/unidesk-agentrun-managed-repository-");
|
|
try {
|
|
const work = join(temporary, "work");
|
|
const remote = join(temporary, "remote.git");
|
|
const cache = join(temporary, "cache");
|
|
const auth = join(temporary, "auth");
|
|
const fakeBin = join(temporary, "bin");
|
|
const gitObservationPath = join(temporary, "git-observations.jsonl");
|
|
const tokenPath = join(auth, "token");
|
|
const fixtureAskPassPath = join(auth, "git-askpass.mjs");
|
|
mkdirSync(work, { recursive: true });
|
|
mkdirSync(cache, { recursive: true });
|
|
mkdirSync(auth, { recursive: true });
|
|
mkdirSync(fakeBin, { recursive: true });
|
|
git(["init", work]);
|
|
git(["-C", work, "config", "user.name", "UniDesk fixture"]);
|
|
git(["-C", work, "config", "user.email", "fixture@unidesk.invalid"]);
|
|
writeFileSync(join(work, "README.md"), "managed repository fixture\n");
|
|
git(["-C", work, "add", "README.md"]);
|
|
git(["-C", work, "commit", "-m", "fixture"]);
|
|
git(["-C", work, "branch", "-M", "main"]);
|
|
git(["clone", "--bare", work, remote]);
|
|
const fixtureToken = "github_pat_fixture_not_for_logs";
|
|
writeFileSync(tokenPath, `${fixtureToken}\n`);
|
|
copyFileSync(askPassPath, fixtureAskPassPath);
|
|
chmodSync(fixtureAskPassPath, 0o555);
|
|
|
|
const retainedPath = join(cache, "undeclared", "retained.git");
|
|
mkdirSync(retainedPath, { recursive: true });
|
|
writeFileSync(join(retainedPath, "marker"), "retain\n");
|
|
|
|
const remoteUrl = "https://github.com/acme/example.git";
|
|
writeGitShim({
|
|
path: join(fakeBin, "git"),
|
|
realGit: Bun.which("git")!,
|
|
observationPath: gitObservationPath,
|
|
remoteUrl,
|
|
replacementRemoteUrl: `file://${remote}`,
|
|
});
|
|
const repository = {
|
|
key: "fixture",
|
|
repository: "acme/example",
|
|
remote: remoteUrl,
|
|
sourceBranch: "main",
|
|
desiredFingerprint: sha256(["fixture", "acme/example", remoteUrl, "main"].join("\0")),
|
|
};
|
|
const desired = {
|
|
schemaVersion: 1,
|
|
kind: "AgentRunManagedRepositoryReconciler",
|
|
nodeId: "fixture-node",
|
|
lane: "fixture-lane",
|
|
cacheMountPath: cache,
|
|
stateDirectory: ".managed-state",
|
|
reconcileIntervalMs: 1000,
|
|
fetchTimeoutMs: 10000,
|
|
shutdownGraceMs: 1000,
|
|
maxConcurrentRepositories: 1,
|
|
retry: { maxAttempts: 1, initialDelayMs: 10, maxDelayMs: 10 },
|
|
freshness: { maxAgeMs: 60000 },
|
|
lifecycle: { undeclaredRepositoryPolicy: "retain", managedRefs: "source-branch-only" },
|
|
repositories: [repository],
|
|
remoteAuth: {
|
|
configRef: "config/platform-infra/gitea.yaml#sourceAuthority.credentials.github.gitFetchCredential",
|
|
capabilitySha256: "a".repeat(64),
|
|
authMode: "github-https-token",
|
|
host: "github.com",
|
|
tokenFile: tokenPath,
|
|
askPassFile: fixtureAskPassPath,
|
|
proxy: { host: "127.0.0.1", port: 10808 },
|
|
},
|
|
};
|
|
const config = { ...desired, desiredHash: sha256(JSON.stringify(desired)) };
|
|
const configPath = join(temporary, "config.json");
|
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
|
|
const once = Bun.spawnSync(["node", controllerPath, "--once", "--config", configPath], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: inheritedHostileGitEnvironment(fakeBin, fixtureToken),
|
|
});
|
|
expect(once.exitCode, once.stderr.toString()).toBe(0);
|
|
expect(`${once.stdout.toString()}\n${once.stderr.toString()}`).not.toContain(fixtureToken);
|
|
const onceStatus = JSON.parse(once.stdout.toString());
|
|
expect(onceStatus.ready).toBe(true);
|
|
expect(onceStatus.repositories[0].remoteFingerprint).toBe(`sha256:${sha256(remoteUrl)}`);
|
|
expect(existsSync(join(retainedPath, "marker"))).toBe(true);
|
|
const gitObservations = readFileSync(gitObservationPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line));
|
|
expect(gitObservations.some((item) => item.args.includes(remoteUrl))).toBe(true);
|
|
expect(gitObservations.every((item) => item.inheritedGitSsh === false)).toBe(true);
|
|
expect(gitObservations.every((item) => item.inheritedGitConfig === false)).toBe(true);
|
|
expect(gitObservations.every((item) => item.inheritedGithubToken === false)).toBe(true);
|
|
expect(gitObservations.every((item) => item.askPassConfigured === true)).toBe(true);
|
|
expect(gitObservations.every((item) => item.tlsVerifyForced === true)).toBe(true);
|
|
expect(readFileSync(gitObservationPath, "utf8")).not.toContain(fixtureToken);
|
|
|
|
const mirroredCommit = git([
|
|
"--git-dir",
|
|
join(cache, "acme", "example.git"),
|
|
"rev-parse",
|
|
"--verify",
|
|
"refs/heads/main^{commit}",
|
|
]).trim();
|
|
expect(mirroredCommit).toMatch(/^[0-9a-f]{40}$/u);
|
|
expect(readFileSync(join(cache, "acme", "example.git", "config"), "utf8")).not.toContain(fixtureToken);
|
|
expect(readFileSync(join(cache, ".managed-state", "status.json"), "utf8")).not.toContain(fixtureToken);
|
|
git([
|
|
"--git-dir",
|
|
join(cache, "acme", "example.git"),
|
|
"update-ref",
|
|
"refs/heads/retained-gitops",
|
|
mirroredCommit,
|
|
]);
|
|
const secondOnce = Bun.spawnSync(["node", controllerPath, "--once", "--config", configPath], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: inheritedHostileGitEnvironment(fakeBin, fixtureToken),
|
|
});
|
|
expect(secondOnce.exitCode, secondOnce.stderr.toString()).toBe(0);
|
|
expect(git([
|
|
"--git-dir",
|
|
join(cache, "acme", "example.git"),
|
|
"rev-parse",
|
|
"--verify",
|
|
"refs/heads/retained-gitops^{commit}",
|
|
])).toBe(mirroredCommit);
|
|
expect(git([
|
|
"--git-dir",
|
|
join(cache, "acme", "example.git"),
|
|
"for-each-ref",
|
|
"--format=%(refname)",
|
|
"refs/agentrun-managed",
|
|
])).toBe("");
|
|
|
|
const health = Bun.spawnSync(["node", controllerPath, "--health", "--config", configPath], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: inheritedHostileGitEnvironment(fakeBin, fixtureToken),
|
|
});
|
|
expect(health.exitCode, health.stderr.toString()).toBe(0);
|
|
const healthStatus = JSON.parse(health.stdout.toString());
|
|
expect(healthStatus.ready).toBe(true);
|
|
expect(healthStatus.repositories[0]).toMatchObject({
|
|
refPresent: true,
|
|
fresh: true,
|
|
commit: mirroredCommit,
|
|
remoteFingerprintAligned: true,
|
|
});
|
|
} finally {
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("keeps readiness false and skips Git when the projected credential is missing or empty", () => {
|
|
const temporary = mkdtempSync("/tmp/unidesk-agentrun-managed-repository-credential-");
|
|
try {
|
|
const cache = join(temporary, "cache");
|
|
const auth = join(temporary, "auth");
|
|
const fakeBin = join(temporary, "bin");
|
|
const gitMarker = join(temporary, "git-invoked");
|
|
const tokenPath = join(auth, "token");
|
|
const fixtureAskPassPath = join(auth, "git-askpass.mjs");
|
|
mkdirSync(cache, { recursive: true });
|
|
mkdirSync(auth, { recursive: true });
|
|
mkdirSync(fakeBin, { recursive: true });
|
|
copyFileSync(askPassPath, fixtureAskPassPath);
|
|
chmodSync(fixtureAskPassPath, 0o555);
|
|
writeFileSync(join(fakeBin, "git"), `#!/bin/sh\ntouch ${JSON.stringify(gitMarker)}\nexit 99\n`);
|
|
chmodSync(join(fakeBin, "git"), 0o755);
|
|
|
|
const configPath = join(temporary, "config.json");
|
|
writeControllerConfig(configPath, { cache, tokenPath, askPassFile: fixtureAskPassPath });
|
|
writeFileSync(tokenPath, "\n");
|
|
const empty = Bun.spawnSync(["node", controllerPath, "--once", "--config", configPath], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: inheritedHostileGitEnvironment(fakeBin, "inherited-token-must-not-leak"),
|
|
});
|
|
expect(empty.exitCode).toBe(1);
|
|
const emptyStatus = lastJsonLine(empty.stdout.toString());
|
|
expect(emptyStatus).toMatchObject({
|
|
ready: false,
|
|
credential: { ready: false, errorCode: "credential-empty", valuesPrinted: false },
|
|
repositories: [{ lastAttemptOk: false, errorCode: "credential-empty" }],
|
|
valuesPrinted: false,
|
|
});
|
|
expect(existsSync(gitMarker)).toBe(false);
|
|
|
|
rmSync(tokenPath);
|
|
const missing = Bun.spawnSync(["node", controllerPath, "--health", "--config", configPath], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: inheritedHostileGitEnvironment(fakeBin, "inherited-token-must-not-leak"),
|
|
});
|
|
expect(missing.exitCode).toBe(1);
|
|
expect(JSON.parse(missing.stdout.toString())).toMatchObject({
|
|
ready: false,
|
|
credential: { ready: false, errorCode: "credential-file-missing", valuesPrinted: false },
|
|
valuesPrinted: false,
|
|
});
|
|
expect(existsSync(gitMarker)).toBe(false);
|
|
} finally {
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("redacts authentication failures from logs, status, and repository config", () => {
|
|
const temporary = mkdtempSync("/tmp/unidesk-agentrun-managed-repository-redaction-");
|
|
try {
|
|
const work = join(temporary, "work");
|
|
const remote = join(temporary, "remote.git");
|
|
const cache = join(temporary, "cache");
|
|
const auth = join(temporary, "auth");
|
|
const fakeBin = join(temporary, "bin");
|
|
const tokenPath = join(auth, "token");
|
|
const fixtureAskPassPath = join(auth, "git-askpass.mjs");
|
|
const gitObservationPath = join(temporary, "git-observations.jsonl");
|
|
const remoteUrl = "https://github.com/acme/example.git";
|
|
const sensitive = "github_pat_sensitive_authentication_failure";
|
|
mkdirSync(work, { recursive: true });
|
|
mkdirSync(cache, { recursive: true });
|
|
mkdirSync(auth, { recursive: true });
|
|
mkdirSync(fakeBin, { recursive: true });
|
|
git(["init", work]);
|
|
git(["clone", "--bare", work, remote]);
|
|
writeFileSync(tokenPath, `${sensitive}\n`);
|
|
copyFileSync(askPassPath, fixtureAskPassPath);
|
|
chmodSync(fixtureAskPassPath, 0o555);
|
|
writeGitShim({
|
|
path: join(fakeBin, "git"),
|
|
realGit: Bun.which("git")!,
|
|
observationPath: gitObservationPath,
|
|
remoteUrl,
|
|
replacementRemoteUrl: `file://${remote}`,
|
|
failFetchMessage: `authentication failed for ${sensitive}`,
|
|
});
|
|
const configPath = join(temporary, "config.json");
|
|
writeControllerConfig(configPath, { cache, tokenPath, askPassFile: fixtureAskPassPath });
|
|
const once = Bun.spawnSync(["node", controllerPath, "--once", "--config", configPath], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: inheritedHostileGitEnvironment(fakeBin, sensitive),
|
|
});
|
|
expect(once.exitCode).toBe(1);
|
|
const combined = `${once.stdout.toString()}\n${once.stderr.toString()}`;
|
|
expect(combined).not.toContain(sensitive);
|
|
const status = lastJsonLine(once.stdout.toString());
|
|
expect(status.repositories[0]).toMatchObject({
|
|
lastAttemptOk: false,
|
|
errorCode: "authentication-failed",
|
|
});
|
|
expect(status.repositories[0].errorFingerprint).toMatch(/^[0-9a-f]{16}$/u);
|
|
expect(readFileSync(gitObservationPath, "utf8")).not.toContain(sensitive);
|
|
expect(readFileSync(join(cache, ".managed-state", "status.json"), "utf8")).not.toContain(sensitive);
|
|
expect(existsSync(join(cache, "acme", "example.git"))).toBe(false);
|
|
} finally {
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("keeps the legacy default summary contract and bounds the final CLI envelope", () => {
|
|
const target = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" });
|
|
const spec = target.spec;
|
|
const authority = resolveCicdDeliveryAuthority({
|
|
node: spec.nodeId,
|
|
lane: spec.lane,
|
|
sourceRepository: spec.source.repository,
|
|
sourceBranch: spec.source.branch,
|
|
declaredSourceAuthorityMode: spec.source.sourceAuthority?.mode ?? null,
|
|
declaredConfigRef: target.configPath,
|
|
});
|
|
expect(authority.kind).toBe("pac-pr-merge");
|
|
const defaultRepositoryLimit = spec.gitMirror.repositoryReconciler.status.defaultRepositoryLimit;
|
|
const fixtureRepositoryTotal = defaultRepositoryLimit + 2;
|
|
const repositories = Array.from({ length: fixtureRepositoryTotal }, (_, index) => ({
|
|
key: `repo-${index}`,
|
|
repository: `acme/repo-${index}`,
|
|
sourceBranch: "main",
|
|
cacheCommit: String(index).repeat(40),
|
|
cacheRefPresent: true,
|
|
servedCommit: String(index).repeat(40),
|
|
servedRefPresent: true,
|
|
refAligned: true,
|
|
remoteFingerprintAligned: true,
|
|
fresh: true,
|
|
}));
|
|
const summary = {
|
|
ok: true,
|
|
repository: "pikasTech/agentrun",
|
|
sourceBranch: "v0.2",
|
|
gitopsBranch: "nc01-v0.2-gitops",
|
|
sourceCommit: "a".repeat(40),
|
|
gitopsCommit: "b".repeat(40),
|
|
controller: {
|
|
deploymentReady: true,
|
|
configRendered: true,
|
|
workloadRendered: true,
|
|
statusAligned: true,
|
|
heartbeatFresh: true,
|
|
},
|
|
repositories,
|
|
firstDrift: null,
|
|
valuesPrinted: false,
|
|
};
|
|
const observation = {
|
|
ok: true,
|
|
summary,
|
|
raw: JSON.stringify(summary),
|
|
result: { exitCode: 0, stdout: JSON.stringify(summary), stderr: "" },
|
|
} as any;
|
|
const baseOptions = { node: "NC01", lane: "nc01-v02" };
|
|
const compact = renderGitMirrorStatusEnvelope(
|
|
target,
|
|
authority,
|
|
observation,
|
|
{ ...baseOptions, full: false, raw: false },
|
|
) as any;
|
|
expect(compact.summary).toMatchObject({
|
|
repository: "pikasTech/agentrun",
|
|
sourceBranch: "v0.2",
|
|
gitopsBranch: "nc01-v0.2-gitops",
|
|
sourceCommit: "a".repeat(40),
|
|
gitopsCommit: "b".repeat(40),
|
|
});
|
|
expect(compact.summary.repositories).toHaveLength(defaultRepositoryLimit);
|
|
expect(compact.summary.repositoryView).toEqual({
|
|
limit: defaultRepositoryLimit,
|
|
total: fixtureRepositoryTotal,
|
|
shown: defaultRepositoryLimit,
|
|
omitted: fixtureRepositoryTotal - defaultRepositoryLimit,
|
|
});
|
|
expect(compact.target.repositoryView).toEqual({
|
|
limit: defaultRepositoryLimit,
|
|
total: spec.gitMirror.repositories.length,
|
|
shown: spec.gitMirror.repositories.length,
|
|
omitted: 0,
|
|
});
|
|
expect(compact.probe.stdoutTailOmitted).toBe(true);
|
|
expect(compact.disclosure.probeTailOmitted).toBe(true);
|
|
expect(compact.next).toMatchObject({ status: expect.any(String), history: expect.any(String) });
|
|
expect(compact.next.sync).toBeUndefined();
|
|
|
|
for (const options of [
|
|
{ ...baseOptions, full: true, raw: false },
|
|
{ ...baseOptions, full: false, raw: true },
|
|
]) {
|
|
const expanded = renderGitMirrorStatusEnvelope(target, authority, observation, options) as any;
|
|
expect(expanded.summary.repositories).toHaveLength(fixtureRepositoryTotal);
|
|
expect(expanded.summary.repositoryView).toEqual({
|
|
limit: null,
|
|
total: fixtureRepositoryTotal,
|
|
shown: fixtureRepositoryTotal,
|
|
omitted: 0,
|
|
});
|
|
expect(expanded.target.repositories).toHaveLength(spec.gitMirror.repositories.length);
|
|
expect(expanded.target.repositoryView).toEqual({
|
|
limit: null,
|
|
total: spec.gitMirror.repositories.length,
|
|
shown: spec.gitMirror.repositories.length,
|
|
omitted: 0,
|
|
});
|
|
expect(expanded.probe.stdoutTailOmitted).toBe(!options.raw);
|
|
expect(expanded.probe.stdoutTail).toBe(options.raw ? JSON.stringify(summary) : undefined);
|
|
expect(expanded.disclosure.probeTailOmitted).toBe(!options.raw);
|
|
}
|
|
});
|
|
|
|
test("keeps legacy main-repository fields and bounds per-repository probes", () => {
|
|
const spec = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }).spec;
|
|
const script = yamlLaneGitMirrorStatusScript(spec);
|
|
expect(script).toContain("'repository': os.environ.get('REPOSITORY')");
|
|
expect(script).toContain("'sourceCommit': legacy_ref(");
|
|
expect(script).toContain("'gitopsCommit': legacy_ref(");
|
|
expect(script.match(/timeout 20 kubectl/gu)?.length).toBe(spec.gitMirror.repositories.length);
|
|
expect(script.match(/timeout 20 kubectl[^\n]+ &/gu)?.length).toBe(spec.gitMirror.repositories.length);
|
|
expect(script).toContain("remoteFingerprintAligned");
|
|
});
|
|
});
|
|
|
|
function git(args: string[]): string {
|
|
const result = Bun.spawnSync(["git", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
if (result.exitCode !== 0) throw new Error(`git ${args[0]} failed: ${result.stderr.toString()}`);
|
|
return result.stdout.toString().trim();
|
|
}
|
|
|
|
function sha256(value: string): string {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
function writeControllerConfig(path: string, input: { cache: string; tokenPath: string; askPassFile: string }): void {
|
|
const remote = "https://github.com/acme/example.git";
|
|
const repository = {
|
|
key: "fixture",
|
|
repository: "acme/example",
|
|
remote,
|
|
sourceBranch: "main",
|
|
desiredFingerprint: sha256(["fixture", "acme/example", remote, "main"].join("\0")),
|
|
};
|
|
const desired = {
|
|
schemaVersion: 1,
|
|
kind: "AgentRunManagedRepositoryReconciler",
|
|
nodeId: "fixture-node",
|
|
lane: "fixture-lane",
|
|
cacheMountPath: input.cache,
|
|
stateDirectory: ".managed-state",
|
|
reconcileIntervalMs: 1000,
|
|
fetchTimeoutMs: 10000,
|
|
shutdownGraceMs: 1000,
|
|
maxConcurrentRepositories: 1,
|
|
retry: { maxAttempts: 1, initialDelayMs: 10, maxDelayMs: 10 },
|
|
freshness: { maxAgeMs: 60000 },
|
|
lifecycle: { undeclaredRepositoryPolicy: "retain", managedRefs: "source-branch-only" },
|
|
repositories: [repository],
|
|
remoteAuth: {
|
|
configRef: "config/platform-infra/gitea.yaml#sourceAuthority.credentials.github.gitFetchCredential",
|
|
capabilitySha256: "a".repeat(64),
|
|
authMode: "github-https-token",
|
|
host: "github.com",
|
|
tokenFile: input.tokenPath,
|
|
askPassFile: input.askPassFile,
|
|
proxy: { host: "127.0.0.1", port: 10808 },
|
|
},
|
|
};
|
|
writeFileSync(path, `${JSON.stringify({ ...desired, desiredHash: sha256(JSON.stringify(desired)) }, null, 2)}\n`);
|
|
}
|
|
|
|
function lastJsonLine(output: string): any {
|
|
const lines = output.trim().split("\n");
|
|
return JSON.parse(lines.at(-1) ?? "null");
|
|
}
|
|
|
|
function inheritedHostileGitEnvironment(fakeBin: string, fixtureToken: string): NodeJS.ProcessEnv {
|
|
return {
|
|
...process.env,
|
|
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
|
GIT_SSH: "/tmp/inherited-git-ssh",
|
|
GIT_SSH_COMMAND: "inherited-ssh-command",
|
|
GIT_CONFIG: "/tmp/inherited-git-config",
|
|
GIT_CONFIG_COUNT: "1",
|
|
GIT_CONFIG_KEY_0: "credential.helper",
|
|
GIT_CONFIG_VALUE_0: "!inherited-credential-helper",
|
|
GIT_SSL_NO_VERIFY: "1",
|
|
GH_TOKEN: fixtureToken,
|
|
GITHUB_TOKEN: fixtureToken,
|
|
};
|
|
}
|
|
|
|
function writeGitShim(input: {
|
|
path: string;
|
|
realGit: string;
|
|
observationPath: string;
|
|
remoteUrl: string;
|
|
replacementRemoteUrl: string;
|
|
failFetchMessage?: string;
|
|
}): void {
|
|
writeFileSync(input.path, `#!/usr/bin/env node
|
|
import { appendFileSync } from "node:fs";
|
|
import { spawnSync } from "node:child_process";
|
|
const args = process.argv.slice(2);
|
|
appendFileSync(${JSON.stringify(input.observationPath)}, JSON.stringify({
|
|
args,
|
|
inheritedGitSsh: Object.hasOwn(process.env, "GIT_SSH") || Object.hasOwn(process.env, "GIT_SSH_COMMAND"),
|
|
inheritedGitConfig: Object.hasOwn(process.env, "GIT_CONFIG") || Object.hasOwn(process.env, "GIT_CONFIG_COUNT") || Object.keys(process.env).some((key) => /^GIT_CONFIG_(?:KEY|VALUE)_/u.test(key)),
|
|
inheritedGithubToken: Object.hasOwn(process.env, "GH_TOKEN") || Object.hasOwn(process.env, "GITHUB_TOKEN"),
|
|
askPassConfigured: typeof process.env.GIT_ASKPASS === "string" && process.env.GIT_ASKPASS.length > 0 && process.env.GIT_ASKPASS_REQUIRE === "force",
|
|
tlsVerifyForced: process.env.GIT_SSL_NO_VERIFY === "0" && args.includes("http.sslVerify=true"),
|
|
}) + "\\n");
|
|
if (args.includes("fetch") && ${JSON.stringify(input.failFetchMessage ?? null)} !== null) {
|
|
process.stderr.write(${JSON.stringify(input.failFetchMessage ?? "")} + "\\n");
|
|
process.exit(1);
|
|
}
|
|
const mapped = args.map((arg) => arg === ${JSON.stringify(input.remoteUrl)} ? ${JSON.stringify(input.replacementRemoteUrl)} : arg);
|
|
const result = spawnSync(${JSON.stringify(input.realGit)}, mapped, { env: process.env, stdio: "inherit" });
|
|
process.exit(result.status ?? 1);
|
|
`);
|
|
chmodSync(input.path, 0o755);
|
|
}
|