mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ci): release workflow checks fail on macOS Bash 3.2 (#121669)
* fix(ci): keep release workflow checks portable on macOS * fix(ci): repair current main compact shard regressions * fix(ci): repair latest main validation drift * fix(approvals): restore native account ownership gates * fix(ci): repair skill workshop validation drift * fix(ci): align approval route selection with main * fix(ci): remove stale skill workshop test exports * fix(ci): align repairs with latest main
This commit is contained in:
committed by
GitHub
parent
d6317094a9
commit
3cd034f7a8
@@ -507,7 +507,7 @@ jobs:
|
||||
done
|
||||
|
||||
if [[ "$qa_filter_seen" == "true" ]]; then
|
||||
repo_live_suite_filter="$(IFS=,; printf '%s' "${repo_filter_tokens[*]:-}")"
|
||||
repo_live_suite_filter="$(IFS=,; printf '%s' "${repo_filter_tokens[*]-}")"
|
||||
fi
|
||||
|
||||
if [[ "${#disabled_required_lanes[@]}" -gt 0 ]]; then
|
||||
|
||||
@@ -135,7 +135,12 @@ describe("executeAgentTurn: lifecycle progress", () => {
|
||||
name: "read",
|
||||
phase: "start",
|
||||
status: "running",
|
||||
summary: undefined,
|
||||
progressText: undefined,
|
||||
meta: undefined,
|
||||
commandBearing: false,
|
||||
approvalId: undefined,
|
||||
approvalSlug: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -227,7 +232,12 @@ describe("executeAgentTurn: lifecycle progress", () => {
|
||||
name: "bash",
|
||||
phase: "start",
|
||||
status: "running",
|
||||
summary: undefined,
|
||||
progressText: undefined,
|
||||
meta: undefined,
|
||||
commandBearing: false,
|
||||
approvalId: undefined,
|
||||
approvalSlug: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CronService } from "./service.js";
|
||||
import { setupCronServiceSuite } from "./service.test-harness.js";
|
||||
import type { CronServiceDeps } from "./service/state.js";
|
||||
|
||||
const { logger, makeStorePath } = setupCronServiceSuite({ prefix: "cron-stream-trigger-" });
|
||||
|
||||
@@ -194,7 +195,9 @@ describe("cron stream trigger composition", () => {
|
||||
it("reports a failed payload batch without reporting it fired", async () => {
|
||||
const { storePath } = await makeStorePath();
|
||||
const onTriggerDisposition = vi.fn();
|
||||
const sendCronFailureAlert = vi.fn(async () => undefined);
|
||||
const sendCronFailureAlert = vi.fn<NonNullable<CronServiceDeps["sendCronFailureAlert"]>>(
|
||||
async () => undefined,
|
||||
);
|
||||
const cron = new CronService({
|
||||
storePath,
|
||||
cronEnabled: true,
|
||||
@@ -239,15 +242,15 @@ describe("cron stream trigger composition", () => {
|
||||
consecutiveErrors: 1,
|
||||
});
|
||||
expect(sendCronFailureAlert).toHaveBeenCalledOnce();
|
||||
expect(sendCronFailureAlert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
text:
|
||||
'Automation "failing stream payload" failed 1 times\n' +
|
||||
"Check automation history for details.",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const alert = sendCronFailureAlert.mock.calls[0]?.[0];
|
||||
expect(alert?.channel).toBe("telegram");
|
||||
expect(alert?.to).toBe("19098680");
|
||||
expect(alert?.payload).toEqual({
|
||||
text:
|
||||
'Automation "failing stream payload" failed 1 times\n' +
|
||||
"Check automation history for details.",
|
||||
});
|
||||
expect(alert?.job.state.lastError).toBe("boom");
|
||||
} finally {
|
||||
cron.stop();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
CronAgentExecutionStarted,
|
||||
CronJob,
|
||||
} from "../types.js";
|
||||
import { createCronServiceState } from "./state.js";
|
||||
import { createCronServiceState, type CronServiceDeps } from "./state.js";
|
||||
import { onTimer } from "./timer.test-support.js";
|
||||
|
||||
const timerRegressionFixtures = setupCronRegressionFixtures({
|
||||
@@ -723,7 +723,9 @@ describe("cron service timer regressions", () => {
|
||||
const started = createDeferred();
|
||||
let abortObserved = false;
|
||||
const cleanupTimedOutAgentRun = vi.fn(async () => {});
|
||||
const sendCronFailureAlert = vi.fn(async () => {});
|
||||
const sendCronFailureAlert = vi.fn<NonNullable<CronServiceDeps["sendCronFailureAlert"]>>(
|
||||
async () => {},
|
||||
);
|
||||
const state = createCronServiceState({
|
||||
cronEnabled: true,
|
||||
storePath: store.storePath,
|
||||
@@ -775,23 +777,29 @@ describe("cron service timer regressions", () => {
|
||||
await timerPromise;
|
||||
|
||||
const job = requireJob(state, "isolated-before-agent-reply-unhandled-82811");
|
||||
const diagnostic =
|
||||
"cron: isolated agent run stalled before execution start (last phase: runtime-plugins)";
|
||||
expect(abortObserved).toBe(true);
|
||||
expect(job.state.lastStatus).toBe("error");
|
||||
expect(job.state.lastError).toContain("stalled before execution start");
|
||||
expect(job.state.lastError).toContain("runtime-plugins");
|
||||
expect(job.state.lastError).toBe(diagnostic);
|
||||
expect(job.state.lastDiagnosticSummary).toBe(diagnostic);
|
||||
expect(job.state.lastDiagnostics).toEqual({
|
||||
summary: diagnostic,
|
||||
entries: [
|
||||
{ source: "cron-setup", severity: "error", message: diagnostic, ts: scheduledAt },
|
||||
],
|
||||
});
|
||||
expect(cleanupTimedOutAgentRun).toHaveBeenCalledTimes(1);
|
||||
expect(sendCronFailureAlert).toHaveBeenCalledTimes(1);
|
||||
expect(sendCronFailureAlert).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
to: "12345",
|
||||
payload: expect.objectContaining({
|
||||
text:
|
||||
'Automation "before agent reply unhandled regression" failed 1 times\n' +
|
||||
"Check automation history for details.",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const alert = sendCronFailureAlert.mock.calls[0]?.[0];
|
||||
expect(alert?.channel).toBe("telegram");
|
||||
expect(alert?.to).toBe("12345");
|
||||
expect(alert?.payload).toEqual({
|
||||
text:
|
||||
'Automation "before agent reply unhandled regression" failed 1 times\n' +
|
||||
"Check automation history for details.",
|
||||
});
|
||||
expect(alert?.job.state.lastDiagnosticSummary).toBe(diagnostic);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function runExclusiveSystemAgentSetupActivation<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves after both the wizard runner and its setup-target admission have settled. */
|
||||
export function whenAdmittedWizardSessionSettled(session: {
|
||||
whenSettled(): Promise<unknown>;
|
||||
}): Promise<unknown> {
|
||||
|
||||
@@ -250,26 +250,12 @@ describe("resolveApprovalOverGateway", () => {
|
||||
|
||||
it("sends channel custody to an injected canonical runtime", async () => {
|
||||
const injectedRequest = vi.fn(async () => ({ applied: true, approval: recordedApproval }));
|
||||
const scopedRequest: GatewayNativeApprovalRuntime["request"] = vi.fn(
|
||||
async <T = unknown>(method: string): Promise<T> => {
|
||||
const fixture =
|
||||
method === "exec.approval.list"
|
||||
? [
|
||||
{
|
||||
id: "approval-1",
|
||||
request: {
|
||||
command: "printf approval",
|
||||
turnSourceChannel: "imessage",
|
||||
turnSourceAccountId: "personal",
|
||||
},
|
||||
},
|
||||
]
|
||||
: { applied: true, approval: recordedApproval };
|
||||
return fixture as T;
|
||||
},
|
||||
) as GatewayNativeApprovalRuntime["request"];
|
||||
const scopedRequest = vi.fn();
|
||||
const runtime = {
|
||||
request: scopedRequest,
|
||||
request: async <T>(): Promise<T> => {
|
||||
scopedRequest();
|
||||
throw new Error("unexpected scoped approval request");
|
||||
},
|
||||
requestRoute: vi.fn(),
|
||||
routeCoordinator: { doesAccountHandleRequest: () => true } as never,
|
||||
subscribe: vi.fn(),
|
||||
|
||||
@@ -773,6 +773,8 @@ export function createNativeApprovalChannelRouteGates<TTarget extends NativeAppr
|
||||
approvalKind: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean => {
|
||||
// Per-account runtimes report raw candidates here. The route coordinator rejects
|
||||
// unbound multi-account groups as ambiguous before any runtime can deliver.
|
||||
const accountId = input.accountId ?? params.resolveDefaultAccountId(input.cfg);
|
||||
const eligibleAccountIds = params.isTransportEnabled({ cfg: input.cfg, accountId })
|
||||
? [accountId]
|
||||
|
||||
@@ -20,10 +20,28 @@ import { getArchivedSkillFiles } from "./curator.js";
|
||||
import { readSkillProposalTargetTreeSha256 } from "./proposal-bundle.js";
|
||||
import { withSkillCollectionLock } from "./target-lock.js";
|
||||
|
||||
type CopyDirectoryHook = (
|
||||
source: unknown,
|
||||
destination: unknown,
|
||||
options?: unknown,
|
||||
) => Promise<void>;
|
||||
|
||||
const copyDirectoryBefore = vi.hoisted(() => vi.fn<CopyDirectoryHook>(async () => {}));
|
||||
const copyDirectoryAfter = vi.hoisted(() => vi.fn<CopyDirectoryHook>(async () => {}));
|
||||
const dispatchCommittedSkillChangeBestEffort = vi.hoisted(() =>
|
||||
vi.fn(async (_event: { action: string }) => {}),
|
||||
);
|
||||
const snapshotCommittedSkillArtifactBestEffort = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||
const cp: typeof actual.cp = async (source, destination, options) => {
|
||||
await copyDirectoryBefore(source, destination, options);
|
||||
await actual.cp(source, destination, options);
|
||||
await copyDirectoryAfter(source, destination, options);
|
||||
};
|
||||
const patched = { ...actual, cp };
|
||||
return { ...patched, default: patched };
|
||||
});
|
||||
vi.mock("../lifecycle/skill-change-hook.js", () => ({
|
||||
hasCommittedSkillChangeHooks: () => true,
|
||||
snapshotCommittedSkillArtifactBestEffort,
|
||||
@@ -35,6 +53,10 @@ let testState: OpenClawTestState;
|
||||
let workspaceDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
copyDirectoryBefore.mockReset();
|
||||
copyDirectoryBefore.mockResolvedValue(undefined);
|
||||
copyDirectoryAfter.mockReset();
|
||||
copyDirectoryAfter.mockResolvedValue(undefined);
|
||||
dispatchCommittedSkillChangeBestEffort.mockClear();
|
||||
snapshotCommittedSkillArtifactBestEffort.mockReset();
|
||||
snapshotCommittedSkillArtifactBestEffort.mockResolvedValue(undefined);
|
||||
@@ -239,9 +261,7 @@ describe("skill collection reconciliation", () => {
|
||||
await fs.mkdir(path.dirname(supportFile), { recursive: true });
|
||||
await fs.writeFile(supportFile, "Before\n", "utf8");
|
||||
const receipt = await readCollectionReceipt();
|
||||
const copy = fs.cp.bind(fs);
|
||||
const copySpy = vi.spyOn(fs, "cp").mockImplementation(async (source, destination, options) => {
|
||||
await copy(source, destination, options);
|
||||
copyDirectoryAfter.mockImplementationOnce(async () => {
|
||||
await fs.appendFile(supportFile, "External edit\n", "utf8");
|
||||
});
|
||||
|
||||
@@ -260,7 +280,7 @@ describe("skill collection reconciliation", () => {
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow("Skill tree changed before collection mutation: procedure");
|
||||
copySpy.mockRestore();
|
||||
copyDirectoryAfter.mockReset();
|
||||
|
||||
await expect(fs.readFile(path.join(skillDir, "SKILL.md"), "utf8")).resolves.toContain(
|
||||
"# Original",
|
||||
@@ -450,12 +470,16 @@ describe("skill collection reconciliation", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
const skillDir = path.join(workspaceDir, "skills", "procedure");
|
||||
const canonicalWorkspaceDir = await fs.realpath(workspaceDir);
|
||||
const skillDir = path.join(canonicalWorkspaceDir, "skills", "procedure");
|
||||
const skillFile = path.join(skillDir, "SKILL.md");
|
||||
const backupRoot = path.join(testState.stateDir, "skill-workshop", "collection-backups");
|
||||
const originalCopy = fs.cp.bind(fs);
|
||||
const backupRoot = path.join(
|
||||
await fs.realpath(testState.stateDir),
|
||||
"skill-workshop",
|
||||
"collection-backups",
|
||||
);
|
||||
let failed = false;
|
||||
const copySpy = vi.spyOn(fs, "cp").mockImplementation(async (source, destination, options) => {
|
||||
copyDirectoryBefore.mockImplementation(async (source, destination) => {
|
||||
if (
|
||||
!failed &&
|
||||
String(source).startsWith(backupRoot) &&
|
||||
@@ -465,7 +489,6 @@ describe("skill collection reconciliation", () => {
|
||||
failed = true;
|
||||
throw new Error("forced restore copy failure");
|
||||
}
|
||||
await originalCopy(source, destination, options);
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -473,7 +496,7 @@ describe("skill collection reconciliation", () => {
|
||||
restoreLatestSkillCollectionBackup({ workspaceDir, env: testState.env }),
|
||||
).rejects.toThrow("forced restore copy failure");
|
||||
} finally {
|
||||
copySpy.mockRestore();
|
||||
copyDirectoryBefore.mockReset();
|
||||
}
|
||||
await expect(fs.readFile(skillFile, "utf8")).resolves.toContain("# Clean");
|
||||
|
||||
@@ -498,14 +521,12 @@ describe("skill collection reconciliation", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
const skillDir = path.join(workspaceDir, "skills", "procedure");
|
||||
const skillDir = path.join(await fs.realpath(workspaceDir), "skills", "procedure");
|
||||
const beforeVersion = getSkillsSnapshotVersion();
|
||||
const originalCopy = fs.cp.bind(fs);
|
||||
const copySpy = vi.spyOn(fs, "cp").mockImplementation(async (source, destination, options) => {
|
||||
copyDirectoryBefore.mockImplementation(async (source, destination) => {
|
||||
if (path.resolve(String(destination)) === path.resolve(skillDir)) {
|
||||
throw new Error(`forced restore copy failure: ${String(source)}`);
|
||||
}
|
||||
await originalCopy(source, destination, options);
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -513,7 +534,7 @@ describe("skill collection reconciliation", () => {
|
||||
restoreLatestSkillCollectionBackup({ workspaceDir, env: testState.env }),
|
||||
).rejects.toThrow("current collection was not restored");
|
||||
} finally {
|
||||
copySpy.mockRestore();
|
||||
copyDirectoryBefore.mockReset();
|
||||
}
|
||||
|
||||
expect(getSkillsSnapshotVersion()).toBeGreaterThan(beforeVersion);
|
||||
|
||||
@@ -367,6 +367,7 @@ describe("skill collection review", () => {
|
||||
|
||||
it("groups symlink aliases before comparing shared-workspace identities", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-collection-review-real-workspace-");
|
||||
const canonicalWorkspaceDir = await fs.realpath(workspaceDir);
|
||||
const aliasParent = await tempDirs.make("openclaw-collection-review-alias-parent-");
|
||||
const workspaceAlias = path.join(aliasParent, "workspace-alias");
|
||||
await fs.symlink(
|
||||
@@ -400,7 +401,7 @@ describe("skill collection review", () => {
|
||||
onError,
|
||||
});
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error), workspaceDir);
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error), canonicalWorkspaceDir);
|
||||
expect(runWithGatewayIndependentRootWorkAdmission).not.toHaveBeenCalled();
|
||||
expect(runEmbeddedAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -450,6 +451,7 @@ describe("skill collection review", () => {
|
||||
|
||||
it("admits and reports each workspace independently", async () => {
|
||||
const oversizedWorkspace = await tempDirs.make("openclaw-collection-review-failed-");
|
||||
const canonicalOversizedWorkspace = await fs.realpath(oversizedWorkspace);
|
||||
const healthyWorkspace = await tempDirs.make("openclaw-collection-review-healthy-");
|
||||
await writeWorkspaceSkills(oversizedWorkspace, [
|
||||
{ name: "oversized", description: "Oversized", body: "x".repeat(240_001) },
|
||||
@@ -489,7 +491,7 @@ describe("skill collection review", () => {
|
||||
});
|
||||
|
||||
expect(runWithGatewayIndependentRootWorkAdmission).toHaveBeenCalledTimes(2);
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error), oversizedWorkspace);
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error), canonicalOversizedWorkspace);
|
||||
expect(runEmbeddedAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -3871,7 +3871,7 @@ describe("package artifact reuse", () => {
|
||||
expect(workflow).toContain("repo_live_suite_filter:");
|
||||
expect(workflow).toContain('repo_filter_tokens+=("$token")');
|
||||
expect(workflow).toContain(
|
||||
'repo_live_suite_filter="$(IFS=,; printf \'%s\' "${repo_filter_tokens[*]:-}")"',
|
||||
'repo_live_suite_filter="$(IFS=,; printf \'%s\' "${repo_filter_tokens[*]-}")"',
|
||||
);
|
||||
expect(workflow).toContain("cross_os_suite_filter:");
|
||||
expect(workflow).toContain("advisory: false");
|
||||
@@ -6356,12 +6356,11 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
||||
const releaseChecksParent = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_checks");
|
||||
expect(releaseChecksParent["runs-on"]).toBe("blacksmith-4vcpu-ubuntu-2404");
|
||||
expect(releaseChecksParent["timeout-minutes"]).toBe(420);
|
||||
const releasePackageTimeouts = Object.fromEntries(
|
||||
profiles.map((profile) => [
|
||||
profile,
|
||||
releasePackagePaths[profile].reduce((total, timeout) => total + timeout, 0),
|
||||
]),
|
||||
) as Record<(typeof profiles)[number], number>;
|
||||
const releasePackageTimeouts = {
|
||||
beta: releasePackagePaths.beta.reduce((total, timeout) => total + timeout, 0),
|
||||
stable: releasePackagePaths.stable.reduce((total, timeout) => total + timeout, 0),
|
||||
full: releasePackagePaths.full.reduce((total, timeout) => total + timeout, 0),
|
||||
};
|
||||
expect(releasePackageTimeouts).toEqual({ beta: 280, stable: 280, full: 310 });
|
||||
for (const [profile, childTimeout] of Object.entries(releasePackageTimeouts)) {
|
||||
expect(childTimeout, `release-package:${profile}`).toBeLessThanOrEqual(420);
|
||||
|
||||
Reference in New Issue
Block a user