Merge pull request #295 from pikasTech/fix/294-resource-bundle-fetch
Pipelines as Code CI / agentrun-nc01-v02-ci-4805e02441f576ca19fae22a5815c7385f9ba110 Success
Pipelines as Code CI / agentrun-nc01-v02-ci-4805e02441f576ca19fae22a5815c7385f9ba110 Success
fix(v0.2): 补全 resource-bundle fetch 首错与任务失败投影
This commit is contained in:
@@ -363,10 +363,16 @@ async function enrichQueueTaskForRead(store: AgentRunStore, task: JsonRecord): P
|
||||
const activeSession = await queueTaskActiveSession(store, task);
|
||||
const supervisor = await queueTaskSupervisor(store, task, activeSession);
|
||||
const activeBySession = activeSession?.active === true || stringJsonValue(activeSession?.activeRunId) !== null || stringJsonValue(activeSession?.activeCommandId) !== null;
|
||||
const state = stringJsonValue(task.state);
|
||||
const terminalFailure = state === "failed" || state === "blocked" || state === "cancelled";
|
||||
const failureKind = terminalFailure ? stringJsonValue(supervisor?.failureKind) : null;
|
||||
const failureMessage = terminalFailure ? stringJsonValue(supervisor?.failureMessage) : null;
|
||||
return {
|
||||
...task,
|
||||
...(activeSession ? { activeSession } : {}),
|
||||
...(supervisor ? { supervisor } : {}),
|
||||
...(failureKind ? { failureKind } : {}),
|
||||
...(failureMessage ? { failureMessage } : {}),
|
||||
...(activeBySession ? { active: true, attentionState: "active-session" } : {}),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -401,6 +407,7 @@ async function queueTaskSupervisor(store: AgentRunStore, task: JsonRecord, activ
|
||||
status: stringJsonValue(result.status),
|
||||
terminalStatus: stringJsonValue(result.terminalStatus),
|
||||
failureKind: stringJsonValue(result.failureKind),
|
||||
failureMessage: boundedJsonString(result.failureMessage, 240),
|
||||
diagnosis: asJsonRecord(result.diagnosis),
|
||||
terminalClassification: terminalClassification ? compactTerminalClassification(terminalClassification) : null,
|
||||
phase: stringJsonValue(liveness?.phase),
|
||||
|
||||
+116
-22
@@ -65,6 +65,18 @@ interface GitBundleSource {
|
||||
commitId?: string;
|
||||
ref?: string;
|
||||
gitMirror?: GitMirrorConfig;
|
||||
credentialRefPresent: boolean;
|
||||
}
|
||||
|
||||
interface GitBundleSourceContext extends JsonRecord {
|
||||
kind: "resource-bundle" | "bundle";
|
||||
index: number | null;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
interface GitSourceRef {
|
||||
kind: "ref" | "commit";
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface MaterializedGitBundle {
|
||||
@@ -99,16 +111,16 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
const gitMirror = gitMirrorConfig(resourceBundleRef, env);
|
||||
const defaultSource = defaultGitBundleSource(resourceBundleRef, env, gitMirror);
|
||||
const checkoutCache = new Map<string, Promise<GitCheckout>>();
|
||||
const checkoutFor = (source: GitBundleSource) => {
|
||||
const checkoutFor = (source: GitBundleSource, sourceContext: GitBundleSourceContext) => {
|
||||
const key = stableHash(gitSourceIdentity(source));
|
||||
let checkout = checkoutCache.get(key);
|
||||
if (!checkout) {
|
||||
checkout = checkoutGitSource(checkoutRoot, source);
|
||||
checkout = checkoutGitSource(checkoutRoot, source, sourceContext);
|
||||
checkoutCache.set(key, checkout);
|
||||
}
|
||||
return checkout;
|
||||
};
|
||||
const defaultCheckout = await checkoutFor(defaultSource);
|
||||
const defaultCheckout = await checkoutFor(defaultSource, { kind: "resource-bundle", index: null, name: null });
|
||||
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
|
||||
const tools = await prepareGitBundleTools(workspacePath, env);
|
||||
const skills = await discoverGitBundleSkills(workspacePath, materializedBundles);
|
||||
@@ -168,37 +180,49 @@ function defaultGitMirrorConfig(env: NodeJS.ProcessEnv): GitMirrorConfig {
|
||||
|
||||
function defaultGitBundleSource(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv, gitMirror?: GitMirrorConfig): GitBundleSource {
|
||||
const ref = optionalNonEmpty(resourceBundleRef.ref) ?? optionalNonEmpty(env.AGENTRUN_RESOURCE_BUNDLE_REF) ?? optionalNonEmpty(env.AGENTRUN_WORKSPACE_REF) ?? optionalNonEmpty(env.AGENTRUN_WORKSPACE_BRANCH);
|
||||
if (ref) return { repoUrl: resourceBundleRef.repoUrl, ref, ...(gitMirror ? { gitMirror } : {}) };
|
||||
const credentialRefPresent = resourceBundleRef.credentialRef !== undefined;
|
||||
if (ref) return { repoUrl: resourceBundleRef.repoUrl, ref, credentialRefPresent, ...(gitMirror ? { gitMirror } : {}) };
|
||||
const commitId = optionalNonEmpty(resourceBundleRef.commitId);
|
||||
if (commitId) return { repoUrl: resourceBundleRef.repoUrl, commitId, ...(gitMirror ? { gitMirror } : {}) };
|
||||
return { repoUrl: resourceBundleRef.repoUrl, ref: "HEAD", ...(gitMirror ? { gitMirror } : {}) };
|
||||
if (commitId) return { repoUrl: resourceBundleRef.repoUrl, commitId, credentialRefPresent, ...(gitMirror ? { gitMirror } : {}) };
|
||||
return { repoUrl: resourceBundleRef.repoUrl, ref: "HEAD", credentialRefPresent, ...(gitMirror ? { gitMirror } : {}) };
|
||||
}
|
||||
|
||||
function bundleGitSource(bundle: ResourceBundleRef["bundles"][number], resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource): GitBundleSource {
|
||||
const repoUrl = bundle.repoUrl ?? resourceBundleRef.repoUrl;
|
||||
const ref = optionalNonEmpty(bundle.ref);
|
||||
const mirror = defaultSource.gitMirror;
|
||||
if (ref) return { repoUrl, ref, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
const credentialRefPresent = defaultSource.credentialRefPresent;
|
||||
if (ref) return { repoUrl, ref, credentialRefPresent, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
const commitId = optionalNonEmpty(bundle.commitId);
|
||||
if (commitId) return { repoUrl, commitId, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
if (commitId) return { repoUrl, commitId, credentialRefPresent, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
if (repoUrl === defaultSource.repoUrl) return defaultSource;
|
||||
if (defaultSource.ref) return { repoUrl, ref: defaultSource.ref, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
if (defaultSource.commitId) return { repoUrl, commitId: defaultSource.commitId, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
return { repoUrl, ref: "HEAD", ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
if (defaultSource.ref) return { repoUrl, ref: defaultSource.ref, credentialRefPresent, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
if (defaultSource.commitId) return { repoUrl, commitId: defaultSource.commitId, credentialRefPresent, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
return { repoUrl, ref: "HEAD", credentialRefPresent, ...(mirror ? { gitMirror: mirror } : {}) };
|
||||
}
|
||||
|
||||
async function checkoutGitSource(checkoutRoot: string, source: GitBundleSource): Promise<GitCheckout> {
|
||||
async function checkoutGitSource(checkoutRoot: string, source: GitBundleSource, sourceContext: GitBundleSourceContext): Promise<GitCheckout> {
|
||||
const checkoutPath = path.join(checkoutRoot, stableHash(gitSourceIdentity(source)).slice(0, 16));
|
||||
const fetch = gitFetchSource(source);
|
||||
const sourceRef: GitSourceRef = source.ref ? { kind: "ref", value: source.ref } : { kind: "commit", value: source.commitId ?? "" };
|
||||
const fetchOptions: GitCommandOptions = {
|
||||
operation: "fetch",
|
||||
repoUrl: source.repoUrl,
|
||||
fetchRepoUrl: fetch.fetchRepoUrl,
|
||||
mirrorUsed: fetch.mirrorUsed,
|
||||
sourceRef,
|
||||
sourceContext,
|
||||
credentialRefPresent: source.credentialRefPresent,
|
||||
};
|
||||
await mkdir(checkoutPath, { recursive: true });
|
||||
await git(["init"], checkoutPath);
|
||||
await git(["remote", "remove", "origin"], checkoutPath, { allowFailure: true });
|
||||
await git(["remote", "add", "origin", fetch.fetchRepoUrl], checkoutPath);
|
||||
if (source.ref) {
|
||||
await git(["fetch", "--depth", "1", "origin", source.ref], checkoutPath, { operation: "fetch", repoUrl: source.repoUrl, fetchRepoUrl: fetch.fetchRepoUrl });
|
||||
await git(["fetch", "--depth", "1", "origin", source.ref], checkoutPath, fetchOptions);
|
||||
await git(["checkout", "--detach", "FETCH_HEAD"], checkoutPath);
|
||||
} else if (source.commitId) {
|
||||
await git(["fetch", "--depth", "1", "origin", source.commitId], checkoutPath, { operation: "fetch", repoUrl: source.repoUrl, fetchRepoUrl: fetch.fetchRepoUrl });
|
||||
await git(["fetch", "--depth", "1", "origin", source.commitId], checkoutPath, fetchOptions);
|
||||
await git(["checkout", "--detach", source.commitId], checkoutPath);
|
||||
} else {
|
||||
throw new AgentRunError("schema-invalid", "gitbundle source must include repo ref or commit", { httpStatus: 400 });
|
||||
@@ -246,11 +270,11 @@ function cleanGithubPath(owner: string, repo: string): string | null {
|
||||
return `${cleanOwner}/${cleanRepo}`;
|
||||
}
|
||||
|
||||
async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
|
||||
async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource, sourceContext: GitBundleSourceContext) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
|
||||
const items: MaterializedGitBundle[] = [];
|
||||
for (const [index, bundle] of resourceBundleRef.bundles.entries()) {
|
||||
const gitSource = bundleGitSource(bundle, resourceBundleRef, defaultSource);
|
||||
const checkout = gitSource === defaultSource ? defaultCheckout : await checkoutFor(gitSource);
|
||||
const checkout = gitSource === defaultSource ? defaultCheckout : await checkoutFor(gitSource, { kind: "bundle", index, name: bundle.name ?? null });
|
||||
const source = resolveBundlePath(checkout.checkoutPath, bundle.subpath, `bundles[${index}].subpath`);
|
||||
const target = resolveWorkspaceTargetPath(workspacePath, bundle.targetPath, `bundles[${index}].target_path`);
|
||||
let sourceStat;
|
||||
@@ -563,7 +587,30 @@ function fileErrorSummary(error: unknown): JsonRecord {
|
||||
return { code: typeof record.code === "string" ? record.code : null, message: typeof record.message === "string" ? redactText(record.message).slice(0, 300) : null };
|
||||
}
|
||||
|
||||
async function git(args: string[], cwd: string, options: { allowFailure?: boolean; operation?: string; repoUrl?: string; fetchRepoUrl?: string } = {}): Promise<{ stdout: string; stderr: string }> {
|
||||
interface GitCommandOptions {
|
||||
allowFailure?: boolean;
|
||||
operation?: string;
|
||||
repoUrl?: string;
|
||||
fetchRepoUrl?: string;
|
||||
mirrorUsed?: boolean;
|
||||
sourceRef?: GitSourceRef;
|
||||
sourceContext?: GitBundleSourceContext;
|
||||
credentialRefPresent?: boolean;
|
||||
}
|
||||
|
||||
export function classifyGitStderr(stderr: string): string {
|
||||
const text = stderr.trim();
|
||||
if (/repository\s+['"].+['"]\s+not found|repository not found/iu.test(text)) return "repository-not-found";
|
||||
if (/couldn't find remote ref|remote ref .+ not found|not our ref|unadvertised object/iu.test(text)) return "source-ref-not-found";
|
||||
if (/authentication failed|permission denied|could not read username|access denied|http[^\n]*\b(?:401|403)\b/iu.test(text)) return "authentication-failed";
|
||||
if (/could not resolve host|name or service not known|temporary failure in name resolution/iu.test(text)) return "dns-failed";
|
||||
if (/failed to connect|connection refused|connection timed out|network is unreachable|no route to host/iu.test(text)) return "connection-failed";
|
||||
if (/certificate|\bssl\b|\btls\b/iu.test(text)) return "tls-failed";
|
||||
if (/early eof|rpc failed|remote end hung up|http[^\n]*\b5\d\d\b/iu.test(text)) return "transport-failed";
|
||||
return text.length === 0 ? "stderr-empty" : "unclassified";
|
||||
}
|
||||
|
||||
async function git(args: string[], cwd: string, options: GitCommandOptions = {}): Promise<{ stdout: string; stderr: string }> {
|
||||
const env = gitCommandEnv(process.env);
|
||||
const child = spawn("git", gitArgs(args), { cwd, stdio: ["ignore", "pipe", "pipe"], env });
|
||||
let stdout = "";
|
||||
@@ -596,20 +643,43 @@ async function git(args: string[], cwd: string, options: { allowFailure?: boolea
|
||||
clearTimeout(timeout);
|
||||
if (result.code !== 0 && !options.allowFailure) {
|
||||
const failureKind = timedOut ? "backend-timeout" : "infra-failed";
|
||||
throw new AgentRunError(failureKind, `git ${args[0] ?? "command"} ${timedOut ? `timed out after ${timeoutMs}ms` : `failed with code ${result.code}`}`, {
|
||||
const stderrCategory = timedOut ? "operation-timeout" : classifyGitStderr(stderr);
|
||||
throw new AgentRunError(failureKind, `git ${args[0] ?? "command"} ${timedOut ? `timed out after ${timeoutMs}ms` : `failed with code ${result.code} (${stderrCategory})`}`, {
|
||||
httpStatus: timedOut ? 504 : 502,
|
||||
details: {
|
||||
operation: options.operation ?? args[0] ?? "git",
|
||||
repoUrl: options.repoUrl ?? null,
|
||||
fetchRepoUrl: options.fetchRepoUrl ?? null,
|
||||
stage: options.operation === "fetch" ? "git-fetch" : `git-${options.operation ?? args[0] ?? "command"}`,
|
||||
source: {
|
||||
bundle: options.sourceContext ?? null,
|
||||
sourceRef: options.sourceRef ? { present: true, kind: options.sourceRef.kind, value: options.sourceRef.value, valuesPrinted: false } : { present: false, kind: null, value: null, valuesPrinted: false },
|
||||
credentialRefPresent: options.credentialRefPresent === true,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
remote: {
|
||||
declared: gitRemoteSummary(options.repoUrl),
|
||||
fetch: gitRemoteSummary(options.fetchRepoUrl),
|
||||
mirrorUsed: options.mirrorUsed === true,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
stderr: {
|
||||
category: stderrCategory,
|
||||
firstLineCategory: classifyGitStderr(firstNonEmptyLine(stderr)),
|
||||
bytes: Buffer.byteLength(stderr, "utf8"),
|
||||
lines: nonEmptyLineCount(stderr),
|
||||
truncated: stderr.length > 4000,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
stdout: {
|
||||
bytes: Buffer.byteLength(stdout, "utf8"),
|
||||
truncated: stdout.length > 1000,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
timeoutMs,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
protocol: defaultGitHttpVersion,
|
||||
terminalPrompt: false,
|
||||
credentialHelper: "gh-auth-setup-git",
|
||||
proxyDecision: proxyDecisionForUrl(options.fetchRepoUrl ?? options.repoUrl ?? ""),
|
||||
stderr: redactText(stderr.slice(-4000)),
|
||||
stdout: redactText(stdout.slice(-1000)),
|
||||
signal: timedOut ? "SIGTERM" : result.signal,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
@@ -618,6 +688,30 @@ async function git(args: string[], cwd: string, options: { allowFailure?: boolea
|
||||
return { stdout, stderr };
|
||||
}
|
||||
|
||||
function gitRemoteSummary(value: string | undefined): JsonRecord | null {
|
||||
const remote = optionalNonEmpty(value);
|
||||
if (!remote) return null;
|
||||
const scp = /^(?:[^@\s]+@)?([^:/\s]+):/u.exec(remote);
|
||||
let scheme = scp ? "ssh" : "file";
|
||||
let host: string | null = scp?.[1] ?? null;
|
||||
try {
|
||||
const parsed = new URL(remote);
|
||||
scheme = parsed.protocol.replace(/:$/u, "") || scheme;
|
||||
host = parsed.hostname || null;
|
||||
} catch {
|
||||
// Local paths and SCP-style SSH remotes are summarized without parsing their path.
|
||||
}
|
||||
return { scheme, host, fingerprint: sha256Text(remote), valuesPrinted: false };
|
||||
}
|
||||
|
||||
function firstNonEmptyLine(value: string): string {
|
||||
return value.split(/\r?\n/u).find((line) => line.trim().length > 0)?.trim() ?? "";
|
||||
}
|
||||
|
||||
function nonEmptyLineCount(value: string): number {
|
||||
return value.split(/\r?\n/u).filter((line) => line.trim().length > 0).length;
|
||||
}
|
||||
|
||||
function gitArgs(args: string[]): string[] {
|
||||
return [
|
||||
"-c", `http.version=${defaultGitHttpVersion}`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { promisify } from "node:util";
|
||||
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -178,9 +179,17 @@ async function assertSessionProfileSwitchAllowed(client: ManagerClient, context:
|
||||
|
||||
async function assertResourceBundleFailure(client: ManagerClient, context: SelfTestContext, managerUrl: string): Promise<void> {
|
||||
const repo = await createLocalGitRepo(context);
|
||||
const missingCommit = "0000000000000000000000000000000000000000";
|
||||
const run = await client.post("/api/v1/runs", {
|
||||
...runPayload(context, "codex", "selftest-bad-bundle-session"),
|
||||
resourceBundleRef: { kind: "gitbundle", repoUrl: repo.repoUrl, commitId: "0000000000000000000000000000000000000000", bundles: [{ name: "tools", subpath: "tools", targetPath: "tools" }], submodules: false, lfs: false },
|
||||
resourceBundleRef: {
|
||||
kind: "gitbundle",
|
||||
repoUrl: repo.repoUrl,
|
||||
commitId: repo.commitId,
|
||||
bundles: [{ name: "missing-tools", repoUrl: repo.repoUrl, commitId: missingCommit, subpath: "tools", targetPath: "tools" }],
|
||||
submodules: false,
|
||||
lfs: false,
|
||||
},
|
||||
}) as { id: string };
|
||||
const command = await client.post(`/api/v1/runs/${run.id}/commands`, { type: "turn", payload: { prompt: "bad bundle" }, idempotencyKey: "selftest-bad-bundle" }) as { id: string };
|
||||
const result = await runOnce({ managerUrl, runId: run.id, commandId: command.id, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "bad-bundle-workspaces") }, oneShot: true }) as JsonRecord;
|
||||
@@ -189,6 +198,33 @@ async function assertResourceBundleFailure(client: ManagerClient, context: SelfT
|
||||
const envelope = await client.get(`/api/v1/runs/${run.id}/commands/${command.id}/result`) as JsonRecord;
|
||||
assert.equal(envelope.completed, false);
|
||||
assert.equal(envelope.failureKind, "infra-failed");
|
||||
const failureDetails = envelope.failureDetails as JsonRecord;
|
||||
const source = failureDetails.source as JsonRecord;
|
||||
const bundle = source.bundle as JsonRecord;
|
||||
const sourceRef = source.sourceRef as JsonRecord;
|
||||
const remote = failureDetails.remote as JsonRecord;
|
||||
const stderr = failureDetails.stderr as JsonRecord;
|
||||
const stdout = failureDetails.stdout as JsonRecord;
|
||||
assert.equal(failureDetails.stage, "git-fetch");
|
||||
assert.deepEqual(bundle, { kind: "bundle", index: 0, name: "missing-tools" });
|
||||
assert.equal(sourceRef.present, true);
|
||||
assert.equal(sourceRef.kind, "commit");
|
||||
assert.equal(sourceRef.value, missingCommit);
|
||||
assert.equal(source.credentialRefPresent, false);
|
||||
assert.equal(remote.mirrorUsed, false);
|
||||
assert.equal((remote.declared as JsonRecord).scheme, "file");
|
||||
const expectedRemoteFingerprint = createHash("sha256").update(repo.repoUrl, "utf8").digest("hex");
|
||||
assert.equal((remote.declared as JsonRecord).fingerprint, expectedRemoteFingerprint);
|
||||
assert.equal((remote.fetch as JsonRecord).fingerprint, expectedRemoteFingerprint);
|
||||
assert.equal(stderr.category, "source-ref-not-found");
|
||||
assert.equal(stderr.firstLineCategory, "source-ref-not-found");
|
||||
assert.equal(typeof stderr.bytes, "number");
|
||||
assert.equal(typeof stderr.lines, "number");
|
||||
assert.equal(Object.hasOwn(failureDetails, "repoUrl"), false);
|
||||
assert.equal(Object.hasOwn(stderr, "text"), false);
|
||||
assert.equal(Object.hasOwn(stdout, "text"), false);
|
||||
assert.equal(JSON.stringify(failureDetails).includes(repo.repoUrl), false);
|
||||
assertNoSecretLeak(failureDetails);
|
||||
const commandRecord = await client.get(`/api/v1/runs/${run.id}/commands/${command.id}`) as { state?: string };
|
||||
assert.equal(commandRecord.state, "failed");
|
||||
const runRecord = await client.get(`/api/v1/runs/${run.id}`) as { status?: string; terminalStatus?: string; failureKind?: string };
|
||||
|
||||
@@ -246,9 +246,12 @@ process.exit(1);
|
||||
const blockedDispatched = await client.post(`/api/v1/queue/tasks/${blockedCreated.id}/dispatch`, { attemptId: "attempt_queue_q2_blocked_selftest" }) as QueueDispatchResult;
|
||||
await client.patch(`/api/v1/commands/${blockedDispatched.command.id}/status`, { terminalStatus: "blocked", failureKind: "required-skill-unavailable", failureMessage: "missing required skill" });
|
||||
await client.patch(`/api/v1/runs/${blockedDispatched.run.id}/status`, { terminalStatus: "blocked", failureKind: "required-skill-unavailable", failureMessage: "missing required skill" });
|
||||
const blockedShown = await client.get(`/api/v1/queue/tasks/${blockedCreated.id}`) as QueueTaskRecord;
|
||||
const blockedShown = await client.get(`/api/v1/queue/tasks/${blockedCreated.id}`) as QueueTaskRecord & { failureKind?: string; failureMessage?: string; supervisor?: JsonRecord };
|
||||
assert.equal(blockedShown.state, "blocked");
|
||||
assert.equal(blockedShown.latestAttempt?.state, "blocked");
|
||||
assert.equal(blockedShown.failureKind, "required-skill-unavailable");
|
||||
assert.equal(blockedShown.failureMessage, "missing required skill");
|
||||
assert.equal(blockedShown.supervisor?.failureKind, "required-skill-unavailable");
|
||||
|
||||
const cancelCreated = await client.post("/api/v1/queue/tasks", {
|
||||
tenantId: "unidesk",
|
||||
@@ -296,7 +299,7 @@ process.exit(1);
|
||||
assert.ok(JSON.stringify(cancelManifest).includes(cancelDispatched.run.id));
|
||||
assertNoSecretLeak(dispatched);
|
||||
assertNoSecretLeak(cancelled);
|
||||
return { name: "queue-q2-dispatch", tests: ["queue-cli-json-stdin-dry-run", "queue-dispatch-run-command-runner-job", "queue-read-views-refresh-terminal-state", "queue-refresh-from-core-status", "queue-dispatch-no-repeat", "queue-unidesk-ssh-endpoint-auto-env", "queue-blocked-run-state-wins-over-command-failed", "queue-cancel-propagates-to-run-command-session"] };
|
||||
return { name: "queue-q2-dispatch", tests: ["queue-cli-json-stdin-dry-run", "queue-dispatch-run-command-runner-job", "queue-read-views-refresh-terminal-state", "queue-refresh-from-core-status", "queue-dispatch-no-repeat", "queue-unidesk-ssh-endpoint-auto-env", "queue-blocked-run-state-wins-over-command-failed", "queue-read-projects-terminal-failure", "queue-cancel-propagates-to-run-command-session"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { startManagerServer } from "../../mgr/server.js";
|
||||
import { MemoryAgentRunStore } from "../../mgr/store.js";
|
||||
import type { JsonRecord } from "../../common/types.js";
|
||||
import { validateResourceBundleRef } from "../../common/validation.js";
|
||||
import { resolveGitBundleFetchSource } from "../../runner/resource-bundle.js";
|
||||
import { classifyGitStderr, resolveGitBundleFetchSource } from "../../runner/resource-bundle.js";
|
||||
import { assertNoSecretLeak, loadArtificerImageRef, type SelfTestCase } from "../harness.js";
|
||||
|
||||
const selfTest: SelfTestCase = async (context) => {
|
||||
@@ -79,6 +79,9 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
const nonGithub = resolveGitBundleFetchSource("ssh://git@example.test/repo.git", { baseUrl: "http://mirror.example.test" }, {});
|
||||
assert.equal(nonGithub.fetchRepoUrl, "ssh://git@example.test/repo.git");
|
||||
assert.equal(nonGithub.mirrorUsed, false);
|
||||
assert.equal(classifyGitStderr("fatal: repository 'http://mirror.example.test/pikasTech/agent_skills.git/' not found\n"), "repository-not-found");
|
||||
assert.equal(classifyGitStderr("fatal: couldn't find remote ref missing-branch\n"), "source-ref-not-found");
|
||||
assert.equal(classifyGitStderr("fatal: Authentication failed for 'https://example.test/repo.git/'\n"), "authentication-failed");
|
||||
assert.throws(() => validateResourceBundleRef({ kind: "gitbundle", repoUrl: "git@github.com:pikasTech/unidesk.git", ref: "master", gitMirror: { enabled: false }, bundles: [{ subpath: ".", targetPath: "." }] }), /resourceBundleRef.gitMirror is removed/u);
|
||||
|
||||
const submitPlan = await runCliJson(context, server.baseUrl, ["queue", "submit", "--aipod", "Artificer", "--prompt", "处理 pikasTech/unidesk#245", "--idempotency-key", "selftest-aipod-cli", "--dry-run"]);
|
||||
@@ -93,7 +96,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-image-ref-render", "aipod-spec-artificer-github-url-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-github-url-render", "aipod-spec-git-mirror-url", "git-fetch-stderr-classification", "queue-submit-aipod-dry-run", "aipod-cli-help"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user