fix: wait for device-pod build verify output (#830)
Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
@@ -2054,6 +2054,60 @@ test("cloud api dispatches authorized device jobs to the internal device-pod exe
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api waits longer for synchronous device-pod build verify output (HWLAB #821)", async () => {
|
||||
const executor = createServer(async (request, response) => {
|
||||
const body = await requestJson(request);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
const text = JSON.stringify({ ok: true, action: "workspace.build.verify", data: { buildJob: { jobId: "keil-build-slow" }, artifacts: [], missing: [] } });
|
||||
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
||||
response.end(JSON.stringify({
|
||||
accepted: true,
|
||||
status: "completed",
|
||||
contractVersion: "device-pod-executor-v1",
|
||||
devicePodId: "device-pod-71-freq",
|
||||
traceId: body.traceId,
|
||||
operationId: body.operationId,
|
||||
job: { id: body.jobId, devicePodId: body.devicePodId, status: "completed", intent: body.intent, reason: body.reason, traceId: body.traceId, operationId: body.operationId },
|
||||
output: { text, bytes: Buffer.byteLength(text, "utf8"), truncation: { maxBytes: 12000, truncated: false } }
|
||||
}));
|
||||
});
|
||||
await new Promise((resolve) => executor.listen(0, "127.0.0.1", resolve));
|
||||
const executorPort = executor.address().port;
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
|
||||
HWLAB_DEVICE_POD_INTERNAL_TOKEN: INTERNAL_TOKEN,
|
||||
HWLAB_DEVICE_POD_URL: `http://127.0.0.1:${executorPort}`
|
||||
},
|
||||
now: () => "2026-05-28T00:00:00.000Z"
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
|
||||
const userCreate = await postJson(port, "/v1/admin/users", { username: "slow-alice", password: "alice-pass" }, adminLogin.cookie);
|
||||
await postJson(port, "/v1/admin/device-pods", {
|
||||
devicePodId: "device-pod-71-freq",
|
||||
profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } }
|
||||
}, adminLogin.cookie);
|
||||
await postJson(port, "/v1/admin/device-pod-grants", { devicePodId: "device-pod-71-freq", userId: userCreate.body.user.id }, adminLogin.cookie);
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "slow-alice", password: "alice-pass" });
|
||||
|
||||
const verifyJob = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", { intent: "workspace.evidence", args: { kind: "verify", expected: "axf,hex" } }, aliceLogin.cookie);
|
||||
assert.equal(verifyJob.status, 200);
|
||||
assert.equal(verifyJob.body.status, "completed");
|
||||
const verify = JSON.parse(verifyJob.body.output.text);
|
||||
assert.equal(verify.action, "workspace.build.verify");
|
||||
assert.equal(verify.data.buildJob.jobId, "keil-build-slow");
|
||||
} 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())));
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api bounds device-pod job output payloads", async () => {
|
||||
const longText = "x".repeat(65000);
|
||||
const executor = createServer(async (request, response) => {
|
||||
|
||||
@@ -221,7 +221,7 @@ function accessStoreForRuntime(runtimeStore, options = {}) {
|
||||
}
|
||||
|
||||
class AccessController {
|
||||
constructor({ store, env = process.env, gatewayRegistry = null, fetchImpl = fetch, traceStore = null, codeAgentChatResults = null, devicePodExecutorUrl = env.HWLAB_DEVICE_POD_URL, devicePodExecutorTimeoutMs = 1200, now = () => new Date().toISOString(), required = truthyFlag(env.HWLAB_ACCESS_CONTROL_REQUIRED) } = {}) {
|
||||
constructor({ store, env = process.env, gatewayRegistry = null, fetchImpl = fetch, traceStore = null, codeAgentChatResults = null, devicePodExecutorUrl = env.HWLAB_DEVICE_POD_URL, devicePodExecutorTimeoutMs = 1200, devicePodSyncTimeoutMs = env.HWLAB_DEVICE_POD_SYNC_TIMEOUT_MS ?? 120000, now = () => new Date().toISOString(), required = truthyFlag(env.HWLAB_ACCESS_CONTROL_REQUIRED) } = {}) {
|
||||
this.store = store;
|
||||
this.env = env;
|
||||
this.gatewayRegistry = gatewayRegistry;
|
||||
@@ -233,6 +233,7 @@ class AccessController {
|
||||
this.devicePodInternalToken = textOr(env.HWLAB_DEVICE_POD_INTERNAL_TOKEN, "");
|
||||
this.devicePodApiKey = textOr(env.HWLAB_DEVICE_POD_API_KEY, "");
|
||||
this.devicePodExecutorTimeoutMs = Number.parseInt(String(devicePodExecutorTimeoutMs), 10) || 1200;
|
||||
this.devicePodSyncTimeoutMs = Number.parseInt(String(devicePodSyncTimeoutMs), 10) || 120000;
|
||||
this.now = now;
|
||||
this.required = required;
|
||||
this.bootstrapAttempted = false;
|
||||
@@ -1587,7 +1588,7 @@ class AccessController {
|
||||
traceId: job.traceId,
|
||||
operationId: job.operationId
|
||||
})
|
||||
}, this.devicePodExecutorTimeoutMs);
|
||||
}, devicePodExecutorCreateTimeoutMs(this, job));
|
||||
const updated = await this.updateDevicePodJobFromExecutorResponse({ job, pod, response });
|
||||
const status = updated.status;
|
||||
return { job: updated ?? job, httpStatus: response.status >= 400 ? response.status : status === "running" ? 202 : 200 };
|
||||
@@ -2244,6 +2245,8 @@ function redactedProfile(profile = {}) { return { schemaVersion: profile.schemaV
|
||||
function targetIdFromProfile(profile = {}) { return profile.target?.id ?? profile.targetId ?? null; }
|
||||
function terminalJobStatus(status) { return ["completed", "failed", "blocked", "canceled"].includes(status); }
|
||||
function shouldReturnDevicePodCreateOutput(job) { return terminalJobStatus(job?.status) && job?.intent === "workspace.evidence" && textOr(normalizeObject(job.args).kind, "") === "verify"; }
|
||||
function shouldSynchronouslyWaitForDevicePodJob(job) { return job?.intent === "workspace.evidence" && textOr(normalizeObject(job.args).kind, "") === "verify"; }
|
||||
function devicePodExecutorCreateTimeoutMs(controller, job) { return shouldSynchronouslyWaitForDevicePodJob(job) ? controller.devicePodSyncTimeoutMs : controller.devicePodExecutorTimeoutMs; }
|
||||
function publicJob(job) { return { id: job.id, devicePodId: job.devicePodId, ownerUserId: job.ownerUserId, status: job.status, intent: job.intent, reason: job.reason, traceId: job.traceId, operationId: job.operationId, createdAt: job.createdAt, updatedAt: job.updatedAt, completedAt: job.completedAt }; }
|
||||
function jobRefs(job) { return { jobId: job.id, traceId: job.traceId, operationId: job.operationId }; }
|
||||
function formatEventLine(event) { return [event.ts?.slice(11, 19) ?? "00:00:00", event.scope?.toUpperCase() ?? "JOB", event.status, event.intent, event.summary, event.refs?.traceId ? `trace=${event.refs.traceId}` : null, event.blocker?.code ? `blocker=${event.blocker.code}` : null].filter(Boolean).join(" "); }
|
||||
|
||||
Reference in New Issue
Block a user