125 lines
8.7 KiB
TypeScript
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");
|
|
}
|