mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 10:06:03 +00:00
* fix(ci): bound Telegram provenance lookups * docs(agents): note linked worktree invocations
738 lines
30 KiB
TypeScript
738 lines
30 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { parse } from "yaml";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const RELEASE_CHECKS_PATH = ".github/workflows/openclaw-release-checks.yml";
|
|
const WORKFLOW_PATH = ".github/workflows/openclaw-release-telegram-qa.yml";
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
type WorkflowJob = {
|
|
"continue-on-error"?: boolean;
|
|
environment?: string;
|
|
if?: string;
|
|
needs?: string[];
|
|
outputs?: Record<string, unknown>;
|
|
permissions?: Record<string, string>;
|
|
"runs-on"?: unknown;
|
|
"timeout-minutes"?: unknown;
|
|
steps?: Array<{
|
|
env?: Record<string, unknown>;
|
|
if?: string;
|
|
name?: string;
|
|
run?: string;
|
|
uses?: string;
|
|
with?: Record<string, unknown>;
|
|
}>;
|
|
uses?: string;
|
|
with?: Record<string, unknown>;
|
|
};
|
|
|
|
function workflowJob(name: string): WorkflowJob {
|
|
const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
};
|
|
const job = workflow.jobs?.[name];
|
|
if (!job) {
|
|
throw new Error(`Expected workflow job ${name}`);
|
|
}
|
|
return job;
|
|
}
|
|
|
|
function workflowStep(job: WorkflowJob, name: string) {
|
|
const step = job.steps?.find((candidate) => candidate.name === name);
|
|
if (!step) {
|
|
throw new Error(`Expected workflow step ${name}`);
|
|
}
|
|
return step;
|
|
}
|
|
|
|
function runIdentityVerification(params: {
|
|
expectedTrustedWorkflowSha: string;
|
|
invocation?: "dispatch" | "reusable";
|
|
oidcJobWorkflowSha?: string;
|
|
oidcWorkflowSha?: string;
|
|
workflowSha?: string;
|
|
}) {
|
|
const repository = "openclaw/openclaw";
|
|
const trustedWorkflowRef = `${repository}/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main`;
|
|
const invocation = params.invocation ?? "dispatch";
|
|
const workflowRef =
|
|
invocation === "dispatch"
|
|
? trustedWorkflowRef
|
|
: `${repository}/.github/workflows/openclaw-release-checks.yml@refs/heads/release-ci/test`;
|
|
const workflowRefName =
|
|
invocation === "dispatch" ? "refs/heads/main" : "refs/heads/release-ci/test";
|
|
const targetSha = "a".repeat(40);
|
|
const workdir = tempDirs.make("openclaw-telegram-identity-");
|
|
const fakeBin = join(workdir, "bin");
|
|
const curlPath = join(fakeBin, "curl");
|
|
const githubOutput = join(workdir, "github-output");
|
|
mkdirSync(fakeBin);
|
|
const workflowSha = params.workflowSha ?? params.expectedTrustedWorkflowSha;
|
|
const oidcWorkflowSha = params.oidcWorkflowSha ?? workflowSha;
|
|
const payload = {
|
|
aud: "openclaw-release-telegram-qa",
|
|
event_name: "workflow_dispatch",
|
|
iss: "https://token.actions.githubusercontent.com",
|
|
...(invocation === "reusable"
|
|
? {
|
|
job_workflow_ref: trustedWorkflowRef,
|
|
job_workflow_sha: params.oidcJobWorkflowSha ?? params.expectedTrustedWorkflowSha,
|
|
}
|
|
: {}),
|
|
ref: workflowRefName,
|
|
repository,
|
|
runner_environment: "github-hosted",
|
|
sha: workflowSha,
|
|
workflow_ref: workflowRef,
|
|
workflow_sha: oidcWorkflowSha,
|
|
};
|
|
const token = [
|
|
Buffer.from("{}").toString("base64url"),
|
|
Buffer.from(JSON.stringify(payload)).toString("base64url"),
|
|
"signature",
|
|
].join(".");
|
|
writeFileSync(curlPath, "#!/usr/bin/env bash\nprintf '%s\\n' \"$FAKE_OIDC_JSON\"\n", {
|
|
mode: 0o755,
|
|
});
|
|
const script = workflowStep(
|
|
workflowJob("trusted_identity"),
|
|
"Verify dispatched-main identity",
|
|
).run;
|
|
if (!script) {
|
|
throw new Error("Expected trusted identity script");
|
|
}
|
|
return spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
ACTIONS_ID_TOKEN_REQUEST_TOKEN: "test-token",
|
|
ACTIONS_ID_TOKEN_REQUEST_URL: "https://example.invalid/oidc?",
|
|
CALLER_WORKFLOW_REF: workflowRef,
|
|
CALLER_WORKFLOW_SHA: workflowSha,
|
|
EXPECTED_TRUSTED_WORKFLOW_SHA: params.expectedTrustedWorkflowSha,
|
|
FAKE_OIDC_JSON: JSON.stringify({ value: token }),
|
|
GITHUB_EVENT_NAME: "workflow_dispatch",
|
|
GITHUB_OUTPUT: githubOutput,
|
|
GITHUB_REF: workflowRefName,
|
|
GITHUB_REPOSITORY: repository,
|
|
GITHUB_SHA: workflowSha,
|
|
JOB_CONTEXT: JSON.stringify({
|
|
workflow_ref: trustedWorkflowRef,
|
|
workflow_repository: repository,
|
|
workflow_sha: params.expectedTrustedWorkflowSha,
|
|
}),
|
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
TARGET_REF: "refs/heads/release/2026.7.1",
|
|
TARGET_SHA: targetSha,
|
|
WORKFLOW_REF: workflowRef,
|
|
WORKFLOW_SHA: workflowSha,
|
|
},
|
|
});
|
|
}
|
|
|
|
function runAdvisoryStatus(overrides: Record<string, string> = {}) {
|
|
const runId = "123456";
|
|
const runAttempt = "1";
|
|
const targetSha = "a".repeat(40);
|
|
const workflowSha = "b".repeat(40);
|
|
const workdir = tempDirs.make("openclaw-telegram-advisory-status-");
|
|
const githubOutput = join(workdir, "github-output");
|
|
const script = workflowStep(workflowJob("advisory_status"), "Record advisory status").run;
|
|
if (!script) {
|
|
throw new Error("Expected advisory status script");
|
|
}
|
|
const result = spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
ARCHIVE_NAME: `release-telegram-candidate-${runId}-${runAttempt}-${targetSha}.tar.zst`,
|
|
ARCHIVE_SHA256: "c".repeat(64),
|
|
ATTESTATION_RESULT: "success",
|
|
ATTESTATION_STATUS: "success",
|
|
BUILD_RESULT: "success",
|
|
BUILD_STATUS: "success",
|
|
CANDIDATE_ARTIFACT_DIGEST: "d".repeat(64),
|
|
CANDIDATE_ARTIFACT_ID: "123",
|
|
CANDIDATE_VERSION: "2026.7.1-beta.3",
|
|
EVIDENCE_ARTIFACT_DIGEST: "e".repeat(64),
|
|
EVIDENCE_ARTIFACT_ID: "456",
|
|
EVIDENCE_ARTIFACT_NAME: `release-qa-live-telegram-${runId}-${runAttempt}-${targetSha}`,
|
|
EXECUTION_STATUS: "success",
|
|
GITHUB_OUTPUT: githubOutput,
|
|
GITHUB_RUN_ATTEMPT: runAttempt,
|
|
GITHUB_RUN_ID: runId,
|
|
IDENTITY_RESULT: "success",
|
|
IDENTITY_STATUS: "success",
|
|
PATH: process.env.PATH,
|
|
RUN_RESULT: "success",
|
|
TARGET_SHA: targetSha,
|
|
WORKFLOW_SHA: workflowSha,
|
|
...overrides,
|
|
},
|
|
});
|
|
const output = result.status === 0 ? readFileSync(githubOutput, "utf8") : "";
|
|
const status = output.match(/^status=(.*)$/mu)?.[1] ?? "";
|
|
const requireScript = workflowStep(
|
|
workflowJob("advisory_status"),
|
|
"Require successful Telegram release check",
|
|
).run;
|
|
if (!requireScript) {
|
|
throw new Error("Expected terminal Telegram status script");
|
|
}
|
|
const requireResult = spawnSync("bash", ["-c", requireScript], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
STATUS: status,
|
|
},
|
|
});
|
|
const statusFile = join(
|
|
workdir,
|
|
".artifacts",
|
|
"release-check-status",
|
|
`qa_live_telegram_release_checks-${runId}-${runAttempt}.env`,
|
|
);
|
|
return {
|
|
output,
|
|
recordResult: result,
|
|
requireResult,
|
|
status,
|
|
statusFile: result.status === 0 ? readFileSync(statusFile, "utf8") : "",
|
|
};
|
|
}
|
|
|
|
describe("release Telegram QA workflow", () => {
|
|
it("attributes GitHub web-flow and unsigned release merges to their exact maintainer merger", () => {
|
|
const source = readFileSync(WORKFLOW_PATH, "utf8");
|
|
|
|
expect(source.match(/associatedPullRequests\(first:10\)/gu)).toHaveLength(2);
|
|
expect(source.match(/if \.signature == null then "missing"/gu)).toHaveLength(2);
|
|
expect(source.match(/\$signature_status" == "invalid"/gu)).toHaveLength(2);
|
|
expect(
|
|
source.match(/\$signature_status" == "missing" \|\| "\$signer" == "web-flow"/gu),
|
|
).toHaveLength(2);
|
|
expect(source.match(/\.mergeCommit\.oid == \$sha/gu)).toHaveLength(2);
|
|
expect(source.match(/\.baseRefName == \$base/gu)).toHaveLength(2);
|
|
expect(source.match(/\.baseRepository\.nameWithOwner == \$repo/gu)).toHaveLength(2);
|
|
expect(source.match(/\.mergedBy\.login\] \| unique \| select\(length == 1\)/gu)).toHaveLength(
|
|
2,
|
|
);
|
|
expect(source.match(/collaborators\/\$\{permission_actor\}\/permission/gu)).toHaveLength(2);
|
|
expect((source.match(/extended-stable\/\[0-9\]/gu) ?? []).length).toBeGreaterThanOrEqual(2);
|
|
expect(source).not.toContain("collaborators/${signer}/permission");
|
|
});
|
|
|
|
it("resolves only candidate-specific provenance refs without fetching histories", () => {
|
|
const source = readFileSync(WORKFLOW_PATH, "utf8");
|
|
|
|
expect(source.match(/branches-where-head/gu)).toHaveLength(2);
|
|
expect(source.match(/gh api --paginate/gu)).toHaveLength(2);
|
|
expect(
|
|
source.match(/git(?: -C \.candidate)? ls-remote --exit-code --refs origin/gu),
|
|
).toHaveLength(2);
|
|
expect(
|
|
source.match(/git(?: -C \.candidate)? ls-remote origin 'refs\/tags\/v\*'/gu),
|
|
).toHaveLength(2);
|
|
expect(source).not.toContain("'+refs/heads/release/*:refs/remotes/origin/release/*'");
|
|
expect(source).not.toContain(
|
|
"'+refs/heads/extended-stable/*:refs/remotes/origin/extended-stable/*'",
|
|
);
|
|
expect(source).not.toContain("'+refs/tags/v*:refs/tags/v*'");
|
|
});
|
|
|
|
it("dispatches one accepted trusted-main child from release checks", () => {
|
|
const releaseSource = readFileSync(RELEASE_CHECKS_PATH, "utf8");
|
|
const reusableSource = readFileSync(WORKFLOW_PATH, "utf8");
|
|
const releaseWorkflow = parse(releaseSource) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
};
|
|
const caller = releaseWorkflow.jobs?.qa_live_telegram_release_checks;
|
|
|
|
expect(caller?.needs).toEqual(["resolve_target"]);
|
|
expect(caller?.if).toContain("needs.resolve_target.outputs.qa_live_telegram_enabled == 'true'");
|
|
expect(caller?.permissions).toEqual({
|
|
actions: "write",
|
|
contents: "read",
|
|
});
|
|
expect(caller?.["timeout-minutes"]).toBe(210);
|
|
expect(caller?.["continue-on-error"]).toBeUndefined();
|
|
expect(caller?.["runs-on"]).toBe("ubuntu-24.04");
|
|
expect(caller?.environment).toBeUndefined();
|
|
const dispatch = caller?.steps?.find(
|
|
(step) => step.name === "Dispatch and await trusted Telegram QA",
|
|
);
|
|
expect(dispatch?.run).toContain('gh workflow run "$workflow"');
|
|
expect(dispatch?.run).toContain('--repo "$GITHUB_REPOSITORY"');
|
|
expect(dispatch?.run).toContain("-F event=workflow_dispatch");
|
|
expect(dispatch?.run).toContain(".display_title == env.RUN_NAME");
|
|
expect(dispatch?.run).toContain("$(openssl rand -hex 16)");
|
|
expect(dispatch?.run).toContain("trap cancel_child_on_failure EXIT");
|
|
expect(dispatch?.run).toContain("for _ in $(seq 1 6)");
|
|
expect(dispatch?.run).toContain("/actions/runs/${run_id}/cancel");
|
|
expect(dispatch?.run).toContain("for dispatch_attempt in 1 2 3 4 5");
|
|
expect(dispatch?.run).toContain('gh api "repos/${GITHUB_REPOSITORY}/commits/main"');
|
|
expect(dispatch?.run).toContain(
|
|
'if [[ "$child_head_sha" == "$expected_trusted_workflow_sha" ]]; then',
|
|
);
|
|
expect(dispatch?.run).toContain("Trusted main moved from");
|
|
expect(dispatch?.run).toContain("Trusted main kept moving");
|
|
expect(dispatch?.run).toContain("for _ in $(seq 1 1080)");
|
|
expect(dispatch?.run).toContain("Trusted Telegram QA concluded ${conclusion}");
|
|
expect(
|
|
releaseSource.match(
|
|
/"qa_live_telegram_release_checks=\$\{QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT\}"/gu,
|
|
),
|
|
).toHaveLength(1);
|
|
expect(releaseSource).not.toContain(
|
|
"qa_live_matrix_release_checks|qa_live_telegram_release_checks|qa_live_discord_release_checks",
|
|
);
|
|
expect(releaseSource).not.toContain("persist-credentials: true");
|
|
|
|
const dispatchers = readdirSync(".github/workflows")
|
|
.filter((name) => name.endsWith(".yml"))
|
|
.flatMap((name) => {
|
|
const path = `.github/workflows/${name}`;
|
|
return readFileSync(path, "utf8").includes('workflow="openclaw-release-telegram-qa.yml"')
|
|
? [path]
|
|
: [];
|
|
});
|
|
expect(dispatchers).toEqual([RELEASE_CHECKS_PATH]);
|
|
expect(reusableSource).toContain(
|
|
"openclaw/openclaw/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main",
|
|
);
|
|
expect(reusableSource).toContain(
|
|
'--cert-identity "https://github.com/openclaw/openclaw/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main"',
|
|
);
|
|
expect(reusableSource).toContain('--signer-digest "$CALLED_WORKFLOW_SHA"');
|
|
const resolveJob = releaseWorkflow.jobs?.resolve_target;
|
|
expect(resolveJob?.outputs?.trusted_workflow_sha).toBeUndefined();
|
|
expect(
|
|
resolveJob?.steps?.find((step) => step.name === "Resolve trusted main Telegram workflow SHA"),
|
|
).toBeUndefined();
|
|
const dispatchedWorkflow = parse(reusableSource) as {
|
|
on?: {
|
|
workflow_call?: {
|
|
inputs?: Record<string, { required?: boolean; type?: string }>;
|
|
secrets?: Record<string, { required?: boolean }>;
|
|
};
|
|
workflow_dispatch?: {
|
|
inputs?: Record<string, { required?: boolean; type?: string }>;
|
|
};
|
|
};
|
|
};
|
|
expect(dispatchedWorkflow.on?.workflow_dispatch?.inputs?.expected_trusted_workflow_sha).toEqual(
|
|
{
|
|
description: "Resolved main SHA authorized for this trusted workflow",
|
|
required: true,
|
|
type: "string",
|
|
},
|
|
);
|
|
expect(dispatchedWorkflow.on?.workflow_dispatch?.inputs?.dispatch_id).toEqual({
|
|
description: "Unique parent release-check dispatch identifier",
|
|
required: true,
|
|
type: "string",
|
|
});
|
|
expect(dispatchedWorkflow.on?.workflow_call?.inputs?.expected_trusted_workflow_sha).toEqual({
|
|
description: "Resolved main SHA authorized for this trusted workflow",
|
|
required: true,
|
|
type: "string",
|
|
});
|
|
expect(dispatchedWorkflow.on?.workflow_call?.secrets).toHaveProperty(
|
|
"OPENCLAW_QA_CONVEX_SECRET_CI",
|
|
);
|
|
});
|
|
|
|
it("binds dispatched and legacy reusable OIDC identity to the resolved main SHA", () => {
|
|
const trustedSha = "b".repeat(40);
|
|
const success = runIdentityVerification({
|
|
expectedTrustedWorkflowSha: trustedSha,
|
|
});
|
|
expect(success.status).toBe(0);
|
|
|
|
const oidcDrifted = runIdentityVerification({
|
|
expectedTrustedWorkflowSha: trustedSha,
|
|
oidcWorkflowSha: "c".repeat(40),
|
|
});
|
|
expect(oidcDrifted.status).toBe(1);
|
|
expect(oidcDrifted.stderr).toContain("OIDC workflow_sha mismatch");
|
|
|
|
const mainMoved = runIdentityVerification({
|
|
expectedTrustedWorkflowSha: trustedSha,
|
|
workflowSha: "c".repeat(40),
|
|
});
|
|
expect(mainMoved.status).toBe(1);
|
|
expect(mainMoved.stderr).toBe("");
|
|
|
|
const reusableSuccess = runIdentityVerification({
|
|
expectedTrustedWorkflowSha: trustedSha,
|
|
invocation: "reusable",
|
|
workflowSha: "d".repeat(40),
|
|
});
|
|
expect(reusableSuccess.status).toBe(0);
|
|
|
|
const reusableDrifted = runIdentityVerification({
|
|
expectedTrustedWorkflowSha: trustedSha,
|
|
invocation: "reusable",
|
|
oidcJobWorkflowSha: "c".repeat(40),
|
|
workflowSha: "d".repeat(40),
|
|
});
|
|
expect(reusableDrifted.status).toBe(1);
|
|
expect(reusableDrifted.stderr).toContain("OIDC job_workflow_sha mismatch");
|
|
});
|
|
|
|
it("keeps candidate construction secretless and credentials inside the isolated runner", () => {
|
|
const source = readFileSync(WORKFLOW_PATH, "utf8");
|
|
const workflow = parse(source) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
};
|
|
const buildJob = workflow.jobs?.build_candidate;
|
|
const runJob = workflow.jobs?.run_telegram;
|
|
|
|
expect(JSON.stringify(buildJob)).not.toContain("secrets.");
|
|
expect(runJob?.environment).toBe("qa-live-shared");
|
|
const secretSteps = runJob?.steps
|
|
?.filter((step) => JSON.stringify(step).includes("secrets."))
|
|
.map((step) => step.name);
|
|
expect(secretSteps).toEqual(["Validate required QA credential env", "Run Telegram live lane"]);
|
|
expect(source).not.toContain("secrets: inherit");
|
|
expect(source).not.toContain("persist-credentials: true");
|
|
expect(source).toContain("trusted_scenario_source=verified_trusted_workflow_sha");
|
|
|
|
for (const [jobName, job] of Object.entries(workflow.jobs ?? {})) {
|
|
for (const step of job.steps ?? []) {
|
|
if (step.uses?.startsWith("actions/checkout@")) {
|
|
expect(step.with?.["persist-credentials"], `${jobName}:${step.name}`).toBe(false);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("allows the tracked-file index to exceed Node's default child-process buffer", () => {
|
|
const compareStep = workflowStep(
|
|
workflowJob("attest_candidate"),
|
|
"Compare candidate tracked source and tree",
|
|
);
|
|
|
|
expect(compareStep.run).toContain("maxBuffer: 16 * 1024 * 1024");
|
|
});
|
|
|
|
it("emits the release-check terminal status contract and fails closed", () => {
|
|
const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
};
|
|
const statusJob = workflow.jobs?.advisory_status;
|
|
const recordStep = statusJob?.steps?.find((step) => step.name === "Record advisory status");
|
|
const uploadStep = statusJob?.steps?.find((step) => step.name === "Upload advisory status");
|
|
const requireStep = statusJob?.steps?.find(
|
|
(step) => step.name === "Require successful Telegram release check",
|
|
);
|
|
|
|
for (const jobName of [
|
|
"trusted_identity",
|
|
"build_candidate",
|
|
"attest_candidate",
|
|
"run_telegram",
|
|
]) {
|
|
expect(workflow.jobs?.[jobName]?.["continue-on-error"], jobName).toBeUndefined();
|
|
}
|
|
for (const jobName of ["build_candidate", "attest_candidate", "run_telegram"]) {
|
|
expect(workflow.jobs?.[jobName]?.if, jobName).toBe("always()");
|
|
}
|
|
expect(statusJob?.if).toBe("always()");
|
|
expect(recordStep?.run).toContain(
|
|
"qa_live_telegram_release_checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.env",
|
|
);
|
|
for (const field of [
|
|
"run_id",
|
|
"run_attempt",
|
|
"target_sha",
|
|
"workflow_sha",
|
|
"job",
|
|
"variant",
|
|
"status",
|
|
"job_status",
|
|
"step_outcomes",
|
|
]) {
|
|
expect(recordStep?.run).toContain(`printf '${field}=`);
|
|
}
|
|
expect(uploadStep?.if).toBe("always()");
|
|
expect(uploadStep?.with?.name).toBe(
|
|
"release-check-status-qa-live-telegram-${{ inputs.target_sha }}-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(uploadStep?.with?.path).toContain("${{ steps.record_status.outputs.status_file }}");
|
|
expect(uploadStep?.with?.path).toContain("${{ steps.record_status.outputs.evidence_file }}");
|
|
expect(requireStep?.if).toBe("always()");
|
|
expect(requireStep?.run).toContain('[[ "$STATUS" == "success" ]]');
|
|
});
|
|
|
|
it("records producer failure and rejects the terminal status", () => {
|
|
const result = runAdvisoryStatus({
|
|
BUILD_RESULT: "failure",
|
|
BUILD_STATUS: "failure",
|
|
});
|
|
|
|
expect(result.recordResult.status).toBe(0);
|
|
expect(result.status).toBe("failure");
|
|
expect(result.statusFile).toContain("status=failure\n");
|
|
expect(result.statusFile).toContain("build:failure");
|
|
expect(result.requireResult.status).toBe(1);
|
|
});
|
|
|
|
it("records empty producer output and rejects the terminal status", () => {
|
|
const result = runAdvisoryStatus({
|
|
IDENTITY_STATUS: "",
|
|
});
|
|
|
|
expect(result.recordResult.status).toBe(0);
|
|
expect(result.status).toBe("failure");
|
|
expect(result.statusFile).toContain("status=failure\n");
|
|
expect(result.statusFile).toContain("identity: ");
|
|
expect(result.requireResult.status).toBe(1);
|
|
});
|
|
|
|
it("accepts only complete successful producer evidence", () => {
|
|
const result = runAdvisoryStatus();
|
|
|
|
expect(result.recordResult.status).toBe(0);
|
|
expect(result.status).toBe("success");
|
|
expect(result.statusFile).toContain("status=success\n");
|
|
expect(result.requireResult.status).toBe(0);
|
|
});
|
|
|
|
it("keeps the isolated SUT lifetime below the credential lease TTL", () => {
|
|
const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
};
|
|
const job = workflow.jobs?.run_telegram;
|
|
expect(job?.["runs-on"]).toBe("ubuntu-24.04");
|
|
expect(job?.["timeout-minutes"]).toBe(60);
|
|
|
|
const validateStep = job?.steps?.find(
|
|
(step) => step.name === "Validate required QA credential env",
|
|
);
|
|
expect(validateStep?.env?.RUNNER_ENVIRONMENT).toBe("${{ runner.environment }}");
|
|
expect(validateStep?.env?.CREDENTIAL_ACQUIRE_TIMEOUT_MS).toBe("60000");
|
|
expect(validateStep?.env?.JOB_TIMEOUT_MINUTES).toBe("60");
|
|
expect(validateStep?.env?.LEASE_TTL_MS).toBe("7200000");
|
|
expect(validateStep?.run).toContain('[[ "$RUNNER_ENVIRONMENT" == "github-hosted" ]]');
|
|
expect(validateStep?.run).toContain("JOB_TIMEOUT_MINUTES * 60 * 1000 < LEASE_TTL_MS");
|
|
|
|
const runStep = job?.steps?.find((step) => step.name === "Run Telegram live lane");
|
|
expect(runStep?.env?.OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS).toBe("60000");
|
|
expect(runStep?.env?.OPENCLAW_QA_CREDENTIAL_LEASE_TTL_MS).toBe("7200000");
|
|
expect(runStep?.env?.OPENCLAW_QA_TELEGRAM_SUT_CLEANUP_TIMEOUT_MS).toBe("60000");
|
|
expect(runStep?.run).toContain("trap terminate_sut_uid_on_exit EXIT");
|
|
expect(runStep?.run).toContain('"$OPENCLAW_QA_TELEGRAM_SUT_OPENCLAW_COMMAND" --terminate-uid');
|
|
expect(runStep?.run).toContain("run_qa_attempt preflight --scenario channel-canary");
|
|
expect(runStep?.run).toContain(
|
|
"Telegram channel canary failed; skipping the remaining scenarios.",
|
|
);
|
|
expect(runStep?.run).toContain("--list-scenarios");
|
|
expect(runStep?.run).toContain('"$scenario_id" != "channel-canary"');
|
|
expect(runStep?.run).toContain(
|
|
'run_qa_attempt "attempt-${attempt}" "${remaining_scenarios[@]}"',
|
|
);
|
|
expect(
|
|
runStep?.run?.indexOf("run_qa_attempt preflight --scenario channel-canary"),
|
|
).toBeLessThan(runStep?.run?.indexOf("for attempt in 1 2") ?? -1);
|
|
});
|
|
|
|
it("serializes stderr behind the workflow-command pause", () => {
|
|
const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
};
|
|
const runStep = workflow.jobs?.run_telegram?.steps?.find(
|
|
(step) => step.name === "Run Telegram live lane",
|
|
);
|
|
expect(runStep?.run).toMatch(
|
|
/run_qa_attempt\(\) \(\n\s+set -euo pipefail\n\s+exec 2>&1\n\s+output_name=/u,
|
|
);
|
|
expect(runStep?.run).toContain("::stop-commands::%s");
|
|
});
|
|
|
|
it("derives SUT-writable paths from the verified runtime root after sudo", () => {
|
|
const source = readFileSync(WORKFLOW_PATH, "utf8");
|
|
expect(source).toContain("Telegram SUT launcher failed: stage=%s line=%s status=%s");
|
|
expect(source).toContain("launcher_stage=root-run-setup");
|
|
expect(source).toContain("launcher_stage=enter-mount-namespace");
|
|
expect(source).toContain("set_launcher_stage mask-host-paths");
|
|
expect(source).toContain('launcher_stage_file="${RUNTIME_ROOT}/launcher-stage-${BASHPID}"');
|
|
expect(source).toContain('set_launcher_stage "mask-host-path:${masked_path}"');
|
|
expect(source).toContain("set_launcher_stage mount-proc");
|
|
expect(source).toContain("set_launcher_stage write-identity");
|
|
expect(source).toContain("set_launcher_stage launch-runtime");
|
|
expect(source).toMatch(/set_launcher_stage launch-runtime\n\s+unset launcher_stage_file/u);
|
|
expect(source).toContain("Telegram SUT runtime preflight failed: stage=%s line=%s status=%s");
|
|
expect(source).toContain("runtime_stage=verify-runtime-identity");
|
|
expect(source).toContain("runtime_stage=verify-runtime-privileges");
|
|
expect(source).toContain("runtime_stage=verify-parent-proc-hidden");
|
|
expect(source).toContain("runtime_stage=verify-proc-visibility");
|
|
expect(source).toContain("for _ in {1..100}; do");
|
|
expect(source).toMatch(
|
|
/if tr "\\0" "\\n" <"\/proc\/\$\{control_pid\}\/environ" 2>\/dev\/null \|\n\s+grep -Fxq "OPENCLAW_QA_PROC_CONTROL=visible"; then/u,
|
|
);
|
|
expect(source).toContain("proc_marker_visible=true\n break");
|
|
expect(source).toContain("sleep 0.05");
|
|
expect(source).toContain('[[ "$proc_marker_visible" == "true" ]]');
|
|
expect(source).toMatch(
|
|
/kill "\$control_pid" >\/dev\/null 2>&1 \|\| true\n\s+wait "\$control_pid" \|\| true/u,
|
|
);
|
|
expect(source).toContain("runtime_stage=verify-secret-env-hidden");
|
|
expect(source).toContain("runtime_stage=verify-runner-fds-hidden");
|
|
expect(source).toContain("runtime_stage=verify-runtime-files");
|
|
expect(source).toContain("runtime_stage=verify-host-paths-hidden");
|
|
expect(source).toContain("runtime_stage=sanitize-runtime-env");
|
|
expect(source).toContain("runtime_stage=verify-runtime-env");
|
|
expect(source).toContain("runtime_stage=write-sandbox-proof");
|
|
expect(source).toContain("runtime_stage=exec-runtime");
|
|
expect(source).toContain('TMPDIR="${SUT_RUNTIME_ROOT}/tmp"');
|
|
expect(source).toContain('"$RUNTIME_ROOT"/tmp/openclaw-qa-suite-*');
|
|
expect(source.indexOf("launcher_stage=enter-mount-namespace")).toBeLessThan(
|
|
source.indexOf("/usr/bin/unshare"),
|
|
);
|
|
expect(source).not.toContain("exec /usr/bin/unshare");
|
|
expect(source).not.toContain("set -x");
|
|
expect(source).toContain('source_node_bin="$(realpath -e "$(command -v node)")"');
|
|
expect(source).toContain('node_bin="${runtime_root}/node"');
|
|
expect(source).toContain('sudo install -o root -g root -m 0555 "$source_node_bin" "$node_bin"');
|
|
expect(source).toContain(
|
|
'[[ "$(stat -c \'%F:%a:%u:%g\' "$node_bin")" == "regular file:555:0:0" ]]',
|
|
);
|
|
expect(source).toContain('"$node_bin" --version >/dev/null');
|
|
expect(source).toContain('for masked_path in "$RUNNER_HOME" /tmp /var/tmp /dev/shm');
|
|
expect(source).not.toMatch(/^\s+node_bin="\$\(realpath -e "\$\(command -v node\)"\)"$/mu);
|
|
expect(source).toContain('temp_root="$(realpath -e "${OPENCLAW_QA_TEMP_ROOT:?}")"');
|
|
expect(source).toContain("sudo install -d -o root -g root -m 0700 /tmp/openclaw");
|
|
expect(source).toContain(
|
|
'-m 0711 \\\n "${runtime_root}/tmp/openclaw-${runner_uid}"',
|
|
);
|
|
expect(source).toContain('"$RUNTIME_ROOT"/tmp/openclaw-"$RUNNER_UID"/openclaw-qa-suite-*');
|
|
expect(source).toContain('proc_stat="$(cat "/proc/${pid}/stat")"');
|
|
expect(source).not.toContain('proc_stat="$(cat /proc/self/stat)"');
|
|
expect(source).toContain('if [[ "${1:-}" == "--root-verify" ]]');
|
|
expect(source).toContain("signal.pidfd_send_signal(pidfd, signal_value)");
|
|
expect(source).toContain('actual_executable="$(realpath -e "/proc/${pid}/exe")"');
|
|
expect(source).toContain("cmdlineSha256: $cmdlineSha256");
|
|
expect(source).toContain('export HOME="${temp_root}/home"');
|
|
expect(source).toContain('export XDG_CONFIG_HOME="${temp_root}/xdg-config"');
|
|
expect(source).toContain('if [[ "${1:-}" == "--root-terminate-uid" ]]');
|
|
});
|
|
|
|
it("keeps the generated SUT launcher valid bash", () => {
|
|
const createSutStep = workflowStep(
|
|
workflowJob("run_telegram"),
|
|
"Create isolated Telegram SUT identity and launcher",
|
|
);
|
|
const launcherSource = createSutStep.run?.match(
|
|
/<<'LAUNCHER'\n([\s\S]*?)\nLAUNCHER(?:\n|$)/u,
|
|
)?.[1];
|
|
expect(launcherSource).toBeTruthy();
|
|
|
|
const result = spawnSync("bash", ["-n"], {
|
|
encoding: "utf8",
|
|
input: launcherSource,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it("arms the boundary preload only in the gateway main thread", () => {
|
|
const createSutStep = workflowStep(
|
|
workflowJob("run_telegram"),
|
|
"Create isolated Telegram SUT identity and launcher",
|
|
);
|
|
const preloadSource = createSutStep.run?.match(/<<'PRELOAD'\n([\s\S]*?)\nPRELOAD/u)?.[1];
|
|
expect(preloadSource).toBeTruthy();
|
|
|
|
const workdir = tempDirs.make("openclaw-telegram-preload-");
|
|
const preloadPath = join(workdir, "preload.mjs");
|
|
writeFileSync(preloadPath, preloadSource ?? "");
|
|
const env = { ...process.env };
|
|
delete env.OPENCLAW_QA_SUT_PREENTRY_STOP;
|
|
|
|
const mainResult = spawnSync(process.execPath, ["--import", preloadPath, "-e", ""], {
|
|
encoding: "utf8",
|
|
env,
|
|
});
|
|
expect(mainResult.status).not.toBe(0);
|
|
expect(mainResult.stderr).toContain("trusted Telegram SUT preload was not armed");
|
|
|
|
const workerResult = spawnSync(
|
|
process.execPath,
|
|
[
|
|
"-e",
|
|
`const { Worker } = require("node:worker_threads");\n` +
|
|
`const worker = new Worker('require("node:worker_threads").parentPort.postMessage("ready")', { eval: true, execArgv: ["--import", ${JSON.stringify(preloadPath)}] });\n` +
|
|
`worker.once("message", () => process.exit(0));\n` +
|
|
`worker.once("error", (error) => { console.error(error); process.exit(1); });`,
|
|
],
|
|
{ encoding: "utf8", env, killSignal: "SIGKILL", timeout: 5_000 },
|
|
);
|
|
expect(workerResult.status, workerResult.stderr).toBe(0);
|
|
});
|
|
|
|
it("reports the namespace-entry stage when its supervised child fails", () => {
|
|
const source = readFileSync(WORKFLOW_PATH, "utf8");
|
|
const trapLine = source.match(/^\s+(trap 'exit_status=.*' ERR)$/mu)?.[1];
|
|
expect(trapLine).toBeTruthy();
|
|
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`set -Eeuo pipefail\nlauncher_stage=enter-mount-namespace\n${trapLine}\nbash -c 'exit 23'`,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
|
|
expect(result.status).toBe(23);
|
|
expect(result.stderr).toMatch(
|
|
/Telegram SUT launcher failed: stage=enter-mount-namespace line=[0-9]+ status=23/u,
|
|
);
|
|
});
|
|
|
|
it("reports the persisted inner launcher stage when namespace supervision fails", () => {
|
|
const source = readFileSync(WORKFLOW_PATH, "utf8");
|
|
const trapLine = source.match(/^\s+(trap 'exit_status=.*' ERR)$/mu)?.[1];
|
|
expect(trapLine).toBeTruthy();
|
|
|
|
const workdir = tempDirs.make("openclaw-telegram-launcher-stage-");
|
|
const stagePath = join(workdir, "stage");
|
|
writeFileSync(stagePath, "mount-proc\n");
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`set -Eeuo pipefail\nlauncher_stage=enter-mount-namespace\nlauncher_stage_file=${JSON.stringify(stagePath)}\n${trapLine}\nbash -c 'exit 23'`,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
|
|
expect(result.status).toBe(23);
|
|
expect(result.stderr).toMatch(
|
|
/Telegram SUT launcher failed: stage=mount-proc line=[0-9]+ status=23/u,
|
|
);
|
|
});
|
|
|
|
it("reports the exact failing runtime preflight line", () => {
|
|
const source = readFileSync(WORKFLOW_PATH, "utf8");
|
|
const diagnosticSource = source.match(
|
|
/^\s+(fail_runtime_stage\(\) \{[\s\S]*?^\s+\}\n\s+trap "fail_runtime_stage \\?\$\? \\?\$LINENO" ERR)$/mu,
|
|
)?.[1];
|
|
expect(diagnosticSource).toBeTruthy();
|
|
|
|
const script = `set -Eeuo pipefail\nruntime_stage=runtime-test\n${diagnosticSource}\nfalse`;
|
|
const failureLine = script.split("\n").findIndex((line) => line === "false") + 1;
|
|
const result = spawnSync("bash", ["-c", script], { encoding: "utf8" });
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
`Telegram SUT runtime preflight failed: stage=runtime-test line=${failureLine} status=1`,
|
|
);
|
|
});
|
|
});
|