Merge pull request #828 from pikasTech/fix/issue820-ls-output-visibility

fix(v02): widen device-pod job output visibility
This commit is contained in:
Lyon
2026-06-04 16:18:52 +08:00
committed by GitHub
6 changed files with 35 additions and 17 deletions
+5 -5
View File
@@ -142,7 +142,7 @@ test("device pod executor lists and isolates multiple logical device pods", asyn
});
test("device-pod executor bounds gateway output text", async () => {
const longText = "z".repeat(25000);
const longText = "z".repeat(65000);
const cloudApi = createServer(async (request, response) => {
const body = await requestJson(request);
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
@@ -184,11 +184,11 @@ test("device-pod executor bounds gateway output text", async () => {
});
assert.equal(jobResponse.status, 202);
const output = await waitForJobStatus(service.port, "device-pod-test", "job_bounded_output_test", "completed");
assert.equal(output.output.text.length, 24000);
assert.equal(output.text.length, 24000);
assert.equal(output.bytes, 24000);
assert.equal(output.output.text.length, 64000);
assert.equal(output.text.length, 64000);
assert.equal(output.bytes, 64000);
assert.equal(output.truncation.truncated, true);
assert.equal(output.truncation.originalBytes, 25000);
assert.equal(output.truncation.originalBytes, 65000);
assert.equal(output.output.omitted.reason, "device_job_output_truncated");
assert.equal(JSON.stringify(output).includes(longText), false);
} finally {
+1 -1
View File
@@ -15,7 +15,7 @@ const environment = process.env.HWLAB_ENVIRONMENT || process.env.HWLAB_GITOPS_PR
const cloudApiInternalUrl = normalizeBaseUrl(process.env.HWLAB_CLOUD_API_INTERNAL_URL || process.env.HWLAB_CLOUD_API_URL);
const internalToken = textOr(process.env.HWLAB_DEVICE_POD_INTERNAL_TOKEN, "");
const dispatchTimeoutMs = numberOr(process.env.HWLAB_DEVICE_POD_GATEWAY_DISPATCH_TIMEOUT_MS, 120000);
const DEVICE_JOB_OUTPUT_MAX_BYTES = 24000;
const DEVICE_JOB_OUTPUT_MAX_BYTES = 64000;
const jobs = new Map();
listen(createServer(async (request, response) => {
+5 -5
View File
@@ -2055,7 +2055,7 @@ test("cloud api dispatches authorized device jobs to the internal device-pod exe
});
test("cloud api bounds device-pod job output payloads", async () => {
const longText = "x".repeat(25000);
const longText = "x".repeat(65000);
const executor = createServer(async (request, response) => {
const body = await requestJson(request);
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
@@ -2100,11 +2100,11 @@ test("cloud api bounds device-pod job output payloads", async () => {
assert.equal(job.status, 200);
const output = await getJson(port, `/v1/device-pods/device-pod-71-freq/jobs/${job.body.job.id}/output`, aliceLogin.cookie);
assert.equal(output.status, 200);
assert.equal(output.body.bytes, 24000);
assert.equal(output.body.text.length, 24000);
assert.equal(output.body.output.text.length, 24000);
assert.equal(output.body.bytes, 64000);
assert.equal(output.body.text.length, 64000);
assert.equal(output.body.output.text.length, 64000);
assert.equal(output.body.truncation.truncated, true);
assert.equal(output.body.truncation.originalBytes, 25000);
assert.equal(output.body.truncation.originalBytes, 65000);
assert.equal(output.body.output.omitted.reason, "device_job_output_truncated");
assert.equal(JSON.stringify(output.body).includes(longText), false);
} finally {
+1 -1
View File
@@ -202,7 +202,7 @@ const DEVICE_JOB_ACTIONABLE_INTENTS = new Set([
"workspace.build",
"debug.download"
]);
const DEVICE_JOB_OUTPUT_MAX_BYTES = 24000;
const DEVICE_JOB_OUTPUT_MAX_BYTES = 64000;
const DEVICE_POD_PROFILE_SECRET_KEY_PATTERN = /(?:token|secret|password|passphrase|credential|api[_-]?key|access[_-]?key|private[_-]?key|git[_-]?key|cloud[_-]?token|kubeconfig|database[_-]?url|db[_-]?url|connection[_-]?string)/iu;
const DEVICE_POD_PROFILE_SECRET_VALUE_PATTERN = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|postgres(?:ql)?:\/\/|mysql:\/\/|mongodb(?:\+srv)?:\/\/|redis:\/\/|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})/u;
+7 -3
View File
@@ -290,6 +290,7 @@ test("device-pod-cli bootsharp creates a formal workspace bootsharp REST job", a
});
test("device-pod-cli job output is compact by default and full with --full", async () => {
const longText = `${"x".repeat(30000)}FREQ_Controller_FW.axf${"y".repeat(30000)}FREQ_Controller_FW.hex${"z".repeat(30000)}`;
const payload = {
serviceId: "hwlab-cloud-api",
contractVersion: "device-pod-job-v1",
@@ -299,9 +300,9 @@ test("device-pod-cli job output is compact by default and full with --full", asy
operationId: "op_job",
job: { id: "job_1", status: "completed" },
output: {
text: "x".repeat(5000),
text: longText,
dispatch: { command: "verbose command that should stay out of compact output" },
bytes: 5000,
bytes: Buffer.byteLength(longText, "utf8"),
summary: "gateway/device-host-cli dispatch completed"
}
};
@@ -313,7 +314,10 @@ test("device-pod-cli job output is compact by default and full with --full", asy
], { fetchImpl, now: () => "2026-05-31T00:00:00.000Z" });
assert.equal(compact.exitCode, 0);
assert.equal(compact.payload.body.compacted, true);
assert.equal(compact.payload.body.text.length, 4000);
assert.equal(compact.payload.body.text.length, 64000);
assert.equal(compact.payload.body.text.includes("device-pod job output compacted"), true);
assert.equal(compact.payload.body.text.includes("FREQ_Controller_FW.hex"), true);
assert.equal(compact.payload.body.text.includes("FREQ_Controller_FW.axf"), true);
assert.equal(JSON.stringify(compact.payload).includes("verbose command"), false);
const full = await runAssembledDevicePodCli([
+16 -2
View File
@@ -766,9 +766,9 @@ function compactJobOutputBody(body: any) {
if (!body || typeof body !== "object") return body;
const output = body.output && typeof body.output === "object" ? body.output : {};
const textValue = typeof body.text === "string" ? body.text : typeof output.text === "string" ? output.text : "";
const maxTextBytes = 4000;
const maxTextBytes = 64000;
const textBuffer = Buffer.from(textValue, "utf8");
const clippedText = textBuffer.length > maxTextBytes ? textBuffer.subarray(0, maxTextBytes).toString("utf8") : textValue;
const clippedText = compactTextPreservingTail(textBuffer, textValue, maxTextBytes);
return clean({
serviceId: body.serviceId,
contractVersion: body.contractVersion,
@@ -800,3 +800,17 @@ function compactJobOutputBody(body: any) {
}
});
}
function compactTextPreservingTail(textBuffer: Buffer, textValue: string, maxTextBytes: number) {
if (textBuffer.length <= maxTextBytes) return textValue;
const marker = "\n... device-pod job output compacted; middle omitted ...\n";
const markerBuffer = Buffer.from(marker, "utf8");
if (markerBuffer.length >= maxTextBytes) return clipUtf8(textBuffer, 0, maxTextBytes);
const headBytes = Math.floor((maxTextBytes - markerBuffer.length) / 2);
const tailBytes = maxTextBytes - markerBuffer.length - headBytes;
return `${clipUtf8(textBuffer, 0, headBytes)}${marker}${clipUtf8(textBuffer, textBuffer.length - tailBytes, textBuffer.length)}`;
}
function clipUtf8(buffer: Buffer, start: number, end: number) {
return buffer.subarray(start, end).toString("utf8").replace(/\uFFFD$/u, "").replace(/^\uFFFD/u, "");
}