fix: add aipod imageRef work-ready runner reuse
This commit is contained in:
@@ -10,11 +10,13 @@ const selfTest: SelfTestCase = async () => {
|
||||
const server = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store });
|
||||
try {
|
||||
const client = new ManagerClient(server.baseUrl);
|
||||
const health = await client.get("/health/readiness") as { database?: { adapter?: string; reachable?: boolean; migrationReady?: boolean; failureKind?: string | null }; secretRefs?: { valuesPrinted?: boolean } };
|
||||
const health = await client.get("/health/readiness") as { database?: { adapter?: string; reachable?: boolean; migrationReady?: boolean; failureKind?: string | null }; runnerWorkReady?: JsonRecord; secretRefs?: { valuesPrinted?: boolean } };
|
||||
assert.equal(health.database?.adapter, "memory-self-test");
|
||||
assert.equal(health.database?.reachable, true);
|
||||
assert.equal(health.database?.migrationReady, true);
|
||||
assert.equal(health.database?.failureKind, null);
|
||||
assert.equal(((health.runnerWorkReady as { valuesPrinted?: unknown } | undefined)?.valuesPrinted), false);
|
||||
assert.ok((((health.runnerWorkReady as { requiredImageTools?: string[] } | undefined)?.requiredImageTools) ?? []).includes("npm"));
|
||||
assert.equal(health.secretRefs?.valuesPrinted, false);
|
||||
await assertLongResultUsesTerminalAssistant(client, store);
|
||||
return { name: "manager-memory", tests: ["manager-memory-lifecycle", "manager-result-long-trace"] };
|
||||
|
||||
@@ -45,6 +45,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal(rendered.mutation, false);
|
||||
assert.equal(((rendered.retention as JsonRecord).ttlSecondsAfterFinished), 86_400);
|
||||
assert.equal((rendered.jobIdentity as { serviceAccountName?: string }).serviceAccountName, "agentrun-v01-runner");
|
||||
assertWorkReadySummary(rendered.workReady as JsonRecord);
|
||||
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");
|
||||
@@ -173,6 +174,7 @@ process.exit(1);
|
||||
namespace: "agentrun-v01",
|
||||
managerUrl: "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080",
|
||||
image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||
envIdentity: "selftest-env-identity",
|
||||
kubectlCommand: fakeKubectl,
|
||||
unideskSshEndpointEnv: { name: "UNIDESK_MAIN_SERVER_IP", value: "https://unidesk.default.example.test" },
|
||||
},
|
||||
@@ -183,6 +185,12 @@ process.exit(1);
|
||||
const created = await jobClient.post(`/api/v1/runs/${jobItem.runId}/runner-jobs`, {
|
||||
commandId: jobItem.commandId,
|
||||
attemptId: "attempt_selftest_create",
|
||||
imageRef: {
|
||||
kind: "env-image-dockerfile",
|
||||
repoUrl: "git@github.com:pikasTech/agentrun.git",
|
||||
commitId: "59272f8edb4e23d805b7b9da0050ec40cfc0233d",
|
||||
dockerfilePath: "deploy/container/Containerfile",
|
||||
},
|
||||
transientEnv: [
|
||||
{ name: "HWLAB_API_KEY", value: "hwl_live_selftest", sensitive: true },
|
||||
{ name: "HWLAB_RUNTIME_API_URL", value: "http://runtime-api.test", sensitive: true },
|
||||
@@ -196,6 +204,10 @@ process.exit(1);
|
||||
],
|
||||
});
|
||||
assert.equal((created as { mutation?: unknown }).mutation, true);
|
||||
assert.equal((((created as JsonRecord).envImage as JsonRecord).status), "runtime-default-reused");
|
||||
assert.equal((((created as JsonRecord).envImage as JsonRecord).digestPinned), true);
|
||||
assert.equal(((((created as JsonRecord).envImage as JsonRecord).imageRef as JsonRecord).kind), "env-image-dockerfile");
|
||||
assertWorkReadySummary((created as JsonRecord).workReady as JsonRecord);
|
||||
assert.equal(((created as JsonRecord).retention as JsonRecord).ttlSecondsAfterFinished, 86_400);
|
||||
assert.deepEqual((((created as JsonRecord).transientEnv as JsonRecord).names) as string[], ["HWLAB_API_KEY", "HWLAB_RUNTIME_API_URL", "HWLAB_RUNTIME_WEB_URL", "HWLAB_RUNTIME_NAMESPACE", "HWLAB_RUNTIME_LANE", "HWLAB_RUNTIME_ENDPOINT_SOURCE", "HWLAB_RUNTIME_ENDPOINT_LOCKED", "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME", "UNIDESK_MAIN_SERVER_IP"]);
|
||||
const transientEnvSecret = (created as JsonRecord).transientEnvSecret as JsonRecord;
|
||||
@@ -319,6 +331,16 @@ function assertRunnerJobUsesTransientEnvSecret(manifest: JsonRecord, envName: st
|
||||
assert.equal(secretKeyRef.key, envName);
|
||||
}
|
||||
|
||||
function assertWorkReadySummary(summary: JsonRecord): void {
|
||||
assert.equal(summary.valuesPrinted, false);
|
||||
assert.equal(typeof summary.version, "string");
|
||||
assert.ok((summary.requiredImageTools as string[]).includes("bun"));
|
||||
assert.ok((summary.requiredImageTools as string[]).includes("npm"));
|
||||
assert.ok((summary.requiredImageTools as string[]).includes("gh"));
|
||||
assert.ok((summary.requiredBundledTools as string[]).includes("tran"));
|
||||
assert.equal(((summary.dependencyStrategy as JsonRecord).projectDependencies), "not-installed-by-default");
|
||||
}
|
||||
|
||||
function assertRunnerJobUsesG14EgressProxy(manifest: JsonRecord): void {
|
||||
const proxy = "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789";
|
||||
assert.equal(runnerEnvValue(manifest, "HTTP_PROXY"), proxy);
|
||||
|
||||
@@ -39,6 +39,12 @@ process.exit(1);
|
||||
`);
|
||||
await chmod(fakeKubectl, 0o755);
|
||||
const store = new MemoryAgentRunStore();
|
||||
const aipodImageRef = {
|
||||
kind: "env-image-dockerfile",
|
||||
repoUrl: "git@github.com:pikasTech/agentrun.git",
|
||||
commitId: "59272f8edb4e23d805b7b9da0050ec40cfc0233d",
|
||||
dockerfilePath: "deploy/container/Containerfile",
|
||||
};
|
||||
const server = await startManagerServer({
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
@@ -48,6 +54,7 @@ process.exit(1);
|
||||
namespace: "agentrun-v01",
|
||||
managerUrl: "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080",
|
||||
image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||
envIdentity: "selftest-env-identity",
|
||||
kubectlCommand: fakeKubectl,
|
||||
unideskSshEndpointEnv: { name: "UNIDESK_MAIN_SERVER_IP", value: "https://unidesk.default.example.test" },
|
||||
},
|
||||
@@ -94,7 +101,7 @@ process.exit(1);
|
||||
resourceBundleRef: null,
|
||||
payload: { prompt: "queue dispatch hello" },
|
||||
references: [{ kind: "issue", url: "https://github.com/pikasTech/agentrun/issues/39" }],
|
||||
metadata: { source: "queue-q2-self-test" },
|
||||
metadata: { source: "queue-q2-self-test", aipodImageRef },
|
||||
idempotencyKey: "queue-q2-dispatch-self-test",
|
||||
}) as QueueTaskRecord;
|
||||
const dispatchPlan = await runCliJson(context, server.baseUrl, ["queue", "dispatch", String(created.id), "--dry-run", "--attempt-id", "attempt_queue_q2_cli_dryrun"]);
|
||||
@@ -111,6 +118,12 @@ process.exit(1);
|
||||
const dispatched = await client.post(`/api/v1/queue/tasks/${created.id}/dispatch`, { attemptId: "attempt_queue_q2_selftest" }) as QueueDispatchResult;
|
||||
assert.equal(dispatched.action, "queue-dispatch");
|
||||
assert.equal(dispatched.mutation, true);
|
||||
assert.equal(((dispatched.envImage as JsonRecord).status), "runtime-default-reused");
|
||||
assert.equal(((dispatched.envImage as JsonRecord).digestPinned), true);
|
||||
assert.equal(((dispatched.runnerJob as JsonRecord).image), "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111");
|
||||
assert.equal(((((dispatched.runnerJob as JsonRecord).envImage as JsonRecord).imageRef as JsonRecord).dockerfilePath), "deploy/container/Containerfile");
|
||||
assert.equal(((dispatched.workReady as JsonRecord).valuesPrinted), false);
|
||||
assert.ok((((dispatched.workReady as JsonRecord).requiredImageTools as string[]) ?? []).includes("npm"));
|
||||
assert.equal(dispatched.latestAttempt.attemptId, "attempt_queue_q2_selftest");
|
||||
assert.equal(dispatched.latestAttempt.runId, dispatched.run.id);
|
||||
assert.equal(dispatched.latestAttempt.commandId, dispatched.command.id);
|
||||
|
||||
@@ -24,6 +24,11 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
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;
|
||||
@@ -32,6 +37,12 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
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;
|
||||
@@ -76,7 +87,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
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-direct-ssh-render", "aipod-spec-git-mirror-url", "queue-submit-aipod-dry-run", "aipod-cli-help"] };
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import path from "node:path";
|
||||
import type { SelfTestCase } from "../harness.js";
|
||||
import { imageRefSourceIdentity, imageRefSourceSummary, isDigestPinnedImage, validateAipodImageRef } from "../../common/env-image-ref.js";
|
||||
import { smokeBundledWorkReadyCapabilities, smokeImageWorkReadyCapabilities, staticWorkReadyCapabilitySummary } from "../../common/work-ready.js";
|
||||
|
||||
const requiredRunnerPackages = Object.freeze(["git", "openssh-client", "ripgrep"]);
|
||||
const requiredRunnerPackages = Object.freeze(["ca-certificates", "curl", "git", "github-cli", "kubectl", "nodejs", "npm", "openssh-client", "ripgrep"]);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const selfTest: SelfTestCase = async (context) => {
|
||||
@@ -18,6 +20,9 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
for (const packageName of requiredRunnerPackages) {
|
||||
assert.equal(apkPackages.has(packageName), true, `runner image must install ${packageName}`);
|
||||
}
|
||||
assert.equal(containerfile.includes("bun install --production"), true, "runner image must install AgentRun npm dependencies at image build time");
|
||||
assert.equal(containerfile.includes("npm --version"), true, "runner image build smoke must verify npm");
|
||||
assert.equal(containerfile.includes("gh --version"), true, "runner image build smoke must verify GitHub CLI");
|
||||
|
||||
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");
|
||||
@@ -35,10 +40,39 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
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);
|
||||
const summary = staticWorkReadyCapabilitySummary();
|
||||
assert.equal(summary.valuesPrinted, false);
|
||||
assert.ok((summary.requiredImageTools as string[]).includes("npm"), "work-ready capability must include npm");
|
||||
assert.ok((summary.requiredImageTools as string[]).includes("gh"), "work-ready capability must include gh");
|
||||
assert.equal(((summary.dependencyStrategy as { projectDependencies?: unknown }).projectDependencies), "not-installed-by-default");
|
||||
const fakeBin = await createFakeToolBin(path.join(context.tmp, "work-ready-bin"));
|
||||
const smokeEnv = { PATH: fakeBin, AGENTRUN_RESOURCE_BIN_PATH: path.join(context.root, "tools") };
|
||||
const imageSmoke = await smokeImageWorkReadyCapabilities(smokeEnv);
|
||||
assert.equal(((imageSmoke.smoke as { ok?: unknown }).ok), true);
|
||||
assert.equal(imageSmoke.valuesPrinted, false);
|
||||
const bundleSmoke = await smokeBundledWorkReadyCapabilities(smokeEnv);
|
||||
assert.equal(((bundleSmoke.smoke as { ok?: unknown }).ok), true);
|
||||
assert.equal(bundleSmoke.valuesPrinted, false);
|
||||
assert.equal(JSON.stringify({ imageSmoke, bundleSmoke }).includes("GH_TOKEN"), false);
|
||||
const imageRef = validateAipodImageRef({ kind: "env-image-dockerfile", repoUrl: "git@github.com:pikasTech/agentrun.git", commitId: "59272f8edb4e23d805b7b9da0050ec40cfc0233d", dockerfilePath: "deploy/container/Containerfile" });
|
||||
assert.equal(imageRefSourceIdentity(imageRef).length, 20);
|
||||
assert.equal((imageRefSourceSummary(imageRef).valuesPrinted), false);
|
||||
assert.equal(isDigestPinnedImage("127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111"), true);
|
||||
assert.equal(isDigestPinnedImage("127.0.0.1:5000/agentrun/agentrun-mgr:self-test"), false);
|
||||
|
||||
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"] };
|
||||
return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "runner image build verifies work-ready tools", "gitbundle tran tools are executable and documented", "runner apply-patch helper is bundled", "work-ready smoke runs without printing secrets", "aipod imageRef validates env image source identity"] };
|
||||
};
|
||||
|
||||
async function createFakeToolBin(dir: string): Promise<string> {
|
||||
await mkdir(dir, { recursive: true });
|
||||
for (const tool of ["bun", "node", "npm", "git", "ssh", "gh", "rg", "curl", "kubectl"]) {
|
||||
const file = path.join(dir, tool);
|
||||
await writeFile(file, `#!/bin/sh\necho ${tool}-selftest-version\n`, "utf8");
|
||||
await chmod(file, 0o755);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
function installedApkPackages(containerfile: string): Set<string> {
|
||||
const packages = new Set<string>();
|
||||
const normalized = containerfile.replace(/\\\s*\r?\n\s*/gu, " ");
|
||||
|
||||
+19
-2
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import { chmod, mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -31,14 +31,19 @@ type SelfTestRunContext = Pick<SelfTestContext, "workspace" | "codexHome"> & Par
|
||||
|
||||
export async function createSelfTestContext(root: string): Promise<SelfTestContext> {
|
||||
const tmp = await mkdtemp(path.join(os.tmpdir(), "agentrun-selftest-"));
|
||||
const previousSelftestWorkReadyBinPath = process.env.AGENTRUN_SELFTEST_WORK_READY_BIN_PATH;
|
||||
const codexHome = path.join(tmp, "codex-home");
|
||||
const deepseekHome = path.join(tmp, "deepseek-home");
|
||||
const minimaxM3Home = path.join(tmp, "minimax-m3-home");
|
||||
const workReadyBin = path.join(tmp, "work-ready-bin");
|
||||
const workspace = path.join(tmp, "workspace");
|
||||
await mkdir(codexHome, { recursive: true });
|
||||
await mkdir(deepseekHome, { recursive: true });
|
||||
await mkdir(minimaxM3Home, { recursive: true });
|
||||
await mkdir(workReadyBin, { recursive: true });
|
||||
await mkdir(workspace, { recursive: true });
|
||||
await writeFakeWorkReadyTools(workReadyBin);
|
||||
process.env.AGENTRUN_SELFTEST_WORK_READY_BIN_PATH = workReadyBin;
|
||||
await writeFile(path.join(codexHome, "auth.json"), JSON.stringify({ token: "test-token-material" }));
|
||||
await writeFile(path.join(codexHome, "config.toml"), "model = \"gpt-test\"\n");
|
||||
await writeFile(path.join(deepseekHome, "auth.json"), JSON.stringify({ token: "test-token-material-deepseek" }));
|
||||
@@ -58,10 +63,22 @@ export async function createSelfTestContext(root: string): Promise<SelfTestConte
|
||||
fakeCodexPath,
|
||||
fakeCodexCommand: process.env.AGENTRUN_SELFTEST_CODEX_COMMAND ?? defaultFakeCommand(),
|
||||
fakeCodexArgs: process.env.AGENTRUN_SELFTEST_CODEX_ARGS ? JSON.parse(process.env.AGENTRUN_SELFTEST_CODEX_ARGS) as string[] : defaultFakeArgs(fakeCodexPath),
|
||||
cleanup: () => rm(tmp, { recursive: true, force: true }),
|
||||
cleanup: async () => {
|
||||
if (previousSelftestWorkReadyBinPath === undefined) delete process.env.AGENTRUN_SELFTEST_WORK_READY_BIN_PATH;
|
||||
else process.env.AGENTRUN_SELFTEST_WORK_READY_BIN_PATH = previousSelftestWorkReadyBinPath;
|
||||
await rm(tmp, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function writeFakeWorkReadyTools(dir: string): Promise<void> {
|
||||
for (const tool of ["bun", "node", "npm", "git", "ssh", "gh", "rg", "curl", "kubectl"]) {
|
||||
const file = path.join(dir, tool);
|
||||
await writeFile(file, `#!/bin/sh\necho ${tool}-selftest-version\n`, "utf8");
|
||||
await chmod(file, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRunWithCommand(client: ManagerClient, context: SelfTestRunContext, prompt: string, idempotencyKey: string, timeoutMs: number): Promise<{ runId: string; commandId: string }> {
|
||||
const backendProfile = context.backendProfile ?? "codex";
|
||||
const run = await client.post("/api/v1/runs", {
|
||||
|
||||
Reference in New Issue
Block a user