fix: normalize empty device job blockers

This commit is contained in:
Codex
2026-05-29 20:18:09 +08:00
parent 23d286086f
commit be50c2fde4
2 changed files with 14 additions and 5 deletions
+7
View File
@@ -458,9 +458,16 @@ test("cloud api dispatches authorized device jobs to the internal device-pod exe
const output = await getJson(port, `/v1/device-pods/device-pod-71-freq/jobs/${runningJob.body.job.id}/output`, aliceLogin.cookie);
assert.equal(output.status, 200);
assert.equal(output.body.status, "completed");
assert.equal(output.body.blocker, null);
assert.equal(output.body.freshness.stale, false);
assert.equal(output.body.output.text, "executor output");
assert.equal(output.body.truncation.truncated, false);
assert.ok(executorRequests.some((item) => item.method === "GET" && item.url.endsWith(`/jobs/${runningJob.body.job.id}/output`)));
const events = await getJson(port, "/v1/device-pods/device-pod-71-freq/events", aliceLogin.cookie);
const completedEvent = events.body.events.find((event) => event.refs.jobId === runningJob.body.job.id);
assert.equal(completedEvent.status, "completed");
assert.equal(completedEvent.blocker, null);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => executor.close((error) => (error ? reject(error) : resolve())));
+7 -5
View File
@@ -675,8 +675,8 @@ class AccessController {
scope: "job",
intent: job.intent,
status: job.status,
summary: job.blocker?.summary ?? `job ${job.id} ${job.status}`,
blocker: job.blocker,
summary: normalizeBlocker(job.blocker)?.summary ?? `job ${job.id} ${job.status}`,
blocker: normalizeBlocker(job.blocker),
refs: jobRefs(job)
}));
return {
@@ -952,6 +952,7 @@ class AccessController {
}
jobPayload(job, pod, { lease = null } = {}) {
const blocker = normalizeBlocker(job.blocker);
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: DEVICE_JOB_CONTRACT_VERSION,
@@ -964,8 +965,8 @@ class AccessController {
operationId: job.operationId,
job: publicJob(job),
lease: lease ? publicDeviceLease(lease) : null,
blocker: job.blocker,
freshness: freshness(job.updatedAt, job.blocker),
blocker,
freshness: freshness(job.updatedAt, blocker),
outputUrl: `/v1/device-pods/${encodeURIComponent(pod.id)}/jobs/${encodeURIComponent(job.id)}/output`,
cancelUrl: `/v1/device-pods/${encodeURIComponent(pod.id)}/jobs/${encodeURIComponent(job.id)}/cancel`
};
@@ -1273,10 +1274,11 @@ function pgUser(row) { return row ? { id: row.id, username: row.username, displa
function pgSession(row) { return { id: row.id, userId: row.user_id, tokenHash: row.session_token_hash, createdAt: row.created_at, lastSeenAt: row.last_seen_at, expiresAt: row.expires_at, revokedAt: row.revoked_at, user: { id: row.user_id, username: row.username, displayName: row.display_name, role: row.role, status: row.status, passwordHash: row.password_hash, createdAt: row.user_created_at, updatedAt: row.user_updated_at } }; }
function pgDevicePod(row) { return { id: row.id, name: row.name, status: row.status, profile: parseJson(row.profile_json, {}), profileHash: row.profile_hash, createdAt: row.created_at, updatedAt: row.updated_at }; }
function pgLease(row) { return row ? { devicePodId: row.device_pod_id, holderSessionId: row.holder_session_id, holderUserId: row.holder_user_id, leaseTokenHash: row.lease_token_hash, createdAt: row.created_at, expiresAt: row.expires_at, releasedAt: row.released_at } : null; }
function pgJob(row) { return row ? { id: row.id, devicePodId: row.device_pod_id, ownerUserId: row.owner_user_id, status: row.status, intent: row.intent, args: parseJson(row.args_json, {}), reason: row.reason, traceId: row.trace_id, operationId: row.operation_id, output: parseJson(row.output_json, {}), blocker: parseJson(row.blocker_json, null), createdAt: row.created_at, updatedAt: row.updated_at, completedAt: row.completed_at } : null; }
function pgJob(row) { return row ? { id: row.id, devicePodId: row.device_pod_id, ownerUserId: row.owner_user_id, status: row.status, intent: row.intent, args: parseJson(row.args_json, {}), reason: row.reason, traceId: row.trace_id, operationId: row.operation_id, output: parseJson(row.output_json, {}), blocker: normalizeBlocker(parseJson(row.blocker_json, null)), createdAt: row.created_at, updatedAt: row.updated_at, completedAt: row.completed_at } : null; }
function pgAgentSession(row) { return row ? { id: row.id, projectId: row.project_id, agentId: row.agent_id, status: row.status, startedAt: row.started_at, endedAt: row.ended_at, ownerUserId: row.owner_user_id, conversationId: row.conversation_id, threadId: row.thread_id, lastTraceId: row.last_trace_id, session: parseJson(row.session_json, {}), updatedAt: row.updated_at } : null; }
function jobParams(job) { return [job.id, job.devicePodId, job.ownerUserId, job.status, job.intent, stableJson(job.args), job.reason, job.traceId, job.operationId, stableJson(job.output), stableJson(job.blocker ?? {}), job.createdAt, job.updatedAt, job.completedAt]; }
function parseJson(value, fallback) { if (!value) return fallback; if (typeof value === "object") return value; try { return JSON.parse(String(value)); } catch { return fallback; } }
function normalizeBlocker(value) { return value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? value : null; }
async function fetchJsonWithTimeout(fetchImpl, url, options, timeoutMs) {
const controller = new AbortController();