Files
pikasTech-agentrun/src/selftest/cases/76-aipod-spec.ts
T
2026-06-11 01:21:56 +08:00

125 lines
8.7 KiB
TypeScript

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 parsedWithoutBunGlobal = await runNodeParserCompat(context);
assert.equal(parsedWithoutBunGlobal.name, "Artificer");
assert.equal(parsedWithoutBunGlobal.hasBunGlobal, false);
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");
const shownImageRef = shownItem.imageRef as JsonRecord;
assert.equal(shownImageRef.kind, "env-image-dockerfile");
assert.equal(shownImageRef.repoUrl, "git@github.com:pikasTech/agentrun.git");
assert.equal(shownImageRef.commitId, "59272f8edb4e23d805b7b9da0050ec40cfc0233d");
assert.equal(shownImageRef.dockerfilePath, "deploy/container/Containerfile");
assert.equal(((shownItem.resourceBundleRef as JsonRecord).gitMirror as JsonRecord).enabled, false);
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");
const taskImageRef = ((task.metadata as JsonRecord).aipodImageRef as JsonRecord);
assert.equal(taskImageRef.kind, "env-image-dockerfile");
assert.equal(taskImageRef.valuesPrinted, false);
const runnerDefaults = ((rendered.dispatchDefaults as JsonRecord).runnerJob as JsonRecord);
assert.equal(((runnerDefaults.imageRef as JsonRecord).kind), "env-image-dockerfile");
assert.equal(((runnerDefaults.imageRef as JsonRecord).dockerfilePath), "deploy/container/Containerfile");
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 === "github" && ((item.projection as JsonRecord).kind) === "env" && ((item.projection as JsonRecord).envName) === "GH_TOKEN" && ((item.secretRef as JsonRecord).name) === "agentrun-v01-tool-github-pr"), true);
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), false);
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 disabledMirror = resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { enabled: false, baseUrl: "http://mirror.example.test/root/" }, {});
assert.equal(disabledMirror.fetchRepoUrl, "git@github.com:pikasTech/unidesk.git");
assert.equal(disabledMirror.mirrorUsed, false);
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-yaml-parser-runtime-compatible", "aipod-spec-artificer-image-ref-render", "aipod-spec-artificer-direct-ssh-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 runNodeParserCompat(context: { root: string }): Promise<JsonRecord> {
const script = `import { readFileSync } from "node:fs";
import { parseAipodSpecYaml } from "./src/common/aipod-specs.ts";
const spec = parseAipodSpecYaml(readFileSync("config/aipods/artificer.yaml", "utf8"), "selftest-no-bun-yaml");
console.log(JSON.stringify({ name: spec.metadata.name, hasBunGlobal: typeof globalThis.Bun !== "undefined" }));`;
const proc = spawn("node", ["--import", "tsx", "--eval", script], { cwd: context.root, 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 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");
}