fix(qa): measure chunked credential payload bytes

This commit is contained in:
Vincent Koc
2026-06-20 07:26:49 +02:00
parent 8e375242be
commit e451a4e875
4 changed files with 106 additions and 7 deletions

View File

@@ -165,6 +165,45 @@ describe("credential lease runtime", () => {
expect(chunkRequest.leaseToken).toBe("lease-chunked");
});
it("validates chunked convex payload length as utf8 bytes", async () => {
const serialized = JSON.stringify({
groupId: "-100123",
driverToken: "driv\u00e9r",
sutToken: "sut",
});
const fetchImpl = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(
jsonResponse({
status: "ok",
credentialId: "cred-utf8",
leaseToken: "lease-utf8",
payload: {
__openclawQaCredentialPayloadChunksV1: true,
byteLength: Buffer.byteLength(serialized, "utf8"),
chunkCount: 1,
},
}),
)
.mockResolvedValueOnce(jsonResponse({ status: "ok", data: serialized }));
const lease = await acquireQaCredentialLease({
kind: "telegram",
source: "convex",
role: "ci",
env: {
OPENCLAW_QA_CONVEX_SITE_URL: "https://qa-cred.example.convex.site",
OPENCLAW_QA_CONVEX_SECRET_CI: "ci-secret",
},
fetchImpl,
resolveEnvPayload: () => ({ groupId: "-1", driverToken: "unused", sutToken: "unused" }),
parsePayload: (payload) =>
payload as { groupId: string; driverToken: string; sutToken: string },
});
expect(lease.payload.driverToken).toBe("driv\u00e9r");
});
it("defaults convex credential role to maintainer outside CI", async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValueOnce(
jsonResponse({

View File

@@ -328,7 +328,7 @@ async function resolveConvexCredentialPayload(params: {
chunks.push(parsed.data);
}
const serialized = chunks.join("");
if (serialized.length !== marker.byteLength) {
if (Buffer.byteLength(serialized, "utf8") !== marker.byteLength) {
throw new Error("Chunked credential payload length mismatch.");
}
return JSON.parse(serialized) as unknown;

View File

@@ -324,7 +324,7 @@ async function hydratePayloadFromLease(params: {
const credentialId = requireString(params.acquired, "credentialId");
const leaseToken = requireString(params.acquired, "leaseToken");
const chunks: string[] = [];
let serializedLength = 0;
let serializedBytes = 0;
for (let index = 0; index < marker.chunkCount; index += 1) {
const chunk = await postBroker({
action: "payload-chunk",
@@ -340,14 +340,14 @@ async function hydratePayloadFromLease(params: {
},
});
const data = requireString(chunk, "data");
serializedLength += data.length;
if (serializedLength > marker.byteLength) {
serializedBytes += Buffer.byteLength(data, "utf8");
if (serializedBytes > marker.byteLength) {
throw new Error("Chunked payload exceeded declared byteLength.");
}
chunks.push(data);
}
const serialized = chunks.join("");
if (serializedLength !== marker.byteLength) {
if (serializedBytes !== marker.byteLength) {
throw new Error("Chunked payload length mismatch.");
}
return parseTelegramUserQaCredentialPayload(JSON.parse(serialized));
@@ -640,4 +640,4 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();
}
export { optionalPositiveInteger, parseChunkedPayloadMarker };
export { hydratePayloadFromLease, optionalPositiveInteger, parseChunkedPayloadMarker };

View File

@@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "no
import { readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path, { win32 } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { fetchJsonWithTimeout, runCommand } from "../../scripts/e2e/telegram-user-credential-io.ts";
import {
expandHome,
@@ -75,6 +75,8 @@ async function waitForExit(
}
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { force: true, recursive: true });
}
@@ -178,6 +180,64 @@ describe("telegram user credential IO", () => {
).toThrow("Chunked payload marker exceeds 67108864 bytes.");
});
it("hydrates chunked lease payloads using utf8 byte lengths", async () => {
const credentialModule = (await import(
`${new URL("../../scripts/e2e/telegram-user-credential.ts", import.meta.url).href}?case=utf8-chunk-${Date.now()}`
)) as {
hydratePayloadFromLease(params: {
acquired: Record<string, unknown>;
ownerId: string;
siteUrl: string;
token: string;
}): Promise<Record<string, unknown>>;
};
const sha256 = "a".repeat(64);
const serialized = JSON.stringify({
groupId: "-100123",
sutToken: "sut-token",
testerUserId: "8709353529",
testerUsername: "OpenClawTestUser",
telegramApiId: "123456",
telegramApiHash: "api-hash-\u00e9",
tdlibDatabaseEncryptionKey: "db-key",
tdlibArchiveBase64: "tdlib-archive",
tdlibArchiveSha256: sha256,
desktopTdataArchiveBase64: "desktop-archive",
desktopTdataArchiveSha256: sha256,
});
const fetchMock = vi.fn<typeof fetch>(
async () =>
new Response(JSON.stringify({ status: "ok", data: serialized }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const payload = await credentialModule.hydratePayloadFromLease({
acquired: {
credentialId: "cred-utf8",
leaseToken: "lease-utf8",
payload: {
[CHUNKED_PAYLOAD_MARKER]: true,
byteLength: Buffer.byteLength(serialized, "utf8"),
chunkCount: 1,
},
},
ownerId: "owner-utf8",
siteUrl: "https://qa.example.invalid",
token: "ci-secret",
});
expect(payload.telegramApiHash).toBe("api-hash-\u00e9");
expect(fetchMock).toHaveBeenCalledWith(
"https://qa.example.invalid/qa-credentials/v1/payload-chunk",
expect.objectContaining({
body: expect.stringContaining('"credentialId":"cred-utf8"'),
}),
);
});
it("rejects loose numeric credential limits instead of parsing prefixes", async () => {
const credentialModule = (await import(
`${new URL("../../scripts/e2e/telegram-user-credential.ts", import.meta.url).href}?case=limits-${Date.now()}`