feat: add aipod spec Artificer assembly
This commit is contained in:
@@ -24,7 +24,13 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
secretRef: { name: "agentrun-v01-tool-unidesk-ssh", keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
|
||||
projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" },
|
||||
}];
|
||||
const combinedToolCredentials = [...githubToolCredentials, ...unideskSshToolCredentials];
|
||||
const githubSshToolCredentials = [{
|
||||
tool: "github",
|
||||
purpose: "github-ssh",
|
||||
secretRef: { name: "agentrun-v01-tool-github-ssh", keys: ["id_ed25519", "known_hosts", "config"] },
|
||||
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
|
||||
}];
|
||||
const combinedToolCredentials = [...githubToolCredentials, ...unideskSshToolCredentials, ...githubSshToolCredentials];
|
||||
const item = await createRunWithCommand(client, { ...context, toolCredentials: combinedToolCredentials }, "job smoke", "selftest-job-render", 15_000);
|
||||
const rendered = renderRunnerJobDryRun({
|
||||
run: await client.get(`/api/v1/runs/${item.runId}`) as RunRecord,
|
||||
@@ -42,6 +48,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assertRunnerJobUsesWritableCodexHome(rendered.manifest as JsonRecord, context.codexHome, "codex-0", "/var/run/agentrun/secrets/codex-0");
|
||||
assertRunnerJobUsesToolCredential(rendered, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN");
|
||||
assertRunnerJobUsesToolCredential(rendered, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN");
|
||||
assertRunnerJobUsesToolCredentialVolume(rendered, "agentrun-v01-tool-github-ssh", "/home/agentrun/.ssh", ["id_ed25519", "known_hosts", "config"]);
|
||||
assertRunnerJobUsesG14EgressProxy(rendered.manifest as JsonRecord);
|
||||
assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "AGENTRUN_CODEX_SHELL_SANDBOX"), "danger-full-access");
|
||||
assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "HWLAB_API_KEY"), "REDACTED");
|
||||
@@ -211,6 +218,7 @@ process.exit(1);
|
||||
assertRunnerJobUsesTransientEnvSecret(manifest, "UNIDESK_MAIN_SERVER_IP", String(transientEnvSecret.name));
|
||||
assertRunnerJobUsesToolCredential({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN");
|
||||
assertRunnerJobUsesToolCredential({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN");
|
||||
assertRunnerJobUsesToolCredentialVolume({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "agentrun-v01-tool-github-ssh", "/home/agentrun/.ssh", ["id_ed25519", "known_hosts", "config"]);
|
||||
assertNoSecretLeak(created);
|
||||
const defaultEndpointJobItem = await createRunWithCommand(jobClient, { ...context, toolCredentials: unideskSshToolCredentials }, "job create unidesk ssh default endpoint", "selftest-job-create-unidesk-ssh-default-endpoint", 15_000);
|
||||
const defaultEndpointCreated = await jobClient.post(`/api/v1/runs/${defaultEndpointJobItem.runId}/runner-jobs`, {
|
||||
@@ -277,7 +285,7 @@ process.exit(1);
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01");
|
||||
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/sessions");
|
||||
assert.equal(envMap.get("AGENTRUN_CODEX_ROLLOUT_SUBDIR"), "sessions");
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-codex-shell-sandbox-env", "runner-k8s-job-g14-egress-proxy-env", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-transient-env-secretref", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-codex-shell-sandbox-env", "runner-k8s-job-g14-egress-proxy-env", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-transient-env-secretref", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-tool-credential-volume", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
@@ -353,6 +361,35 @@ function assertRunnerJobUsesToolCredential(rendered: JsonRecord, envName: string
|
||||
assert.equal(summaryEntry.valuesPrinted, false);
|
||||
}
|
||||
|
||||
function assertRunnerJobUsesToolCredentialVolume(rendered: JsonRecord, secretName: string, mountPath: string, secretKeys: string[]): void {
|
||||
const manifest = rendered.manifest as JsonRecord;
|
||||
const spec = manifest.spec as JsonRecord;
|
||||
const template = spec.template as JsonRecord;
|
||||
const podSpec = template.spec as JsonRecord;
|
||||
const containers = podSpec.containers as JsonRecord[];
|
||||
const runner = containers[0] as JsonRecord;
|
||||
const mounts = runner.volumeMounts as JsonRecord[];
|
||||
const mount = mounts.find((item) => item.mountPath === mountPath) as JsonRecord | undefined;
|
||||
assert.ok(mount, `${mountPath} tool credential volume mount should be present`);
|
||||
assert.equal(mount.readOnly, true);
|
||||
const volumes = podSpec.volumes as JsonRecord[];
|
||||
const volume = volumes.find((item) => item.name === mount.name) as JsonRecord | undefined;
|
||||
assert.ok(volume, `${mountPath} tool credential volume should be present`);
|
||||
const secret = volume.secret as JsonRecord;
|
||||
assert.equal(secret.secretName, secretName);
|
||||
assert.deepEqual((secret.items as JsonRecord[]).map((item) => item.key), secretKeys);
|
||||
|
||||
const summary = rendered.toolCredentials as JsonRecord;
|
||||
assert.equal(summary.valuesPrinted, false);
|
||||
const items = summary.items as JsonRecord[];
|
||||
const summaryEntry = items.find((item) => {
|
||||
const projection = item.projection as JsonRecord;
|
||||
return item.name === secretName && projection.kind === "volume" && projection.mountPath === mountPath;
|
||||
});
|
||||
assert.ok(summaryEntry, `${mountPath} tool credential summary should include its SecretRef and volume projection`);
|
||||
assert.equal(summaryEntry.valuesPrinted, false);
|
||||
}
|
||||
|
||||
function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCodexHome: string, volumeName: string, projectionPath: string): void {
|
||||
const spec = manifest.spec as JsonRecord;
|
||||
const template = spec.template as JsonRecord;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chmod, readFile, writeFile } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { ManagerClient } from "../../mgr/client.js";
|
||||
import { startManagerServer } from "../../mgr/server.js";
|
||||
import { MemoryAgentRunStore } from "../../mgr/store.js";
|
||||
import type { JsonRecord } from "../../common/types.js";
|
||||
import { assertNoSecretLeak, type SelfTestCase } from "../harness.js";
|
||||
|
||||
const privateKeyHeader = ["-----BEGIN OPENSSH", "PRIVATE KEY-----"].join(" ");
|
||||
const privateKeyFooter = ["-----END OPENSSH", "PRIVATE KEY-----"].join(" ");
|
||||
const privateKey = `${privateKeyHeader}
|
||||
selftest-private-key-material
|
||||
${privateKeyFooter}
|
||||
`;
|
||||
const knownHosts = "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIselftestKnownHostsKey\n";
|
||||
const sshConfig = "Host github.com\n HostName ssh.github.com\n User git\n Port 443\n IdentityFile ~/.ssh/id_ed25519\n";
|
||||
|
||||
const selfTest: SelfTestCase = async (context) => {
|
||||
const fakeKubectl = path.join(context.tmp, "fake-tool-kubectl.js");
|
||||
const stateDir = path.join(context.tmp, "tool-secret-state");
|
||||
const createdSecretPath = path.join(context.tmp, "tool-secret-create.json");
|
||||
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
||||
import { mkdirSync } from "node:fs";
|
||||
const args = Bun.argv.slice(2);
|
||||
const stateDir = ${JSON.stringify(stateDir)};
|
||||
const statePath = (name) => stateDir + "/" + name + ".json";
|
||||
mkdirSync(stateDir, { recursive: true });
|
||||
const readStdin = async () => {
|
||||
const chunks = [];
|
||||
for await (const chunk of Bun.stdin.stream()) chunks.push(Buffer.from(chunk));
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
};
|
||||
if (args[0] === "get" && args[1] === "secret") {
|
||||
const name = args[2];
|
||||
const file = Bun.file(statePath(name));
|
||||
if (!(await file.exists())) {
|
||||
console.error('Error from server (NotFound): secrets "' + name + '" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(await file.text());
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === "replace") {
|
||||
const text = await readStdin();
|
||||
const manifest = JSON.parse(text);
|
||||
const file = Bun.file(statePath(manifest.metadata.name));
|
||||
if (!(await file.exists())) {
|
||||
console.error('Error from server (NotFound): secrets "' + manifest.metadata.name + '" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
const next = { ...manifest, metadata: { ...(manifest.metadata ?? {}), resourceVersion: "rv-replaced" } };
|
||||
await Bun.write(statePath(manifest.metadata.name), JSON.stringify(next));
|
||||
console.log(JSON.stringify(next));
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === "create") {
|
||||
const text = await readStdin();
|
||||
const manifest = JSON.parse(text);
|
||||
await Bun.write(${JSON.stringify(createdSecretPath)}, JSON.stringify({ args, manifest }, null, 2));
|
||||
const next = { ...manifest, metadata: { ...(manifest.metadata ?? {}), resourceVersion: "rv-created" } };
|
||||
await Bun.write(statePath(manifest.metadata.name), JSON.stringify(next));
|
||||
console.log(JSON.stringify(next));
|
||||
process.exit(0);
|
||||
}
|
||||
console.error("unsupported fake kubectl args: " + JSON.stringify(args));
|
||||
process.exit(1);
|
||||
`);
|
||||
await chmod(fakeKubectl, 0o755);
|
||||
|
||||
const server = await startManagerServer({
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
sourceCommit: "self-test",
|
||||
store: new MemoryAgentRunStore(),
|
||||
toolCredentialOptions: { namespace: "agentrun-v01", kubectlCommand: fakeKubectl },
|
||||
});
|
||||
try {
|
||||
const client = new ManagerClient(server.baseUrl);
|
||||
const missing = await client.get("/api/v1/tool-credentials") as JsonRecord;
|
||||
assert.equal(missing.action, "tool-credential-list");
|
||||
assert.equal(missing.count, 2);
|
||||
assert.equal(JSON.stringify(missing).includes(privateKey), false);
|
||||
const githubMissing = ((missing.items as JsonRecord[]) ?? []).find((item) => item.name === "github-ssh") as JsonRecord | undefined;
|
||||
assert.equal(githubMissing?.configured, false);
|
||||
assert.equal(githubMissing?.failureKind, "secret-unavailable");
|
||||
|
||||
const updated = await client.put("/api/v1/tool-credentials/github-ssh/credential", { privateKey, knownHosts, config: sshConfig }) as JsonRecord;
|
||||
assert.equal(updated.action, "tool-credential-github-ssh-updated");
|
||||
assert.equal(updated.configured, true);
|
||||
assert.equal(updated.resourceVersion, "rv-created");
|
||||
assertNoCredentialLeak(updated);
|
||||
assertNoSecretLeak(updated);
|
||||
const created = JSON.parse(await readFile(createdSecretPath, "utf8")) as JsonRecord;
|
||||
const manifest = created.manifest as JsonRecord;
|
||||
assert.equal(((manifest.metadata as JsonRecord).name), "agentrun-v01-tool-github-ssh");
|
||||
const data = manifest.data as JsonRecord;
|
||||
assert.equal(Buffer.from(String(data.id_ed25519), "base64").toString("utf8"), privateKey);
|
||||
assert.equal(Buffer.from(String(data.known_hosts), "base64").toString("utf8"), knownHosts);
|
||||
assert.equal(Buffer.from(String(data.config), "base64").toString("utf8"), sshConfig);
|
||||
|
||||
const shown = await client.get("/api/v1/tool-credentials/github-ssh") as JsonRecord;
|
||||
assert.equal(shown.configured, true);
|
||||
assert.deepEqual((shown.keyPresence as JsonRecord), { id_ed25519: true, known_hosts: true, config: true });
|
||||
assertNoCredentialLeak(shown);
|
||||
|
||||
const privateKeyFile = path.join(context.tmp, "id_ed25519");
|
||||
const knownHostsFile = path.join(context.tmp, "known_hosts");
|
||||
await writeFile(privateKeyFile, privateKey);
|
||||
await writeFile(knownHostsFile, knownHosts);
|
||||
const dryRun = await runCliJson(context, ["tool-credentials", "set-github-ssh", "--private-key-file", privateKeyFile, "--known-hosts-file", knownHostsFile, "--dry-run"]);
|
||||
assert.equal(dryRun.ok, true);
|
||||
assert.equal(((dryRun.data as JsonRecord).action), "tool-credential-github-ssh-plan");
|
||||
assertNoCredentialLeak(dryRun);
|
||||
return { name: "tool-credentials", tests: ["tool-credential-list-missing", "tool-credential-github-ssh-upsert-redacted", "tool-credential-cli-dry-run"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
};
|
||||
|
||||
function assertNoCredentialLeak(value: unknown): void {
|
||||
const text = JSON.stringify(value);
|
||||
assert.equal(text.includes("selftest-private-key-material"), false);
|
||||
assert.equal(text.includes(privateKeyHeader), false);
|
||||
assert.equal(text.includes("AAAAC3NzaC1lZDI1NTE5AAAAIselftestKnownHostsKey"), false);
|
||||
assert.equal(text.includes("IdentityFile ~/.ssh/id_ed25519"), false);
|
||||
}
|
||||
|
||||
async function runCliJson(context: { root: string }, args: string[]): Promise<JsonRecord> {
|
||||
const proc = spawn(process.execPath, [`${context.root}/scripts/agentrun-cli.ts`, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
const [stdout, stderr, code] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr), new Promise<number | null>((resolve) => proc.on("close", resolve))]);
|
||||
assert.equal(code, 0, stderr || stdout);
|
||||
return JSON.parse(stdout) as JsonRecord;
|
||||
}
|
||||
|
||||
async function readStream(stream: NodeJS.ReadableStream): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk: Buffer | string) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
stream.on("end", resolve);
|
||||
stream.on("error", reject);
|
||||
});
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
export default selfTest;
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { ManagerClient } from "../../mgr/client.js";
|
||||
import { startManagerServer } from "../../mgr/server.js";
|
||||
import { MemoryAgentRunStore } from "../../mgr/store.js";
|
||||
import type { JsonRecord } from "../../common/types.js";
|
||||
import { resolveGitBundleFetchSource } from "../../runner/resource-bundle.js";
|
||||
import { assertNoSecretLeak, type SelfTestCase } from "../harness.js";
|
||||
|
||||
const selfTest: SelfTestCase = async (context) => {
|
||||
const server = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store: new MemoryAgentRunStore(), aipodSpecDir: path.join(context.root, "config", "aipods") });
|
||||
try {
|
||||
const client = new ManagerClient(server.baseUrl);
|
||||
const listed = await client.get("/api/v1/aipod-specs") as JsonRecord;
|
||||
assert.equal(listed.action, "aipod-spec-list");
|
||||
assert.equal((listed.items as JsonRecord[]).some((item) => item.name === "Artificer"), true);
|
||||
|
||||
const shown = await client.get("/api/v1/aipod-specs/Artificer") as JsonRecord;
|
||||
const shownItem = shown.item as JsonRecord;
|
||||
assert.equal(shownItem.backendProfile, "sub2api");
|
||||
assert.equal(((shownItem.model as JsonRecord).model), "gpt-5.5");
|
||||
assert.equal(((shownItem.resourceBundleRef as JsonRecord).gitMirror as JsonRecord).enabled, true);
|
||||
|
||||
const rendered = await client.post("/api/v1/aipod-specs/Artificer/render", { prompt: "处理 pikasTech/unidesk#245", idempotencyKey: "selftest-aipod-artificer" }) as JsonRecord;
|
||||
assert.equal(rendered.action, "aipod-spec-render");
|
||||
const task = rendered.queueTask as JsonRecord;
|
||||
assert.equal(task.backendProfile, "sub2api");
|
||||
assert.equal(task.providerId, "G14");
|
||||
assert.equal(task.idempotencyKey, "selftest-aipod-artificer");
|
||||
assert.equal(((task.payload as JsonRecord).model), "gpt-5.5");
|
||||
assert.equal((((task.payload as JsonRecord).modelConfig as JsonRecord).reasoningEffort), "xhigh");
|
||||
const policy = task.executionPolicy as JsonRecord;
|
||||
const secretScope = policy.secretScope as JsonRecord;
|
||||
const providerCredentials = secretScope.providerCredentials as JsonRecord[];
|
||||
assert.equal(providerCredentials.some((item) => item.profile === "sub2api" && ((item.secretRef as JsonRecord).name) === "agentrun-v01-provider-sub2api"), true);
|
||||
const toolCredentials = secretScope.toolCredentials as JsonRecord[];
|
||||
assert.equal(toolCredentials.some((item) => item.tool === "unidesk-ssh" && ((item.projection as JsonRecord).envName) === "UNIDESK_SSH_CLIENT_TOKEN"), true);
|
||||
assert.equal(toolCredentials.some((item) => item.tool === "github" && ((item.projection as JsonRecord).kind) === "volume" && ((item.projection as JsonRecord).mountPath) === "/home/agentrun/.ssh"), true);
|
||||
const bundle = task.resourceBundleRef as JsonRecord;
|
||||
assert.equal(((bundle.gitMirror as JsonRecord).enabled), true);
|
||||
const bundles = bundle.bundles as JsonRecord[];
|
||||
const toolBundle = bundles.find((item) => item.name === "agentrun-runner-tools") as JsonRecord | undefined;
|
||||
assert.equal(toolBundle?.repoUrl, "git@github.com:pikasTech/agentrun.git");
|
||||
assert.equal(toolBundle?.ref, "v0.1");
|
||||
assert.equal(toolBundle?.subpath, "tools");
|
||||
assert.equal(toolBundle?.targetPath, "tools");
|
||||
assert.equal((bundle.requiredSkills as JsonRecord[]).some((item) => item.name === "dad-dev"), true);
|
||||
assert.equal((bundle.requiredSkills as JsonRecord[]).some((item) => item.name === "unidesk-gh"), true);
|
||||
assertNoSecretLeak(rendered);
|
||||
|
||||
const mirrored = resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { enabled: true, baseUrl: "http://mirror.example.test/root/" }, {});
|
||||
assert.equal(mirrored.fetchRepoUrl, "http://mirror.example.test/root/pikasTech/unidesk.git");
|
||||
assert.equal(mirrored.mirrorUsed, true);
|
||||
const nonGithub = resolveGitBundleFetchSource("ssh://git@example.test/repo.git", { enabled: true, baseUrl: "http://mirror.example.test" }, {});
|
||||
assert.equal(nonGithub.fetchRepoUrl, "ssh://git@example.test/repo.git");
|
||||
assert.equal(nonGithub.mirrorUsed, false);
|
||||
|
||||
const submitPlan = await runCliJson(context, server.baseUrl, ["queue", "submit", "--aipod", "Artificer", "--prompt", "处理 pikasTech/unidesk#245", "--idempotency-key", "selftest-aipod-cli", "--dry-run"]);
|
||||
assert.equal(submitPlan.ok, true);
|
||||
assert.equal(((submitPlan.data as JsonRecord).action), "queue-submit-plan");
|
||||
assert.equal((((submitPlan.data as JsonRecord).jsonInput as JsonRecord).source), "aipod-spec");
|
||||
const request = ((submitPlan.data as JsonRecord).request as JsonRecord);
|
||||
assert.equal(((request.body as JsonRecord).idempotencyKey), "selftest-aipod-cli");
|
||||
|
||||
const help = await runCliJson(context, server.baseUrl, ["help"]);
|
||||
const commands = ((help.data as JsonRecord).commands as string[]) ?? [];
|
||||
assert.equal(commands.some((item) => item.includes("aipod-specs render <name>")), true);
|
||||
assert.equal(commands.some((item) => item.includes("queue submit --aipod <name>")), true);
|
||||
assertNoSecretLeak(submitPlan);
|
||||
return { name: "aipod-spec", tests: ["aipod-spec-artificer-yaml-render", "aipod-spec-git-mirror-url", "queue-submit-aipod-dry-run", "aipod-cli-help"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
};
|
||||
|
||||
export default selfTest;
|
||||
|
||||
async function runCliJson(context: { root: string }, managerUrl: string, args: string[]): Promise<JsonRecord> {
|
||||
const proc = spawn(process.execPath, [`${context.root}/scripts/agentrun-cli.ts`, "--manager-url", managerUrl, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
const [stdout, stderr, code] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr), new Promise<number | null>((resolve) => proc.on("close", resolve))]);
|
||||
assert.equal(code, 0, stderr || stdout);
|
||||
return JSON.parse(stdout) as JsonRecord;
|
||||
}
|
||||
|
||||
async function readStream(stream: NodeJS.ReadableStream): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk: Buffer | string) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
stream.on("end", resolve);
|
||||
stream.on("error", reject);
|
||||
});
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
const apkPackages = installedApkPackages(containerfile);
|
||||
const tran = await readFile(path.join(context.root, "tools/tran"), "utf8");
|
||||
const trans = await readFile(path.join(context.root, "tools/trans"), "utf8");
|
||||
const applyPatch = await readFile(path.join(context.root, "tools/apply_patch"), "utf8");
|
||||
|
||||
for (const packageName of requiredRunnerPackages) {
|
||||
assert.equal(apkPackages.has(packageName), true, `runner image must install ${packageName}`);
|
||||
@@ -20,16 +21,22 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
|
||||
assert.equal(tran.startsWith("#!/usr/bin/env bun\n"), true, "tools/tran must be a shebang executable discovered by gitbundle tools");
|
||||
assert.equal(trans.startsWith("#!/bin/sh\n"), true, "tools/trans must be a shebang executable discovered by gitbundle tools");
|
||||
assert.equal(applyPatch.startsWith("#!/bin/sh\n"), true, "tools/apply_patch must be a shebang helper copied with runner tools");
|
||||
assert.equal(tran.includes("UNIDESK_SSH_CLIENT_TOKEN"), true, "tools/tran must require the scoped UniDesk SSH client token");
|
||||
assert.equal(tran.includes("/ws/ssh"), true, "tools/tran must use the frontend SSH WebSocket path");
|
||||
assert.equal(tran.includes("apply-patch < patch.diff"), true, "tools/tran help must advertise runner-side apply-patch");
|
||||
|
||||
const help = await execFileAsync(path.join(context.root, "tools/tran"), ["--help"], { cwd: context.root, timeout: 10_000 });
|
||||
const parsed = JSON.parse(help.stdout) as { ok?: boolean; supported?: string[]; valuesPrinted?: boolean };
|
||||
const parsed = JSON.parse(help.stdout) as { ok?: boolean; supported?: string[]; unsupported?: string[]; valuesPrinted?: boolean };
|
||||
assert.equal(parsed.ok, true);
|
||||
assert.equal(parsed.valuesPrinted, false);
|
||||
assert.equal(parsed.supported?.some((line) => line.includes("script")), true);
|
||||
assert.equal(parsed.supported?.some((line) => line.includes("apply-patch")), true);
|
||||
assert.equal(parsed.unsupported?.includes("apply-patch"), false);
|
||||
const patchHelp = await execFileAsync(path.join(context.root, "tools/apply_patch"), ["--help"], { cwd: context.root, timeout: 10_000 });
|
||||
assert.equal(patchHelp.stdout.includes("reads *** Begin Patch format"), true);
|
||||
|
||||
return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "gitbundle tran tools are executable and documented"] };
|
||||
return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "gitbundle tran tools are executable and documented", "runner apply-patch helper is bundled"] };
|
||||
};
|
||||
|
||||
function installedApkPackages(containerfile: string): Set<string> {
|
||||
|
||||
Reference in New Issue
Block a user