diff --git a/src/mgr/server.ts b/src/mgr/server.ts index 342e441..74656bd 100644 --- a/src/mgr/server.ts +++ b/src/mgr/server.ts @@ -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), diff --git a/src/runner/resource-bundle.ts b/src/runner/resource-bundle.ts index 1aa10b6..39dc131 100644 --- a/src/runner/resource-bundle.ts +++ b/src/runner/resource-bundle.ts @@ -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>(); - 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 { +async function checkoutGitSource(checkoutRoot: string, source: GitBundleSource, sourceContext: GitBundleSourceContext): Promise { 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): Promise { +async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource, sourceContext: GitBundleSourceContext) => Promise): Promise { 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}`, diff --git a/src/selftest/cases/60-hwlab-baseline-contract.ts b/src/selftest/cases/60-hwlab-baseline-contract.ts index d76b74f..944a254 100644 --- a/src/selftest/cases/60-hwlab-baseline-contract.ts +++ b/src/selftest/cases/60-hwlab-baseline-contract.ts @@ -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 { 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 }; diff --git a/src/selftest/cases/75-queue-q2-dispatch.ts b/src/selftest/cases/75-queue-q2-dispatch.ts index af94eb7..5be2a8f 100644 --- a/src/selftest/cases/75-queue-q2-dispatch.ts +++ b/src/selftest/cases/75-queue-q2-dispatch.ts @@ -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((resolve) => server.server.close(() => resolve())); } diff --git a/src/selftest/cases/76-aipod-spec.ts b/src/selftest/cases/76-aipod-spec.ts index ab13188..9b1471c 100644 --- a/src/selftest/cases/76-aipod-spec.ts +++ b/src/selftest/cases/76-aipod-spec.ts @@ -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 ")), true); assert.equal(commands.some((item) => item.includes("queue submit --aipod ")), 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((resolve) => server.server.close(() => resolve())); }