1164 lines
54 KiB
TypeScript
1164 lines
54 KiB
TypeScript
import { readFileSync, rmSync } from "node:fs";
|
|
import { describe, expect, test } from "bun:test";
|
|
import { parseAgentRunClientConfigYaml, resolveAgentRunAuth, runAgentRunCommand, stripAgentRunResourceWrapperArgs } from "./agentrun";
|
|
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
|
|
import { agentRunToolCredentialRefs } from "./agentrun/config";
|
|
import { placeholderAgentRunImage, renderAgentRunGitopsFiles } from "./agentrun-manifests";
|
|
import { collectLaneSecretSources, secretSyncScript } from "./agentrun/secrets";
|
|
|
|
function renderedTextOf(value: unknown): string {
|
|
if (typeof value !== "object" || value === null) throw new Error("expected rendered CLI result");
|
|
const text = (value as { renderedText?: unknown }).renderedText;
|
|
if (typeof text !== "string") throw new Error("expected renderedText string");
|
|
return text;
|
|
}
|
|
|
|
function renderedContentTypeOf(value: unknown): string {
|
|
if (typeof value !== "object" || value === null) throw new Error("expected rendered CLI result");
|
|
const contentType = (value as { contentType?: unknown }).contentType;
|
|
if (typeof contentType !== "string") throw new Error("expected contentType string");
|
|
return contentType;
|
|
}
|
|
|
|
const agentRunClientYaml = [
|
|
"version: 1",
|
|
"kind: AgentRunConfig",
|
|
"metadata:",
|
|
" name: agentrun",
|
|
"manager:",
|
|
" baseUrl: https://agentrun.127-0-0-1.nip.io/",
|
|
" timeoutMs: 15000",
|
|
"publicExposure:",
|
|
" enabled: true",
|
|
" proxyName: agentrun-v01-frpc",
|
|
" remotePort: 22880",
|
|
" publicBaseUrl: https://agentrun.127-0-0-1.nip.io/",
|
|
" masterBaseUrl: http://127.0.0.1:22880",
|
|
" masterFrps:",
|
|
" configPath: /opt/hwlab-frp/frps.dev.toml",
|
|
" containerName: hwlab-frps-dev",
|
|
" masterCaddy:",
|
|
" enabled: true",
|
|
" domain: agentrun.127-0-0-1.nip.io",
|
|
" configPath: /etc/caddy/Caddyfile",
|
|
" serviceName: caddy",
|
|
" upstreamBaseUrl: http://127.0.0.1:22880",
|
|
" responseHeaderTimeoutSeconds: 60",
|
|
"auth:",
|
|
" env: HWLAB_API_KEY",
|
|
" file: /tmp/hwlab-api-key.env",
|
|
" header: Authorization",
|
|
" scheme: Bearer",
|
|
"client:",
|
|
" role: render-only",
|
|
" transport: direct-http",
|
|
" sessionPolicy:",
|
|
" tenantId: unidesk",
|
|
" projectId: test",
|
|
" providerId: NC01",
|
|
" backendProfile: codex",
|
|
" workspaceRef:",
|
|
" kind: opaque",
|
|
" executionPolicy:",
|
|
" sandbox: workspace-write",
|
|
" approval: never",
|
|
" timeoutMs: 900000",
|
|
" network: enabled",
|
|
" secretScope:",
|
|
" allowCredentialEcho: false",
|
|
].join("\n");
|
|
|
|
const agentRunMinimalClientYaml = [
|
|
"version: 1",
|
|
"kind: AgentRunConfig",
|
|
"metadata:",
|
|
" name: agentrun",
|
|
"manager:",
|
|
" baseUrl: http://agentrun.example.local:8080",
|
|
" timeoutMs: 15000",
|
|
"auth:",
|
|
" env: HWLAB_API_KEY",
|
|
" file: /tmp/hwlab-api-key.env",
|
|
" header: Authorization",
|
|
" scheme: Bearer",
|
|
"client:",
|
|
" role: render-only",
|
|
" transport: direct-http",
|
|
" sessionPolicy:",
|
|
" tenantId: unidesk",
|
|
" projectId: test",
|
|
" providerId: NC01",
|
|
" backendProfile: codex",
|
|
" workspaceRef:",
|
|
" kind: opaque",
|
|
" executionPolicy:",
|
|
" sandbox: workspace-write",
|
|
" approval: never",
|
|
" timeoutMs: 900000",
|
|
" network: enabled",
|
|
" secretScope:",
|
|
" allowCredentialEcho: false",
|
|
].join("\n");
|
|
|
|
describe("AgentRun resource bridge argv", () => {
|
|
test("does not forward UniDesk output flags to create task prompt argv", () => {
|
|
expect(stripAgentRunResourceWrapperArgs([
|
|
"--aipod",
|
|
"Artificer",
|
|
"--prompt-stdin",
|
|
"-o",
|
|
"json",
|
|
"--idempotency-key",
|
|
"case-262",
|
|
"--dry-run",
|
|
])).toEqual([
|
|
"--aipod",
|
|
"Artificer",
|
|
"--prompt-stdin",
|
|
"--idempotency-key",
|
|
"case-262",
|
|
"--dry-run",
|
|
]);
|
|
});
|
|
|
|
test("strips wrapper display flags while preserving official apply arguments", () => {
|
|
expect(stripAgentRunResourceWrapperArgs([
|
|
"-f",
|
|
"-",
|
|
"--output=yaml",
|
|
"--raw",
|
|
"--full",
|
|
"--input",
|
|
"--full-text",
|
|
"--dry-run",
|
|
])).toEqual([
|
|
"-f",
|
|
"-",
|
|
"--dry-run",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("AgentRun render-only REST client config", () => {
|
|
test("parses explicit YAML config for direct REST render-only client", () => {
|
|
const config = parseAgentRunClientConfigYaml(agentRunClientYaml, "config/agentrun.yaml");
|
|
expect(config.manager.baseUrl).toBe("https://agentrun.127-0-0-1.nip.io/");
|
|
expect(config.publicExposure?.publicBaseUrl).toBe("https://agentrun.127-0-0-1.nip.io/");
|
|
expect(config.publicExposure?.masterCaddy.domain).toBe("agentrun.127-0-0-1.nip.io");
|
|
expect(config.auth.env).toBe("HWLAB_API_KEY");
|
|
expect(config.auth.header).toBe("Authorization");
|
|
expect(config.auth.scheme).toBe("Bearer");
|
|
expect(config.client.role).toBe("render-only");
|
|
expect(config.client.transport).toBe("direct-http");
|
|
});
|
|
|
|
test("uses env auth before configured file auth without exposing the key", () => {
|
|
const config = parseAgentRunClientConfigYaml(agentRunClientYaml, "config/agentrun.yaml");
|
|
const auth = resolveAgentRunAuth(config, { HWLAB_API_KEY: "secret-value" });
|
|
expect(auth.source).toBe("env");
|
|
expect(auth.value).toBe("secret-value");
|
|
});
|
|
|
|
test("requires explicit render-only direct-http client contract", () => {
|
|
expect(() => parseAgentRunClientConfigYaml(agentRunClientYaml.replace(" transport: direct-http", " transport: ssh-bridge"), "config/agentrun.yaml")).toThrow("client.transport must be direct-http");
|
|
expect(() => parseAgentRunClientConfigYaml(agentRunClientYaml.replace(" role: render-only", " role: proxy"), "config/agentrun.yaml")).toThrow("client.role must be render-only");
|
|
});
|
|
});
|
|
|
|
describe("AgentRun default transport contract", () => {
|
|
test("server-facing compatibility groups use REST dispatcher, not official SSH CLI wrapper", () => {
|
|
const source = readFileSync(new URL("./agentrun/entry.ts", import.meta.url), "utf8");
|
|
const commandRouter = source.slice(source.indexOf("export async function runAgentRunCommand"), source.indexOf("function isAgentRunRestCompatGroup"));
|
|
expect(commandRouter).toContain("runAgentRunRestCompatCommand");
|
|
expect(commandRouter).not.toContain("runOfficialAgentRunCli");
|
|
expect(commandRouter).not.toContain("runPreparedOfficialAgentRunCli");
|
|
});
|
|
|
|
test("resource verbs can use direct REST without UniDesk SSH config", async () => {
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
fetch(request) {
|
|
const url = new URL(request.url);
|
|
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
|
|
expect(url.pathname).toBe("/api/v1/queue/tasks");
|
|
expect(url.searchParams.get("queue")).toBe("commander");
|
|
expect(url.searchParams.get("state")).toBe("running");
|
|
return Response.json({
|
|
ok: true,
|
|
data: {
|
|
items: [
|
|
{ id: "qt_1", state: "queued", queue: "commander" },
|
|
],
|
|
},
|
|
});
|
|
},
|
|
});
|
|
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
|
const previousKey = process.env.HWLAB_API_KEY;
|
|
process.env.HWLAB_API_KEY = "secret-value";
|
|
const tempConfigPath = `/tmp/unidesk-agentrun-test-${process.pid}-${Date.now()}.yaml`;
|
|
try {
|
|
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
|
await Bun.write(process.env.AGENTRUN_CLIENT_CONFIG, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
|
const result = await runAgentRunCommand(null, ["get", "tasks", "--queue", "commander", "-o", "json"]);
|
|
expect(result.ok).toBe(true);
|
|
expect("renderedText" in result).toBe(true);
|
|
if ("renderedText" in result) {
|
|
expect(result.renderedText).toContain("\"kind\": \"TaskList\"");
|
|
expect(result.renderedText).toContain("\"name\": \"qt_1\"");
|
|
}
|
|
} finally {
|
|
server.stop(true);
|
|
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
|
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
|
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
|
else process.env.HWLAB_API_KEY = previousKey;
|
|
rmSync(tempConfigPath, { force: true });
|
|
}
|
|
});
|
|
|
|
test("task input drill-down is bounded, reconstructable, and SecretRef-only", async () => {
|
|
const taskId = "qt_artificer_input";
|
|
const hiddenCredential = "HIDDEN_CREDENTIAL_VALUE_MUST_NOT_BE_PRINTED";
|
|
const task = {
|
|
id: taskId,
|
|
tenantId: "unidesk",
|
|
projectId: "pikasTech/unidesk",
|
|
queue: "commander",
|
|
lane: "v0.1",
|
|
title: "Artificer input fixture",
|
|
priority: 50,
|
|
state: "completed",
|
|
version: 7,
|
|
backendProfile: "sub2api",
|
|
providerId: "G14",
|
|
workspaceRef: { kind: "opaque", path: ".", repo: "pikasTech/unidesk", branch: "master" },
|
|
sessionRef: { sessionId: "ses_fixture", conversationId: "conv_fixture", metadata: { credential: hiddenCredential } },
|
|
executionPolicy: {
|
|
sandbox: "workspace-write",
|
|
approval: "never",
|
|
timeoutMs: 1_800_000,
|
|
network: "enabled",
|
|
secretScope: {
|
|
allowCredentialEcho: false,
|
|
providerCredentials: [{
|
|
profile: "sub2api",
|
|
secretRef: { name: "agentrun-provider", keys: ["auth.json"], value: hiddenCredential },
|
|
value: hiddenCredential,
|
|
}],
|
|
toolCredentials: [{
|
|
tool: "github",
|
|
purpose: "github-pr",
|
|
secretRef: { name: "agentrun-github", keys: ["GH_TOKEN"], token: hiddenCredential },
|
|
projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN", value: hiddenCredential },
|
|
}],
|
|
},
|
|
},
|
|
resourceBundleRef: {
|
|
kind: "gitbundle",
|
|
repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/unidesk.git`,
|
|
ref: "master",
|
|
bundles: [{ name: "unidesk-skills", subpath: ".agents/skills", targetPath: ".agents/skills" }],
|
|
requiredSkills: [{ name: "unidesk-gh" }],
|
|
credentialRef: { name: "agentrun-resource-git", keys: ["username", "password"], value: hiddenCredential },
|
|
submodules: false,
|
|
lfs: false,
|
|
},
|
|
payload: { prompt: "Inspect the existing sentinel configuration.", model: "gpt-5.5" },
|
|
payloadHash: "sha256:fixture",
|
|
references: [{ kind: "github-issue", url: "https://github.com/pikasTech/unidesk/issues/1819" }],
|
|
metadata: {
|
|
aipod: "Artificer",
|
|
source: "config/aipods/artificer.yaml",
|
|
aipodSpecHash: "sha256:aipod-fixture",
|
|
aipodImageRef: {
|
|
kind: "env-image-dockerfile",
|
|
repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/agentrun.git`,
|
|
commitId: "a".repeat(40),
|
|
dockerfilePath: "deploy/container/Containerfile",
|
|
},
|
|
credential: hiddenCredential,
|
|
},
|
|
idempotencyKey: "artificer-input-fixture",
|
|
latestAttempt: { attemptId: "qat_fixture", state: "completed", runId: "run_fixture", commandId: "cmd_fixture", runnerJobId: "rjob_fixture", sessionId: "ses_fixture" },
|
|
supervisor: { verboseDiagnostic: "x".repeat(20_000), credential: hiddenCredential },
|
|
valuesPrinted: false,
|
|
};
|
|
let requests = 0;
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
fetch(request) {
|
|
const url = new URL(request.url);
|
|
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
|
|
expect(url.pathname).toBe(`/api/v1/queue/tasks/${taskId}`);
|
|
requests += 1;
|
|
return Response.json({ ok: true, data: task });
|
|
},
|
|
});
|
|
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
|
const previousKey = process.env.HWLAB_API_KEY;
|
|
process.env.HWLAB_API_KEY = "secret-value";
|
|
const tempConfigPath = `/tmp/unidesk-agentrun-input-test-${process.pid}-${Date.now()}.yaml`;
|
|
try {
|
|
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
|
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
|
|
|
const invalidCombinations = [
|
|
{ args: ["--input", "--full"], contentType: "text/plain" },
|
|
{ args: ["--full", "--input", "-o", "json"], contentType: "application/json" },
|
|
{ args: ["--input", "--full", "-o", "yaml"], contentType: "application/yaml" },
|
|
{ args: ["--input", "--raw"], contentType: "application/json" },
|
|
];
|
|
for (const invalid of invalidCombinations) {
|
|
const rejected = await runAgentRunCommand(null, ["describe", `task/${taskId}`, ...invalid.args]);
|
|
const rejectedText = renderedTextOf(rejected);
|
|
expect(rejected.ok).toBe(false);
|
|
expect(renderedContentTypeOf(rejected)).toBe(invalid.contentType);
|
|
expect(rejectedText).toContain("validation-failed");
|
|
expect(rejectedText).toContain("cannot be combined with complete resource disclosure");
|
|
expect(rejectedText).not.toContain(hiddenCredential);
|
|
expect(rejectedText).not.toContain("supervisor");
|
|
expect(Buffer.byteLength(rejectedText, "utf8")).toBeLessThan(2400);
|
|
}
|
|
expect(requests).toBe(0);
|
|
|
|
const summaryText = renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`]));
|
|
expect(summaryText).toContain(`agentrun describe task/${taskId} --input -o json`);
|
|
expect(summaryText).not.toContain(hiddenCredential);
|
|
|
|
const compact = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "-o", "json"]))) as { next?: { input?: string } };
|
|
expect(compact.next?.input).toContain("--input -o json");
|
|
|
|
const inputText = renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "--input"]));
|
|
const input = JSON.parse(inputText) as {
|
|
kind?: string;
|
|
spec?: Record<string, any>;
|
|
aipodSpec?: Record<string, any>;
|
|
provenance?: Record<string, any>;
|
|
redaction?: Record<string, any>;
|
|
valuesPrinted?: boolean;
|
|
};
|
|
expect(input.kind).toBe("TaskInput");
|
|
expect(input.spec?.payload?.prompt).toBe(task.payload.prompt);
|
|
expect(input.spec?.idempotencyKey).toBe(task.idempotencyKey);
|
|
expect(input.spec?.metadata?.aipod).toBe("Artificer");
|
|
expect(input.aipodSpec?.specHash).toBe("sha256:aipod-fixture");
|
|
expect(input.provenance?.sourceTaskId).toBe(taskId);
|
|
expect(input.spec?.executionPolicy?.secretScope?.providerCredentials?.[0]?.secretRef).toEqual({
|
|
name: "agentrun-provider",
|
|
keys: ["auth.json"],
|
|
valuesPrinted: false,
|
|
});
|
|
expect(input.spec?.resourceBundleRef?.credentialRef).toEqual({
|
|
name: "agentrun-resource-git",
|
|
keys: ["username", "password"],
|
|
valuesPrinted: false,
|
|
});
|
|
expect(input.spec?.resourceBundleRef?.repoUrl).toBe("https://github.com/pikasTech/unidesk.git");
|
|
expect(input.aipodSpec?.imageRef?.repoUrl).toBe("https://github.com/pikasTech/agentrun.git");
|
|
expect(input.redaction?.credentialValues).toBe("omitted");
|
|
expect(input.valuesPrinted).toBe(false);
|
|
expect(inputText).not.toContain(hiddenCredential);
|
|
expect(Buffer.byteLength(inputText, "utf8")).toBeLessThan(10_240);
|
|
|
|
const full = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "--full", "-o", "json"]))) as { resource?: typeof task };
|
|
expect(full.resource?.executionPolicy.secretScope.providerCredentials[0]?.secretRef.value).toBe(hiddenCredential);
|
|
const raw = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `task/${taskId}`, "--raw"]))) as { data?: typeof task };
|
|
expect(raw.data?.resourceBundleRef.credentialRef.value).toBe(hiddenCredential);
|
|
expect(requests).toBe(5);
|
|
} finally {
|
|
server.stop(true);
|
|
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
|
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
|
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
|
else process.env.HWLAB_API_KEY = previousKey;
|
|
rmSync(tempConfigPath, { force: true });
|
|
}
|
|
});
|
|
|
|
test("aipodspec describe is bounded and homologous while full and raw stay complete", async () => {
|
|
const aipodName = "Artificer";
|
|
const hiddenCredential = "HIDDEN_AIPOD_CREDENTIAL_VALUE_MUST_NOT_BE_PRINTED";
|
|
const providerCredential = {
|
|
profile: "sub2api",
|
|
name: "agentrun-provider",
|
|
namespace: null,
|
|
keys: ["auth.json", "config.toml"],
|
|
value: hiddenCredential,
|
|
valuesPrinted: false,
|
|
};
|
|
const toolCredentials = [
|
|
{
|
|
tool: "github",
|
|
purpose: "github-pr",
|
|
name: "agentrun-github",
|
|
namespace: null,
|
|
keys: ["GH_TOKEN"],
|
|
projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN", value: hiddenCredential },
|
|
token: hiddenCredential,
|
|
valuesPrinted: false,
|
|
},
|
|
{
|
|
tool: "ssh",
|
|
purpose: "remote-workspace",
|
|
name: "agentrun-ssh",
|
|
namespace: null,
|
|
keys: ["config", "key"],
|
|
projection: { kind: "mount", mountPath: "/run/secrets/ssh", value: hiddenCredential },
|
|
valuesPrinted: false,
|
|
},
|
|
{
|
|
tool: "provider",
|
|
purpose: "model-auth",
|
|
name: "agentrun-provider",
|
|
namespace: null,
|
|
keys: ["auth.json"],
|
|
projection: { kind: "mount", mountPath: "/run/secrets/provider", value: hiddenCredential },
|
|
valuesPrinted: false,
|
|
},
|
|
];
|
|
const data = {
|
|
action: "show",
|
|
item: {
|
|
name: aipodName,
|
|
displayName: "Artificer",
|
|
description: "x".repeat(20_000),
|
|
specHash: "sha256:aipod-fixture",
|
|
source: "config/aipods/artificer.yaml",
|
|
backendProfile: "sub2api",
|
|
model: { model: "gpt-5.5", reasoningEffort: "high" },
|
|
imageRef: {
|
|
kind: "env-image-dockerfile",
|
|
repoUrl: "https://github.com/pikasTech/agentrun.git",
|
|
commitId: "a".repeat(40),
|
|
dockerfilePath: "deploy/container/Containerfile",
|
|
sourceIdentity: "agentrun@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
valuesPrinted: false,
|
|
},
|
|
queue: "commander",
|
|
lane: "v0.1",
|
|
providerId: "NC01",
|
|
providerCredentials: { count: 1, profiles: ["sub2api"], items: [providerCredential], valuesPrinted: false },
|
|
toolCredentials: { count: 3, tools: ["github", "ssh", "provider"], items: toolCredentials, valuesPrinted: false },
|
|
resourceBundleRef: {
|
|
kind: "gitbundle",
|
|
repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/unidesk.git`,
|
|
ref: "master",
|
|
commitId: null,
|
|
bundles: { count: 6, items: [{ name: "unidesk-skills", targetPath: ".agents/skills" }], valuesPrinted: false },
|
|
requiredSkills: { count: 11, names: ["unidesk-gh"], valuesPrinted: false },
|
|
promptRefs: { count: 0, names: [], valuesPrinted: false },
|
|
valuesPrinted: false,
|
|
},
|
|
createdAt: "2026-07-12T00:00:00.000Z",
|
|
updatedAt: "2026-07-12T01:00:00.000Z",
|
|
valuesPrinted: false,
|
|
},
|
|
spec: {
|
|
apiVersion: "agentrun.pikastech.local/v1alpha1",
|
|
kind: "AipodSpec",
|
|
metadata: { name: aipodName, displayName: "Artificer", description: "x".repeat(20_000) },
|
|
spec: {
|
|
queue: "commander",
|
|
lane: "v0.1",
|
|
providerId: "NC01",
|
|
backendProfile: "sub2api",
|
|
model: { model: "gpt-5.5", reasoningEffort: "high" },
|
|
workspaceRef: { kind: "opaque", path: "." },
|
|
executionPolicy: {
|
|
secretScope: {
|
|
allowCredentialEcho: false,
|
|
providerCredentials: [{ profile: "sub2api", secretRef: { name: "agentrun-provider", keys: ["auth.json"], value: hiddenCredential } }],
|
|
toolCredentials: [{ tool: "github", secretRef: { name: "agentrun-github", keys: ["GH_TOKEN"], token: hiddenCredential } }],
|
|
},
|
|
},
|
|
resourceBundleRef: {
|
|
kind: "gitbundle",
|
|
repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/unidesk.git`,
|
|
ref: "master",
|
|
bundles: [{ name: "unidesk-skills", subpath: ".agents/skills", targetPath: ".agents/skills" }],
|
|
requiredSkills: [{ name: "unidesk-gh" }],
|
|
},
|
|
metadata: { aipod: aipodName, source: "config/aipods/artificer.yaml" },
|
|
},
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
let requests = 0;
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
fetch(request) {
|
|
const url = new URL(request.url);
|
|
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
|
|
expect(url.pathname).toBe(`/api/v1/aipod-specs/${aipodName}`);
|
|
requests += 1;
|
|
return Response.json({ ok: true, data });
|
|
},
|
|
});
|
|
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
|
const previousKey = process.env.HWLAB_API_KEY;
|
|
process.env.HWLAB_API_KEY = "secret-value";
|
|
const tempConfigPath = `/tmp/unidesk-agentrun-aipodspec-test-${process.pid}-${Date.now()}.yaml`;
|
|
try {
|
|
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
|
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
|
|
|
const text = renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`]));
|
|
expect(text).toContain(`Name: aipodspec/${aipodName}`);
|
|
expect(text).toContain("Hash: sha256:aipod-fixture");
|
|
expect(text).toContain("Kind: opaque path=. branch=-");
|
|
expect(text).toContain("References: bundles=6 requiredSkills=11 promptRefs=0");
|
|
expect(text).toContain("Credentials: provider=1 tool=3 total=4");
|
|
expect(text).toContain("secret=agentrun-provider keys=auth.json,config.toml");
|
|
expect(text).toContain(`agentrun describe aipodspec/${aipodName} --full -o json`);
|
|
expect(Buffer.byteLength(text, "utf8")).toBeLessThan(10_240);
|
|
expect(text).not.toContain(hiddenCredential);
|
|
|
|
const jsonText = renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "-o", "json"]));
|
|
const json = JSON.parse(jsonText) as Record<string, any>;
|
|
const yamlText = renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "-o", "yaml"]));
|
|
const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
|
|
expect(yaml).toEqual(json);
|
|
expect(json.kind).toBe("AipodSpec");
|
|
expect(json.identity.specHash).toBe("sha256:aipod-fixture");
|
|
expect(json.runtime).toMatchObject({ lane: "v0.1", backendProfile: "sub2api", providerId: "NC01" });
|
|
expect(json.workspaceRef).toMatchObject({ kind: "opaque", path: ".", valuesPrinted: false });
|
|
expect(json.resourceBundleRef.sourceIdentity.repoUrl).toBe("https://github.com/pikasTech/unidesk.git");
|
|
expect(json.resourceBundleRef).toMatchObject({ bundleCount: 6, requiredSkillCount: 11, promptRefCount: 0 });
|
|
expect(json.credentialRefs).toMatchObject({ totalCount: 4, valuesPrinted: false });
|
|
expect(json.credentialRefs.provider.items[0].secretRef).toEqual({
|
|
namespace: null,
|
|
name: "agentrun-provider",
|
|
keys: ["auth.json", "config.toml"],
|
|
valuesPrinted: false,
|
|
});
|
|
expect(json.redaction).toEqual({ credentialValues: "omitted", secretRefsOnly: true, valuesPrinted: false });
|
|
expect(json.valuesPrinted).toBe(false);
|
|
for (const bounded of [jsonText, yamlText]) {
|
|
expect(Buffer.byteLength(bounded, "utf8")).toBeLessThan(10_240);
|
|
expect(bounded).not.toContain(hiddenCredential);
|
|
expect(bounded).not.toContain("x".repeat(200));
|
|
}
|
|
|
|
const full = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "--full", "-o", "json"]))) as { resource?: typeof data };
|
|
expect(full.resource?.item.description).toBe("x".repeat(20_000));
|
|
expect(full.resource?.spec.kind).toBe("AipodSpec");
|
|
const raw = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "--raw"]))) as { data?: typeof data };
|
|
expect(raw.data?.item.description).toBe("x".repeat(20_000));
|
|
expect(raw.data?.spec.kind).toBe("AipodSpec");
|
|
expect(requests).toBe(5);
|
|
} finally {
|
|
server.stop(true);
|
|
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
|
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
|
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
|
else process.env.HWLAB_API_KEY = previousKey;
|
|
rmSync(tempConfigPath, { force: true });
|
|
}
|
|
});
|
|
|
|
test("retry dry-run is server-validated and attempts use the formal resource API", async () => {
|
|
const taskId = "qt_failed_1";
|
|
const attempt = {
|
|
attemptId: "qat_retry_1",
|
|
taskId,
|
|
retryIndex: 1,
|
|
state: "reserved",
|
|
idempotencyKey: "retry-key-1",
|
|
reason: "dependency repaired",
|
|
payloadHash: "sha256:payload",
|
|
previousAttemptId: "qat_initial_0",
|
|
runId: "run_retry_1",
|
|
commandId: "cmd_retry_1",
|
|
runnerJobId: "rjob_retry_1",
|
|
sessionId: "ses_retry_1",
|
|
sessionPath: "/api/v1/sessions/ses_retry_1",
|
|
failureKind: "infra-failed",
|
|
failureMessage: "kubectl create runner job failed before materialization",
|
|
createdAt: "2026-07-12T00:00:00.000Z",
|
|
updatedAt: "2026-07-12T00:00:01.000Z",
|
|
activatedAt: "2026-07-12T00:00:01.000Z",
|
|
terminalAt: null,
|
|
};
|
|
let retryRequests = 0;
|
|
let attemptRequests = 0;
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
async fetch(request) {
|
|
const url = new URL(request.url);
|
|
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
|
|
if (url.pathname === `/api/v1/queue/tasks/${taskId}/retry`) {
|
|
retryRequests += 1;
|
|
expect(request.method).toBe("POST");
|
|
expect(await request.json()).toEqual({
|
|
idempotencyKey: "retry-key-1",
|
|
reason: "dependency repaired",
|
|
dryRun: true,
|
|
});
|
|
return Response.json({
|
|
ok: true,
|
|
data: {
|
|
action: "queue-retry",
|
|
mutation: false,
|
|
idempotentReplay: false,
|
|
task: { id: taskId, state: "failed", queue: "commander", payloadHash: "sha256:payload" },
|
|
attempt: null,
|
|
previousAttempt: { ...attempt, attemptId: "qat_initial_0", retryIndex: 0, state: "failed", previousAttemptId: null },
|
|
run: null,
|
|
command: null,
|
|
runnerJob: null,
|
|
allowedTransition: { from: "failed", to: "running", retryIndex: 1, previousAttemptId: "qat_initial_0", payloadHash: "sha256:payload", valuesPrinted: false },
|
|
pollActions: [],
|
|
},
|
|
});
|
|
}
|
|
if (url.pathname === `/api/v1/queue/tasks/${taskId}/attempts`) {
|
|
attemptRequests += 1;
|
|
expect(request.method).toBe("GET");
|
|
expect(url.searchParams.get("cursor")).toBe("1");
|
|
expect(url.searchParams.get("limit")).toBe("10");
|
|
return Response.json({ ok: true, data: { taskId, items: [attempt], count: 1, cursor: null } });
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
},
|
|
});
|
|
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
|
const previousKey = process.env.HWLAB_API_KEY;
|
|
process.env.HWLAB_API_KEY = "secret-value";
|
|
const tempConfigPath = `/tmp/unidesk-agentrun-retry-test-${process.pid}-${Date.now()}.yaml`;
|
|
try {
|
|
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
|
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
|
const retryResult = await runAgentRunCommand(null, ["retry", `task/${taskId}`, "--idempotency-key", "retry-key-1", "--reason", "dependency repaired", "--dry-run"]);
|
|
expect(retryResult.ok).toBe(true);
|
|
expect("renderedText" in retryResult).toBe(true);
|
|
if ("renderedText" in retryResult) {
|
|
expect(retryResult.renderedText).toContain(`Task retry validated: task/${taskId}`);
|
|
expect(retryResult.renderedText).toContain("Transition: failed -> running retryIndex=1");
|
|
expect(retryResult.renderedText).toContain("Attempt: planned");
|
|
}
|
|
|
|
const attemptsResult = await runAgentRunCommand(null, ["get", "attempts", "--task", taskId, "--cursor", "1", "--limit", "10", "-o", "json"]);
|
|
expect(attemptsResult.ok).toBe(true);
|
|
expect("renderedText" in attemptsResult).toBe(true);
|
|
if ("renderedText" in attemptsResult && typeof attemptsResult.renderedText === "string") {
|
|
const payload = JSON.parse(attemptsResult.renderedText) as { kind?: string; taskId?: string; count?: number; items?: Array<Record<string, unknown>> };
|
|
expect(payload.kind).toBe("AttemptList");
|
|
expect(payload.taskId).toBe(taskId);
|
|
expect(payload.count).toBe(1);
|
|
expect(payload.items?.[0]?.name).toBe(attempt.attemptId);
|
|
expect(payload.items?.[0]?.retryIndex).toBe(1);
|
|
expect(payload.items?.[0]?.failureMessage).toBe(attempt.failureMessage);
|
|
}
|
|
|
|
const attemptsHumanResult = await runAgentRunCommand(null, ["get", "attempts", "--task", taskId, "--cursor", "1", "--limit", "10"]);
|
|
expect("renderedText" in attemptsHumanResult ? attemptsHumanResult.renderedText : "").toContain("infra-failed: kubectl create runner job failed before materialization");
|
|
expect(retryRequests).toBe(1);
|
|
expect(attemptRequests).toBe(2);
|
|
} finally {
|
|
server.stop(true);
|
|
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
|
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
|
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
|
else process.env.HWLAB_API_KEY = previousKey;
|
|
rmSync(tempConfigPath, { force: true });
|
|
}
|
|
});
|
|
|
|
test("retry and attempt resource help stays scoped to the controlled commands", async () => {
|
|
const retryHelp = await runAgentRunCommand(null, ["retry", "--help"]);
|
|
const attemptHelp = await runAgentRunCommand(null, ["get", "attempts", "--help"]);
|
|
const describeHelp = await runAgentRunCommand(null, ["describe", "--help"]);
|
|
expect("renderedText" in retryHelp ? retryHelp.renderedText : "").toContain("agentrun retry task/<taskId> --idempotency-key <key> --reason <text>");
|
|
expect("renderedText" in attemptHelp ? attemptHelp.renderedText : "").toContain("agentrun get attempts --task <taskId>");
|
|
expect("renderedText" in describeHelp ? describeHelp.renderedText : "").toContain("agentrun describe task/qt_... --input -o json");
|
|
|
|
const rootHelp = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", "--help"], {
|
|
cwd: new URL("../..", import.meta.url).pathname,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const rootHelpText = rootHelp.stdout.toString();
|
|
expect(rootHelp.exitCode).toBe(0);
|
|
expect(rootHelpText).toContain("Verbs: get, describe, events, logs, result, ack, cancel, retry,");
|
|
expect(rootHelpText).toContain("Resources: task/qt, attempt, run,");
|
|
expect(rootHelpText).toContain("agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run");
|
|
expect(rootHelpText).toContain("agentrun get attempts --task <taskId> --limit 20");
|
|
expect(rootHelpText.split("\n").length).toBeLessThan(40);
|
|
});
|
|
|
|
test("local resource validation preserves bounded human and machine errors", async () => {
|
|
const cases = [
|
|
{
|
|
args: ["retry", "task/qt_failed_1", "--reason", "dependency repaired"],
|
|
message: "retry requires --idempotency-key <key>",
|
|
},
|
|
{
|
|
args: ["retry", "task/qt_failed_1", "--idempotency-key", "retry-key-1"],
|
|
message: "retry requires --reason <text>",
|
|
},
|
|
{
|
|
args: ["get", "attempts"],
|
|
message: "get attempts requires --task <taskId>",
|
|
},
|
|
{
|
|
args: ["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired"],
|
|
message: "retry supports task/<taskId>",
|
|
},
|
|
{
|
|
args: ["retry"],
|
|
message: "retry requires task/<taskId>",
|
|
},
|
|
];
|
|
for (const item of cases) {
|
|
const human = await runAgentRunCommand(null, item.args);
|
|
const humanText = renderedTextOf(human);
|
|
expect(human.ok).toBe(false);
|
|
expect(humanText).toContain("Error: validation-failed");
|
|
expect(humanText).toContain(item.message);
|
|
expect(humanText.length).toBeLessThan(1600);
|
|
|
|
for (const outputArgs of [["-o", "json"], ["--raw"]]) {
|
|
const machine = await runAgentRunCommand(null, [...item.args, ...outputArgs]);
|
|
const machineText = renderedTextOf(machine);
|
|
const payload = JSON.parse(machineText) as { ok?: boolean; failureKind?: string; message?: string };
|
|
expect(machine.ok).toBe(false);
|
|
expect(renderedContentTypeOf(machine)).toBe("application/json");
|
|
expect(payload.ok).toBe(false);
|
|
expect(payload.failureKind).toBe("validation-failed");
|
|
expect(payload.message).toBe(item.message);
|
|
expect(machineText.length).toBeLessThan(2400);
|
|
}
|
|
}
|
|
|
|
for (const cliArgs of [
|
|
["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired", "-o", "json"],
|
|
["get", "attempts", "--raw"],
|
|
["describe", "task/qt_sensitive", "--input", "--full", "-o", "json"],
|
|
["describe", "task/qt_sensitive", "--input", "--raw"],
|
|
]) {
|
|
const cli = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", ...cliArgs], {
|
|
cwd: new URL("../..", import.meta.url).pathname,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const payload = JSON.parse(cli.stdout.toString()) as { ok?: boolean; failureKind?: string };
|
|
expect(cli.exitCode).toBe(1);
|
|
expect(payload.ok).toBe(false);
|
|
expect(payload.failureKind).toBe("validation-failed");
|
|
expect(cli.stdout.toString().length).toBeLessThan(2400);
|
|
expect(cli.stdout.toString()).not.toContain('"resource":');
|
|
expect(cli.stdout.toString()).not.toContain('"data":');
|
|
}
|
|
});
|
|
|
|
test("server retry validation errors stay bounded in human and JSON output", async () => {
|
|
const taskId = "qt_completed_1";
|
|
const longMessage = `queue task ${taskId} cannot be retried from state completed: ${"x".repeat(6000)}`;
|
|
const longDiagnostic = `server diagnostic: ${"y".repeat(9000)}`;
|
|
let requests = 0;
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
async fetch(request) {
|
|
const url = new URL(request.url);
|
|
expect(url.pathname).toBe(`/api/v1/queue/tasks/${taskId}/retry`);
|
|
expect(request.method).toBe("POST");
|
|
expect(await request.json()).toEqual({ idempotencyKey: "retry-key-terminal", reason: "operator requested retry", dryRun: true });
|
|
requests += 1;
|
|
return Response.json({
|
|
ok: false,
|
|
failureKind: "schema-invalid",
|
|
message: longMessage,
|
|
details: { allowedStates: ["failed", "blocked"], diagnostic: longDiagnostic, valuesPrinted: false },
|
|
valuesPrinted: false,
|
|
}, { status: 409 });
|
|
},
|
|
});
|
|
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
|
const previousKey = process.env.HWLAB_API_KEY;
|
|
process.env.HWLAB_API_KEY = "secret-value";
|
|
const tempConfigPath = `/tmp/unidesk-agentrun-retry-error-test-${process.pid}-${Date.now()}.yaml`;
|
|
try {
|
|
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
|
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
|
const baseArgs = ["retry", `task/${taskId}`, "--idempotency-key", "retry-key-terminal", "--reason", "operator requested retry", "--dry-run"];
|
|
const human = await runAgentRunCommand(null, baseArgs);
|
|
const humanText = renderedTextOf(human);
|
|
expect(human.ok).toBe(false);
|
|
expect(humanText).toContain(`queue task ${taskId} cannot be retried from state completed`);
|
|
expect(humanText).not.toContain("x".repeat(500));
|
|
expect(humanText.length).toBeLessThan(1800);
|
|
|
|
const json = await runAgentRunCommand(null, [...baseArgs, "-o", "json"]);
|
|
const jsonText = renderedTextOf(json);
|
|
const payload = JSON.parse(jsonText) as { ok?: boolean; failureKind?: string; message?: string; agentrun?: { details?: { diagnostic?: string } } };
|
|
expect(json.ok).toBe(false);
|
|
expect(renderedContentTypeOf(json)).toBe("application/json");
|
|
expect(payload.failureKind).toBe("validation-failed");
|
|
expect(payload.message?.length).toBeLessThanOrEqual(400);
|
|
expect(payload.agentrun?.details?.diagnostic?.length).toBeLessThanOrEqual(400);
|
|
expect(jsonText.length).toBeLessThan(5000);
|
|
expect(requests).toBe(2);
|
|
} finally {
|
|
server.stop(true);
|
|
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
|
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
|
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
|
else process.env.HWLAB_API_KEY = previousKey;
|
|
rmSync(tempConfigPath, { force: true });
|
|
}
|
|
});
|
|
|
|
test("session send plan and typed partial-write errors stay homologous and bounded", async () => {
|
|
const sessionId = "sess_artificer_fixture";
|
|
const runId = "run_partial_fixture";
|
|
const commandId = "cmd_partial_fixture";
|
|
const hiddenKubernetesPayload = "HIDDEN_KUBERNETES_PAYLOAD_MUST_NOT_BE_PRINTED";
|
|
let postRequests = 0;
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
async fetch(request) {
|
|
const url = new URL(request.url);
|
|
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
|
|
if (request.method === "GET" && url.pathname === `/api/v1/sessions/${sessionId}`) {
|
|
return Response.json({
|
|
ok: true,
|
|
data: {
|
|
sessionId,
|
|
tenantId: "unidesk",
|
|
projectId: "test",
|
|
providerId: "NC01",
|
|
backendProfile: "codex",
|
|
conversationId: sessionId,
|
|
metadata: { title: "Artificer fixture" },
|
|
},
|
|
});
|
|
}
|
|
if (request.method === "POST" && url.pathname === `/api/v1/sessions/${sessionId}/send`) {
|
|
postRequests += 1;
|
|
const body = await request.json() as Record<string, any>;
|
|
if (body.payload?.prompt === "plan fixture") {
|
|
expect(body.dryRun).toBe(true);
|
|
return Response.json({
|
|
ok: true,
|
|
data: {
|
|
action: "session-send-plan",
|
|
dryRun: true,
|
|
mutation: false,
|
|
sessionId,
|
|
decision: "turn",
|
|
internalCommandType: "turn",
|
|
activeBefore: null,
|
|
reusedIdleRun: null,
|
|
request: { method: "POST", path: `/api/v1/sessions/${sessionId}/send`, commandType: "turn", createRunnerJob: true, valuesPrinted: false },
|
|
next: {
|
|
confirm: { action: "send-session", operation: "send", resourceKind: "session", resourceName: sessionId, sessionId, inputKind: "prompt", valuesPrinted: false },
|
|
note: "Remove --dry-run to perform the mutation.",
|
|
},
|
|
valuesPrinted: false,
|
|
},
|
|
});
|
|
}
|
|
return Response.json({
|
|
ok: false,
|
|
failureKind: "infra-failed",
|
|
message: "kubectl get jobs failed with code 1",
|
|
traceId: "trc_session_send_fixture",
|
|
error: {
|
|
name: "AgentRunError",
|
|
failureKind: "infra-failed",
|
|
message: "kubectl get jobs failed with code 1",
|
|
details: {
|
|
code: "runner-admission-failed",
|
|
reason: "runner-retention-observation-failed",
|
|
phase: "runner-admission",
|
|
sessionId,
|
|
runId,
|
|
commandId,
|
|
mutation: true,
|
|
partialWrite: true,
|
|
durable: true,
|
|
resources: {
|
|
session: { id: sessionId, state: "running" },
|
|
run: { id: runId, status: "queued" },
|
|
command: { id: commandId, state: "pending", type: "turn" },
|
|
},
|
|
recoveryActions: [
|
|
{ operation: "describe", resourceKind: "session", resourceName: sessionId },
|
|
{ operation: "events", resourceKind: "run", resourceName: runId, runId, afterSeq: 0, limit: 100 },
|
|
],
|
|
stderr: hiddenKubernetesPayload,
|
|
rawKubernetesObject: { secret: hiddenKubernetesPayload },
|
|
valuesPrinted: false,
|
|
},
|
|
},
|
|
}, { status: 502 });
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
},
|
|
});
|
|
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
|
const previousKey = process.env.HWLAB_API_KEY;
|
|
process.env.HWLAB_API_KEY = "secret-value";
|
|
const tempConfigPath = `/tmp/unidesk-agentrun-session-send-test-${process.pid}-${Date.now()}.yaml`;
|
|
try {
|
|
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
|
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
|
|
|
const planArgs = ["send", `session/${sessionId}`, "--prompt", "plan fixture", "--dry-run"];
|
|
const planHuman = renderedTextOf(await runAgentRunCommand(null, planArgs));
|
|
expect(planHuman).toContain("Session send plan");
|
|
expect(planHuman).toContain("Decision: turn");
|
|
expect(planHuman).toContain("ReuseRun: false");
|
|
expect(planHuman).toContain("CreateRunner: true");
|
|
expect(planHuman).toContain("Mutation: false");
|
|
expect(planHuman).not.toContain("[object Object]");
|
|
const planJsonText = renderedTextOf(await runAgentRunCommand(null, [...planArgs, "-o", "json"]));
|
|
const planYamlText = renderedTextOf(await runAgentRunCommand(null, [...planArgs, "-o", "yaml"]));
|
|
const planJson = JSON.parse(planJsonText) as Record<string, any>;
|
|
const planYaml = Bun.YAML.parse(planYamlText) as Record<string, any>;
|
|
expect(planYaml).toEqual(planJson);
|
|
expect(planJson).toMatchObject({ kind: "SessionSendPlan", decision: "turn", dryRun: true, mutation: false, partialWrite: false, reuseRun: false, createRunner: true });
|
|
expect(planJson.resources.session).toEqual({ kind: "session", id: sessionId });
|
|
expect(planJson.next.actions[0]).toMatchObject({ mutation: true });
|
|
expect(planJson.next.actions[0].command).not.toContain("--dry-run");
|
|
|
|
const failureArgs = ["send", `session/${sessionId}`, "--prompt", "failure fixture"];
|
|
const failureHumanResult = await runAgentRunCommand(null, failureArgs);
|
|
const failureHuman = renderedTextOf(failureHumanResult);
|
|
expect(failureHumanResult.ok).toBe(false);
|
|
expect(failureHuman).toContain("Session send failed");
|
|
expect(failureHuman).toContain("Error: validation-failed");
|
|
expect(failureHuman).toContain("TypedFailure: infra-failed");
|
|
expect(failureHuman).toContain("Code: runner-admission-failed");
|
|
expect(failureHuman).toContain("Reason: runner-retention-observation-failed");
|
|
expect(failureHuman).toContain("Mutation: true");
|
|
expect(failureHuman).toContain("PartialWrite: true");
|
|
expect(failureHuman).toContain(`session/${sessionId} state=running`);
|
|
expect(failureHuman).toContain(`run/${runId} status=queued`);
|
|
expect(failureHuman).toContain(`command/${commandId} state=pending type=turn`);
|
|
expect(failureHuman).not.toContain(hiddenKubernetesPayload);
|
|
expect(failureHuman).not.toContain("agentrun send");
|
|
|
|
const failureJsonResult = await runAgentRunCommand(null, [...failureArgs, "-o", "json"]);
|
|
const failureYamlResult = await runAgentRunCommand(null, [...failureArgs, "-o", "yaml"]);
|
|
const failureJsonText = renderedTextOf(failureJsonResult);
|
|
const failureYamlText = renderedTextOf(failureYamlResult);
|
|
const failureJson = JSON.parse(failureJsonText) as Record<string, any>;
|
|
const failureYaml = Bun.YAML.parse(failureYamlText) as Record<string, any>;
|
|
expect(failureJsonResult.ok).toBe(false);
|
|
expect(failureYamlResult.ok).toBe(false);
|
|
expect(failureYaml).toEqual(failureJson);
|
|
expect(failureJson).toMatchObject({
|
|
kind: "SessionSendError",
|
|
mutation: true,
|
|
partialWrite: true,
|
|
error: {
|
|
failureKind: "validation-failed",
|
|
typedFailureKind: "infra-failed",
|
|
code: "runner-admission-failed",
|
|
reason: "runner-retention-observation-failed",
|
|
traceId: "trc_session_send_fixture",
|
|
},
|
|
});
|
|
for (const bounded of [planHuman, planJsonText, planYamlText, failureHuman, failureJsonText, failureYamlText]) {
|
|
expect(Buffer.byteLength(bounded, "utf8")).toBeLessThan(7000);
|
|
expect(bounded).not.toContain(hiddenKubernetesPayload);
|
|
expect(bounded).not.toContain("rawKubernetesObject");
|
|
}
|
|
expect(postRequests).toBe(6);
|
|
} finally {
|
|
server.stop(true);
|
|
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
|
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
|
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
|
else process.env.HWLAB_API_KEY = previousKey;
|
|
rmSync(tempConfigPath, { force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("AgentRun YAML tool credential binding", () => {
|
|
test("aggregates managed GitHub SSH source keys into one volume credential and one sync target", () => {
|
|
const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" });
|
|
const githubSsh = agentRunToolCredentialRefs(spec).filter((credential) => credential.tool === "github" && credential.purpose === "github-ssh");
|
|
expect(githubSsh).toEqual([{
|
|
sourceId: "github-ssh",
|
|
tool: "github",
|
|
purpose: "github-ssh",
|
|
secretRef: {
|
|
namespace: "agentrun-v02",
|
|
name: "agentrun-v01-tool-github-ssh",
|
|
keys: ["id_ed25519", "known_hosts"],
|
|
},
|
|
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
|
|
valuesPrinted: false,
|
|
}]);
|
|
|
|
const sources = collectLaneSecretSources(spec).filter((source) => source.targetRef.name === "agentrun-v01-tool-github-ssh");
|
|
expect(sources.map((source) => ({ id: source.id, sourceMode: source.sourceMode, sourceRef: source.sourceRef, targetRef: source.targetRef }))).toEqual([
|
|
{
|
|
id: "tool-github-ssh-private-key",
|
|
sourceMode: "file",
|
|
sourceRef: "deploy-ssh/github.com/id_ed25519",
|
|
targetRef: { namespace: "agentrun-v02", name: "agentrun-v01-tool-github-ssh", key: "id_ed25519" },
|
|
},
|
|
{
|
|
id: "tool-github-ssh-known-hosts",
|
|
sourceMode: "file",
|
|
sourceRef: "deploy-ssh/github.com/known_hosts",
|
|
targetRef: { namespace: "agentrun-v02", name: "agentrun-v01-tool-github-ssh", key: "known_hosts" },
|
|
},
|
|
]);
|
|
const syncScript = secretSyncScript(spec, sources.map((source, index) => ({ targetRef: source.targetRef, value: `fixture-${index}` })));
|
|
const payloadMatch = syncScript.match(/^payload_b64='([^']+)'$/mu);
|
|
expect(payloadMatch).not.toBeNull();
|
|
const syncManifest = JSON.parse(Buffer.from(payloadMatch?.[1] ?? "", "base64").toString("utf8")) as Array<{ targetRef: { namespace: string; name: string; key: string } }>;
|
|
expect(syncManifest.map((item) => item.targetRef)).toEqual(sources.map((source) => source.targetRef));
|
|
expect(new Set(syncManifest.map((item) => `${item.targetRef.namespace}/${item.targetRef.name}`))).toEqual(new Set(["agentrun-v02/agentrun-v01-tool-github-ssh"]));
|
|
expect(syncScript).toContain("groups.setdefault(key");
|
|
expect(JSON.stringify({ githubSsh, sources })).not.toContain("/root/.ssh");
|
|
});
|
|
|
|
test("preserves explicit credential subsets across Aipod admission and rejects unknown identities or conflicts", async () => {
|
|
const renderBodies: Record<string, any>[] = [];
|
|
const submittedBodies: Record<string, any>[] = [];
|
|
let credentialMode: "all" | "subset" | "conflict" | "unknown" = "all";
|
|
const renderedToolCredentials = (): Record<string, unknown>[] => {
|
|
const credentials = [
|
|
{
|
|
tool: "github",
|
|
purpose: "github-pr",
|
|
secretRef: { name: "agentrun-v01-tool-github-pr", keys: ["GH_TOKEN"] },
|
|
projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN" },
|
|
},
|
|
{
|
|
tool: "unidesk-ssh",
|
|
purpose: "ssh-passthrough",
|
|
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" },
|
|
},
|
|
{
|
|
tool: "github",
|
|
purpose: "github-ssh",
|
|
secretRef: {
|
|
name: "agentrun-v01-tool-github-ssh",
|
|
keys: credentialMode === "conflict" ? ["id_ed25519", "known_hosts", "config"] : ["id_ed25519", "known_hosts"],
|
|
},
|
|
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
|
|
},
|
|
];
|
|
if (credentialMode === "subset") return credentials.slice(0, 1);
|
|
if (credentialMode === "unknown") {
|
|
return [{
|
|
tool: "artifact-store",
|
|
purpose: "publish",
|
|
secretRef: { name: "agentrun-artifact-store", keys: ["TOKEN"] },
|
|
projection: { kind: "env", envName: "ARTIFACT_TOKEN", secretKey: "TOKEN" },
|
|
}];
|
|
}
|
|
return credentials;
|
|
};
|
|
const renderedTask = () => ({
|
|
tenantId: "unidesk",
|
|
projectId: "pikasTech/unidesk",
|
|
queue: "commander",
|
|
lane: "v0.1",
|
|
backendProfile: "codex",
|
|
providerId: "NC01",
|
|
workspaceRef: { kind: "opaque", path: "." },
|
|
executionPolicy: {
|
|
sandbox: "workspace-write",
|
|
approval: "never",
|
|
timeoutMs: 1_800_000,
|
|
network: "enabled",
|
|
secretScope: {
|
|
allowCredentialEcho: false,
|
|
providerCredentials: [{
|
|
profile: "codex",
|
|
secretRef: { name: "agentrun-v01-provider-codex", keys: ["auth.json", "config.toml"] },
|
|
}],
|
|
toolCredentials: renderedToolCredentials(),
|
|
},
|
|
},
|
|
payload: { prompt: "fixture" },
|
|
});
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
async fetch(request) {
|
|
const url = new URL(request.url);
|
|
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
|
|
if (url.pathname === "/api/v1/aipod-specs/Artificer/render") {
|
|
renderBodies.push(await request.json() as Record<string, any>);
|
|
return Response.json({ ok: true, data: { queueTask: renderedTask() } });
|
|
}
|
|
if (url.pathname === "/api/v1/queue/tasks") {
|
|
submittedBodies.push(await request.json() as Record<string, any>);
|
|
return Response.json({ ok: true, data: { id: "qt_fixture", state: "queued" } });
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
},
|
|
});
|
|
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
|
const previousKey = process.env.HWLAB_API_KEY;
|
|
process.env.HWLAB_API_KEY = "secret-value";
|
|
const tempConfigPath = `/tmp/unidesk-agentrun-tool-credential-test-${process.pid}-${Date.now()}.yaml`;
|
|
try {
|
|
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
|
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
|
|
|
const accepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "--model", "gpt-explicit", "--reasoning-effort", "high", "-o", "json"]);
|
|
expect(accepted.ok).toBe(true);
|
|
expect(renderBodies).toHaveLength(1);
|
|
expect(submittedBodies).toHaveLength(1);
|
|
expect(renderBodies[0]?.backendProfile).toBeUndefined();
|
|
expect(renderBodies[0]?.executionPolicy).toBeUndefined();
|
|
expect(renderBodies[0]?.model).toBe("gpt-explicit");
|
|
expect(renderBodies[0]?.reasoningEffort).toBe("high");
|
|
expect(submittedBodies[0]).toEqual(renderedTask());
|
|
|
|
credentialMode = "subset";
|
|
const subsetAccepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
|
|
expect(subsetAccepted.ok).toBe(true);
|
|
expect(renderBodies).toHaveLength(2);
|
|
expect(submittedBodies).toHaveLength(2);
|
|
expect(submittedBodies[1]).toEqual(renderedTask());
|
|
|
|
credentialMode = "conflict";
|
|
const rejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
|
|
expect(rejected.ok).toBe(false);
|
|
expect(submittedBodies).toHaveLength(2);
|
|
expect(renderedTextOf(rejected)).toContain("github/github-ssh keys conflict");
|
|
|
|
credentialMode = "unknown";
|
|
const unknownRejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
|
|
expect(unknownRejected.ok).toBe(false);
|
|
expect(submittedBodies).toHaveLength(2);
|
|
expect(renderedTextOf(unknownRejected)).toContain("artifact-store/publish is not declared by YAML lane");
|
|
} finally {
|
|
server.stop(true);
|
|
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
|
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
|
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
|
else process.env.HWLAB_API_KEY = previousKey;
|
|
rmSync(tempConfigPath, { force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("AgentRun runner API key SecretRef", () => {
|
|
test("renders manager runtime authority from the YAML runner SecretRef", () => {
|
|
const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" });
|
|
const files = renderAgentRunGitopsFiles(spec, { sourceCommit: "a".repeat(40), image: placeholderAgentRunImage(spec, "a".repeat(40)) });
|
|
const managerFile = files.find((file) => file.path.endsWith("/mgr.yaml"));
|
|
expect(managerFile).toBeDefined();
|
|
const resources = (managerFile?.content ?? "").split(/\n---\n/u).map((document) => Bun.YAML.parse(document) as Record<string, unknown>);
|
|
const deployment = resources.find((resource) => resource.kind === "Deployment") as { spec?: { template?: { spec?: { containers?: Array<{ env?: Array<{ name?: string; value?: string }> }> } } } } | undefined;
|
|
const env = deployment?.spec?.template?.spec?.containers?.[0]?.env ?? [];
|
|
const values = new Map(env.map((item) => [item.name, item.value]));
|
|
expect(values.get("AGENTRUN_RUNNER_API_KEY_SECRET_NAME")).toBe(spec.deployment.runner.apiKeySecretRef.name);
|
|
expect(values.get("AGENTRUN_RUNNER_API_KEY_SECRET_KEY")).toBe(spec.deployment.runner.apiKeySecretRef.key);
|
|
expect(spec.secrets.some((secret) => secret.targetRef.name === "agentrun-v01-api-key")).toBe(false);
|
|
});
|
|
});
|