mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 09:56:06 +00:00
* chore(types): add declaration files for scripts/lib and scripts/e2e modules
* chore(types): add declaration files for top-level script modules (a-m)
* chore(types): add declaration files for top-level script modules (n-z)
* test: use a non-secret-shaped gateway token fixture
* test: type ci workflow guard helpers for the root test lane
* chore(tooling): typecheck root test/** with a dedicated tsgo lane
- test/tsconfig/tsconfig.test.root.json: root-test program (strict unused checks,
fixtures excluded; two Docker E2E clients that import built dist/** stay out,
same rationale as the scripts/e2e exclusion in tsconfig.scripts.json)
- tsgo:test:root wired into tsgo:test, check:test-types, scripts/check.mjs, and
the ci.yml test-types shard, mirroring the tsgo:scripts lane (#104348)
- changed-lane routing: test/**/*.ts (excluding fixtures) and the lane tsconfig
now trigger 'typecheck test root' in check:changed; previously test/ paths ran
lint only, so harness type errors surfaced first in CI (#104287 envDir case)
- burn down all 1071 latent type errors in the program: precise param/local
types across test/scripts, test/vitest, test/e2e, and transitive scripts/e2e
program members; 205 sibling .d.mts declaration files for imported .mjs
modules (committed separately); zero any, zero ts-expect-error
- resolve the pre-existing testing star-export ambiguity in
scripts/e2e/parallels/common.ts with an explicit re-export
Closes #104388
* chore(types): correct declaration fidelity per structured review
- re-derive 51 .d.mts files from implementation data flow instead of
initializers: fix a wrong never return (runTestProjectsDelegation returns
the child), add encoding-sensitive exec/spawn overloads (plain-gh), restore
the full release profile union, make parsed paths string | null, add missing
parseArgs fields via help/non-help unions, add a missing sibling declaration
(budget-number-args), drop 15 unused lint directives
- precise install-record/tuple typing removes the type-aware oxlint
regressions the first declarations caused in scripts/e2e implementations
- route .mts declaration edits under test/ to the testRoot lane and reference
the test-root project from tsconfig.projects.json so tsgo:all covers it
(closes both review findings against the lane wiring)
* chore(scripts): keep telegram runner dist typing structural for the boundary guard
* chore(types): declare runtime pack and gateway readiness exports added on main
* test: pin the importTargetPlan form of the plugin-contract plan import
The guard expectation still referenced the raw await import( form that
7ae5996bb3 (#103975) replaced with the importTargetPlan fallback helper;
the assertion fails on current main.
668 lines
24 KiB
TypeScript
668 lines
24 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
/* oxlint-disable typescript/no-base-to-string -- fetch mocks normalize standard RequestInfo inputs for registry URL assertions. */
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
truncateSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
verifyPublishedClawHubArtifacts,
|
|
verifyPublishedClawHubPackage,
|
|
} from "../../scripts/verify-clawhub-published-artifact.mjs";
|
|
|
|
const tempDirs: string[] = [];
|
|
const clawhubToolchainSha256 = "d".repeat(64);
|
|
const clawhubToolchainVersion = "0.23.1";
|
|
const clawhubToolchainIntegrity =
|
|
"sha512-YvUImhsVaM90BUAv3uP7lfABziwR5XL3ch2Owa+GvNxwQ2xzZFmZC0yVjAtQbvep+dDDS16nUGRwKx7jqnTOEA==";
|
|
|
|
function immutableBinding() {
|
|
return {
|
|
artifactDigest: "c".repeat(64),
|
|
artifactId: "456",
|
|
clawhubToolchainIntegrity,
|
|
clawhubToolchainSha256,
|
|
clawhubToolchainVersion,
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const directory of tempDirs.splice(0)) {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function identity(artifact: Uint8Array) {
|
|
return {
|
|
sha256: createHash("sha256").update(artifact).digest("hex"),
|
|
size: artifact.byteLength,
|
|
npmIntegrity: `sha512-${createHash("sha512").update(artifact).digest("base64")}`,
|
|
npmShasum: createHash("sha1").update(artifact).digest("hex"),
|
|
};
|
|
}
|
|
|
|
function writeManifest(mode: "publish" | "configure-only", artifact: Uint8Array, runAttempt = "1") {
|
|
const root = mkdtempSync(join(tmpdir(), "openclaw-clawhub-readback-"));
|
|
tempDirs.push(root);
|
|
const path = join(root, "manifest.json");
|
|
const artifactIdentity = identity(artifact);
|
|
writeFileSync(
|
|
path,
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
repository: "openclaw/openclaw",
|
|
targetSha: "a".repeat(40),
|
|
workflowSha: "b".repeat(40),
|
|
runId: "123",
|
|
runAttempt,
|
|
artifactName: `clawhub-bootstrap-aaaaaaaaaaaa-123-${runAttempt}`,
|
|
clawhubToolchainIntegrity,
|
|
clawhubToolchainSha256,
|
|
clawhubToolchainVersion,
|
|
requestedPlugins: ["@openclaw/meta"],
|
|
entries: [
|
|
{
|
|
packageName: "@openclaw/meta",
|
|
version: "2026.7.1-beta.3",
|
|
packageDir: "extensions/meta",
|
|
publishTag: "beta",
|
|
bootstrapMode: mode,
|
|
requiresManualOverride: mode === "configure-only",
|
|
artifactPath: "packages/meta/openclaw-meta-2026.7.1-beta.3.tgz",
|
|
sha256: artifactIdentity.sha256,
|
|
size: artifactIdentity.size,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
return path;
|
|
}
|
|
|
|
function writeExpectedArtifact(artifact: Uint8Array) {
|
|
const root = mkdtempSync(join(tmpdir(), "openclaw-clawhub-oidc-readback-"));
|
|
tempDirs.push(root);
|
|
const artifactDir = join(root, "artifact");
|
|
mkdirSync(artifactDir);
|
|
writeFileSync(join(artifactDir, "openclaw-meta-2026.7.1-beta.3.tgz"), artifact);
|
|
return artifactDir;
|
|
}
|
|
|
|
function artifactResponse(artifact: Uint8Array, body: BodyInit = artifact as unknown as BodyInit) {
|
|
const artifactIdentity = identity(artifact);
|
|
return new Response(body, {
|
|
headers: {
|
|
"content-length": String(artifact.byteLength),
|
|
"x-clawhub-artifact-sha256": artifactIdentity.sha256,
|
|
"x-clawhub-npm-integrity": artifactIdentity.npmIntegrity,
|
|
"x-clawhub-npm-shasum": artifactIdentity.npmShasum,
|
|
},
|
|
});
|
|
}
|
|
|
|
function metadataResponse(artifact: Uint8Array, body?: BodyInit) {
|
|
const artifactIdentity = identity(artifact);
|
|
return new Response(
|
|
body ??
|
|
JSON.stringify({
|
|
package: { name: "@openclaw/meta" },
|
|
version: "2026.7.1-beta.3",
|
|
artifact: {
|
|
kind: "npm-pack",
|
|
sha256: artifactIdentity.sha256,
|
|
size: artifactIdentity.size,
|
|
npmIntegrity: artifactIdentity.npmIntegrity,
|
|
npmShasum: artifactIdentity.npmShasum,
|
|
},
|
|
}),
|
|
{ headers: { "content-type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
function registryFetch(artifact: Uint8Array) {
|
|
return vi.fn(async (input: string | URL | Request) => {
|
|
const url = String(input);
|
|
if (url.endsWith("/trusted-publisher")) {
|
|
return Response.json({
|
|
trustedPublisher: {
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: null,
|
|
},
|
|
});
|
|
}
|
|
if (url.endsWith("/artifact/download")) {
|
|
return artifactResponse(artifact);
|
|
}
|
|
if (url.endsWith("/artifact")) {
|
|
return metadataResponse(artifact);
|
|
}
|
|
return Response.json({
|
|
package: { tags: { beta: "2026.7.1-beta.3" } },
|
|
});
|
|
});
|
|
}
|
|
|
|
describe("ClawHub published artifact verification", () => {
|
|
it("uses bounded streaming reads with an active attempt timeout", () => {
|
|
const source = readFileSync("scripts/verify-clawhub-published-artifact.mjs", "utf8");
|
|
expect(source).not.toContain(".arrayBuffer(");
|
|
expect(source).toContain("response.body.getReader()");
|
|
expect(source).toContain("readBoundedBytes(response, url, MAX_JSON_BYTES)");
|
|
expect(source).toContain("readBoundedBytes(response, url, MAX_ARTIFACT_BYTES)");
|
|
expect(source).toContain("AbortSignal.timeout(timeoutMs)");
|
|
});
|
|
|
|
it("verifies normal OIDC publication against the exact prepared artifact bytes", async () => {
|
|
const artifact = new TextEncoder().encode("exact oidc tgz bytes");
|
|
const fetchImpl = registryFetch(artifact);
|
|
const evidence = await verifyPublishedClawHubPackage({
|
|
expectedArtifactDir: writeExpectedArtifact(artifact),
|
|
packageName: "@openclaw/meta",
|
|
packageVersion: "2026.7.1-beta.3",
|
|
publishTag: "beta",
|
|
registry: "https://clawhub.example",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
});
|
|
|
|
expect(evidence).toMatchObject({
|
|
schemaVersion: 1,
|
|
verificationMode: "oidc-postpublish",
|
|
expectedArtifact: identity(artifact),
|
|
package: {
|
|
packageName: "@openclaw/meta",
|
|
registrySha256: identity(artifact).sha256,
|
|
registrySize: artifact.byteLength,
|
|
},
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
it("rejects a non-GitHub Actions trusted publisher", async () => {
|
|
const artifact = new TextEncoder().encode("exact oidc tgz bytes");
|
|
const fetchImpl = registryFetch(artifact);
|
|
fetchImpl
|
|
.mockResolvedValueOnce(
|
|
Response.json({
|
|
package: { tags: { beta: "2026.7.1-beta.3" } },
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
Response.json({
|
|
trustedPublisher: {
|
|
provider: "other",
|
|
repository: "openclaw/openclaw",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: null,
|
|
},
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
verifyPublishedClawHubPackage({
|
|
expectedArtifactDir: writeExpectedArtifact(artifact),
|
|
packageName: "@openclaw/meta",
|
|
packageVersion: "2026.7.1-beta.3",
|
|
publishTag: "beta",
|
|
registry: "https://clawhub.example",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("trusted publisher provider mismatch");
|
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("rejects ambiguous or symlinked normal OIDC artifacts before registry access", async () => {
|
|
const artifact = new TextEncoder().encode("exact oidc tgz bytes");
|
|
const fetchImpl = registryFetch(artifact);
|
|
const ambiguous = writeExpectedArtifact(artifact);
|
|
writeFileSync(join(ambiguous, "second.tgz"), artifact);
|
|
await expect(
|
|
verifyPublishedClawHubPackage({
|
|
expectedArtifactDir: ambiguous,
|
|
packageName: "@openclaw/meta",
|
|
packageVersion: "2026.7.1-beta.3",
|
|
publishTag: "beta",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("exactly one root .tgz regular file");
|
|
|
|
const root = mkdtempSync(join(tmpdir(), "openclaw-clawhub-oidc-symlink-"));
|
|
tempDirs.push(root);
|
|
const artifactDir = join(root, "artifact");
|
|
const target = join(root, "target.tgz");
|
|
mkdirSync(artifactDir);
|
|
writeFileSync(target, artifact);
|
|
symlinkSync(target, join(artifactDir, "linked.tgz"));
|
|
await expect(
|
|
verifyPublishedClawHubPackage({
|
|
expectedArtifactDir: artifactDir,
|
|
packageName: "@openclaw/meta",
|
|
packageVersion: "2026.7.1-beta.3",
|
|
publishTag: "beta",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("exactly one root .tgz regular file");
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requires exact bytes and complete artifact metadata", async () => {
|
|
const artifact = new TextEncoder().encode("exact tgz bytes");
|
|
const evidence = await verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("publish", artifact),
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "2",
|
|
retryOptions: { fetchImpl: registryFetch(artifact), attempts: 1, delayMs: 1 },
|
|
});
|
|
expect(evidence).toMatchObject({
|
|
schemaVersion: 2,
|
|
producerRunAttempt: "1",
|
|
terminalRunAttempt: "2",
|
|
artifactName: "clawhub-bootstrap-aaaaaaaaaaaa-123-1",
|
|
clawhubToolchainIntegrity,
|
|
clawhubToolchainSha256,
|
|
clawhubToolchainVersion,
|
|
requestedPlugins: ["@openclaw/meta"],
|
|
verificationMode: "postpublish",
|
|
packages: [
|
|
{
|
|
packageName: "@openclaw/meta",
|
|
registrySha256: identity(artifact).sha256,
|
|
registrySize: artifact.byteLength,
|
|
npmIntegrity: identity(artifact).npmIntegrity,
|
|
npmShasum: identity(artifact).npmShasum,
|
|
artifactMetadata: {
|
|
kind: "npm-pack",
|
|
packageName: "@openclaw/meta",
|
|
version: "2026.7.1-beta.3",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("proves configure-only registry bytes before trusted-publisher mutation", async () => {
|
|
const artifact = new TextEncoder().encode("historical exact bytes");
|
|
const fetchImpl = registryFetch(artifact);
|
|
const evidence = await verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
});
|
|
expect(evidence.packages[0]).toMatchObject({
|
|
bootstrapMode: "configure-only",
|
|
expectedSha256: identity(artifact).sha256,
|
|
registrySha256: identity(artifact).sha256,
|
|
});
|
|
expect(fetchImpl.mock.calls.some(([url]) => String(url).endsWith("/trusted-publisher"))).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("rejects a missing configure-only tag before artifact or publisher requests", async () => {
|
|
const artifact = new TextEncoder().encode("historical exact bytes");
|
|
const fetchImpl = vi.fn(async (_input: RequestInfo | URL) =>
|
|
Response.json({
|
|
package: { tags: { beta: "2026.7.1-beta.2" } },
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow(
|
|
"@openclaw/meta@2026.7.1-beta.3 ClawHub artifact did not stabilize after 1 attempts; last failure @openclaw/meta ClawHub tag beta mismatch",
|
|
);
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
expect(fetchImpl.mock.calls.some(([url]) => String(url).includes("/artifact"))).toBe(false);
|
|
expect(fetchImpl.mock.calls.some(([url]) => String(url).endsWith("/trusted-publisher"))).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("retries invalid JSON, body read failures, and eventual byte convergence", async () => {
|
|
const expected = new TextEncoder().encode("expected");
|
|
const wrong = new TextEncoder().encode("wrong");
|
|
let detailCalls = 0;
|
|
let artifactCalls = 0;
|
|
const fetchImpl = vi.fn(async (input: string | URL | Request) => {
|
|
const url = String(input);
|
|
if (!url.includes("/artifact") && !url.endsWith("/trusted-publisher")) {
|
|
detailCalls += 1;
|
|
if (detailCalls === 1) {
|
|
return new Response("{invalid");
|
|
}
|
|
return Response.json({ package: { tags: { beta: "2026.7.1-beta.3" } } });
|
|
}
|
|
if (url.endsWith("/trusted-publisher")) {
|
|
return Response.json({
|
|
trustedPublisher: {
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: null,
|
|
},
|
|
});
|
|
}
|
|
if (url.endsWith("/artifact")) {
|
|
return metadataResponse(expected);
|
|
}
|
|
artifactCalls += 1;
|
|
if (artifactCalls === 1) {
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.error(new Error("truncated body"));
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
if (artifactCalls === 2) {
|
|
return artifactResponse(expected, wrong);
|
|
}
|
|
return artifactResponse(expected);
|
|
});
|
|
const sleep = vi.fn(async () => {});
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("publish", expected),
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 4, delayMs: 1, sleep },
|
|
}),
|
|
).resolves.toMatchObject({ packages: [{ registrySha256: identity(expected).sha256 }] });
|
|
expect(sleep).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("keeps the attempt timeout active through a stalled body", async () => {
|
|
const artifact = new TextEncoder().encode("expected");
|
|
let artifactCalls = 0;
|
|
const fetchImpl = vi.fn(
|
|
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
const url = String(input);
|
|
if (url.endsWith("/artifact")) {
|
|
return metadataResponse(artifact);
|
|
}
|
|
if (url.endsWith("/artifact/download")) {
|
|
artifactCalls += 1;
|
|
if (artifactCalls === 1) {
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
init?.signal?.addEventListener(
|
|
"abort",
|
|
() => controller.error(init.signal?.reason),
|
|
{ once: true },
|
|
);
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
return artifactResponse(artifact);
|
|
}
|
|
if (!url.includes("/artifact")) {
|
|
return Response.json({
|
|
package: { tags: { beta: "2026.7.1-beta.3" } },
|
|
});
|
|
}
|
|
throw new Error(`unexpected URL ${url}`);
|
|
},
|
|
);
|
|
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 2, delayMs: 1, timeoutMs: 10 },
|
|
}),
|
|
).resolves.toMatchObject({ packages: [{ registrySize: artifact.byteLength }] });
|
|
expect(artifactCalls).toBe(2);
|
|
});
|
|
|
|
it("cancels retryable response bodies and never sleeps after the final attempt", async () => {
|
|
const artifact = new TextEncoder().encode("expected");
|
|
const canceled: string[] = [];
|
|
const sleep = vi.fn(async () => {});
|
|
const fetchImpl = vi.fn(async () => {
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
cancel() {
|
|
canceled.push("retry");
|
|
},
|
|
}),
|
|
{ status: 503 },
|
|
);
|
|
});
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 2, delayMs: 1, sleep },
|
|
}),
|
|
).rejects.toThrow("did not stabilize after 2 attempts");
|
|
expect(canceled).toEqual(["retry", "retry"]);
|
|
expect(sleep).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("fails immediately on permanent HTTP errors and explicit size limits", async () => {
|
|
const artifact = new TextEncoder().encode("expected");
|
|
const permanentFetch = vi.fn(async () => new Response("denied", { status: 403 }));
|
|
const permanentSleep = vi.fn(async () => {});
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: {
|
|
fetchImpl: permanentFetch,
|
|
attempts: 3,
|
|
delayMs: 1,
|
|
sleep: permanentSleep,
|
|
},
|
|
}),
|
|
).rejects.toThrow("returned HTTP 403");
|
|
expect(permanentFetch).toHaveBeenCalledTimes(1);
|
|
expect(permanentSleep).not.toHaveBeenCalled();
|
|
|
|
const oversizedFetch = vi.fn(async (input: string | URL | Request) => {
|
|
const url = String(input);
|
|
if (url.endsWith("/artifact")) {
|
|
return new Response("{}", {
|
|
headers: { "content-length": String(1024 * 1024 + 1) },
|
|
});
|
|
}
|
|
return Response.json({
|
|
package: { tags: { beta: "2026.7.1-beta.3" } },
|
|
});
|
|
});
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl: oversizedFetch, attempts: 3, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("exceeded 1048576 bytes");
|
|
expect(oversizedFetch).toHaveBeenCalledTimes(2);
|
|
|
|
const oversizedArtifactFetch = vi.fn(async (input: string | URL | Request) => {
|
|
const url = String(input);
|
|
if (url.endsWith("/artifact/download")) {
|
|
return new Response(null, {
|
|
headers: { "content-length": String(130 * 1024 * 1024 + 1) },
|
|
});
|
|
}
|
|
if (url.endsWith("/artifact")) {
|
|
return metadataResponse(artifact);
|
|
}
|
|
return Response.json({
|
|
package: { tags: { beta: "2026.7.1-beta.3" } },
|
|
});
|
|
});
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl: oversizedArtifactFetch, attempts: 3, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("exceeded 136314880 bytes");
|
|
});
|
|
|
|
it("requires a terminal attempt at or after the immutable producer attempt", async () => {
|
|
const artifact = new TextEncoder().encode("expected");
|
|
const baseOptions = {
|
|
...immutableBinding(),
|
|
manifestPath: writeManifest("configure-only", artifact),
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
retryOptions: { fetchImpl: registryFetch(artifact), attempts: 1, delayMs: 1 },
|
|
};
|
|
|
|
await expect(verifyPublishedClawHubArtifacts(baseOptions)).rejects.toThrow(
|
|
"terminalRunAttempt must be an integer",
|
|
);
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({ ...baseOptions, terminalRunAttempt: "0" }),
|
|
).rejects.toThrow("terminalRunAttempt must be an integer");
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...baseOptions,
|
|
manifestPath: writeManifest("configure-only", artifact, "2"),
|
|
terminalRunAttempt: "1",
|
|
}),
|
|
).rejects.toThrow("greater than or equal to the producer run attempt");
|
|
|
|
for (const invalid of ["1junk", "1.5", "1e2"]) {
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...baseOptions,
|
|
terminalRunAttempt: invalid,
|
|
}),
|
|
).rejects.toThrow("terminalRunAttempt must be an integer");
|
|
}
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...baseOptions,
|
|
artifactId: "1junk",
|
|
terminalRunAttempt: "1",
|
|
}),
|
|
).rejects.toThrow("artifactId must be an integer");
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...baseOptions,
|
|
artifactDigest: "A".repeat(64),
|
|
terminalRunAttempt: "1",
|
|
}),
|
|
).rejects.toThrow("artifactDigest is invalid");
|
|
});
|
|
|
|
it("requires the locked ClawHub toolchain identity in the validated manifest", async () => {
|
|
const artifact = new TextEncoder().encode("expected");
|
|
const manifestPath = writeManifest("configure-only", artifact);
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
manifest.clawhubToolchainSha256 = "A".repeat(64);
|
|
writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath,
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl: registryFetch(artifact), attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("manifest.clawhubToolchainSha256 is invalid");
|
|
|
|
manifest.clawhubToolchainSha256 = clawhubToolchainSha256;
|
|
writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
clawhubToolchainSha256: "e".repeat(64),
|
|
manifestPath,
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl: registryFetch(artifact), attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("clawhubToolchainSha256 mismatch");
|
|
});
|
|
|
|
it("rejects noncanonical, oversized, and symlinked manifests before registry access", async () => {
|
|
const artifact = new TextEncoder().encode("expected");
|
|
const fetchImpl = registryFetch(artifact);
|
|
const manifestPath = writeManifest("configure-only", artifact);
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
writeFileSync(manifestPath, JSON.stringify({ ...manifest, unexpected: true }));
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath,
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("keys are invalid");
|
|
|
|
const oversizedPath = writeManifest("configure-only", artifact);
|
|
truncateSync(oversizedPath, 2 * 1024 * 1024 + 1);
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: oversizedPath,
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("size is outside the allowed range: 2097153");
|
|
|
|
const targetPath = writeManifest("configure-only", artifact);
|
|
const symlinkPath = `${targetPath}.link`;
|
|
symlinkSync(targetPath, symlinkPath);
|
|
await expect(
|
|
verifyPublishedClawHubArtifacts({
|
|
...immutableBinding(),
|
|
manifestPath: symlinkPath,
|
|
mode: "configure-only-preflight",
|
|
registry: "https://clawhub.example",
|
|
terminalRunAttempt: "1",
|
|
retryOptions: { fetchImpl, attempts: 1, delayMs: 1 },
|
|
}),
|
|
).rejects.toThrow("must be a regular file");
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
});
|