mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 10:34:44 +00:00
fix(ci): keep full release validation aligned with runtime state (#116653)
Align Telegram artifact reuse and packaged Docker validation with current GitHub Actions and runtime state contracts. Refresh affected regression fixtures and isolate test state.
This commit is contained in:
2
.github/workflows/npm-telegram-beta-e2e.yml
vendored
2
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -352,7 +352,7 @@ jobs:
|
||||
}
|
||||
attempt_started_at="$(jq -er '.run_started_at | fromdateiso8601' <<< "$attempt_json")"
|
||||
if [[ "$ARTIFACT_RUN_ID" == "$GITHUB_RUN_ID" ]]; then
|
||||
jq -e '(.status == "pending" or .status == "queued" or .status == "in_progress") and .conclusion == null' \
|
||||
jq -e '(.status == "pending" or .status == "queued" or .status == "requested" or .status == "waiting" or .status == "in_progress") and .conclusion == null' \
|
||||
<<< "$attempt_json" >/dev/null || {
|
||||
echo "Current-run Package Telegram artifact is not from the active workflow attempt." >&2
|
||||
exit 1
|
||||
|
||||
@@ -902,12 +902,18 @@ describe("handleTelegramAction", () => {
|
||||
expect(entries[0]).toMatchObject({
|
||||
channel: "telegram",
|
||||
to: "12345",
|
||||
payloads: [
|
||||
{
|
||||
text: "times out after queue write",
|
||||
delivery: { pin: { enabled: true, required: true } },
|
||||
},
|
||||
],
|
||||
preparedBatch: {
|
||||
sourcePayloadCount: 1,
|
||||
entries: [
|
||||
{
|
||||
status: "accepted",
|
||||
payload: {
|
||||
text: "times out after queue write",
|
||||
delivery: { pin: { enabled: true, required: true } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
session: { key: "agent:main:telegram:direct:12345", agentId: "main" },
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
retryCount: 0,
|
||||
@@ -917,12 +923,20 @@ describe("handleTelegramAction", () => {
|
||||
.mockImplementationOnce(async () => {
|
||||
const entries = readDurableQueueEntries();
|
||||
const liveEntry = entries.find((entry) =>
|
||||
JSON.stringify(entry.payloads).includes("delivers after queue write"),
|
||||
JSON.stringify(entry.preparedBatch).includes("delivers after queue write"),
|
||||
);
|
||||
expect(liveEntry).toMatchObject({
|
||||
channel: "telegram",
|
||||
to: "12345",
|
||||
payloads: [{ text: "delivers after queue write" }],
|
||||
preparedBatch: {
|
||||
sourcePayloadCount: 1,
|
||||
entries: [
|
||||
{
|
||||
status: "accepted",
|
||||
payload: { text: "delivers after queue write" },
|
||||
},
|
||||
],
|
||||
},
|
||||
retryCount: 0,
|
||||
});
|
||||
return { channel: "telegram", messageId: "tg-ok" };
|
||||
@@ -978,12 +992,18 @@ describe("handleTelegramAction", () => {
|
||||
const retryableEntries = readDurableQueueEntries();
|
||||
expect(retryableEntries).toHaveLength(1);
|
||||
expect(retryableEntries[0]).toMatchObject({
|
||||
payloads: [
|
||||
{
|
||||
text: "times out after queue write",
|
||||
delivery: { pin: { enabled: true, required: true } },
|
||||
},
|
||||
],
|
||||
preparedBatch: {
|
||||
sourcePayloadCount: 1,
|
||||
entries: [
|
||||
{
|
||||
status: "accepted",
|
||||
payload: {
|
||||
text: "times out after queue write",
|
||||
delivery: { pin: { enabled: true, required: true } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
retryCount: 1,
|
||||
});
|
||||
expect(String(retryableEntries[0]?.lastError)).toContain("telegram timeout");
|
||||
@@ -1004,12 +1024,18 @@ describe("handleTelegramAction", () => {
|
||||
});
|
||||
expect(readDurableQueueEntries()).toHaveLength(1);
|
||||
expect(readDurableQueueEntries()[0]).toMatchObject({
|
||||
payloads: [
|
||||
{
|
||||
text: "times out after queue write",
|
||||
delivery: { pin: { enabled: true, required: true } },
|
||||
},
|
||||
],
|
||||
preparedBatch: {
|
||||
sourcePayloadCount: 1,
|
||||
entries: [
|
||||
{
|
||||
status: "accepted",
|
||||
payload: {
|
||||
text: "times out after queue write",
|
||||
delivery: { pin: { enabled: true, required: true } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
retryCount: 1,
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -3,11 +3,12 @@ import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { enqueueCommitmentExtraction } from "../../dist/commitments/runtime.js";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
drainCommitmentExtractionQueue,
|
||||
enqueueCommitmentExtraction,
|
||||
resetCommitmentExtractionRuntimeForTests,
|
||||
} from "../../dist/commitments/runtime.test-support.js";
|
||||
} from "../../dist/commitments/runtime.js";
|
||||
import {
|
||||
listCommitments,
|
||||
listDueCommitmentsForSession,
|
||||
@@ -128,10 +129,33 @@ async function runPackagedDoctor(stateDir: string): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
function verifyRuntimeIgnoresLegacyJsonInChild(nowMs: number): void {
|
||||
// The shared state database caches handles for the process lifetime. Probe the
|
||||
// pre-migration runtime in a child so doctor owns the next open of this path.
|
||||
const result = spawnSync(
|
||||
"tsx",
|
||||
[fileURLToPath(import.meta.url), "--verify-legacy-unread", String(nowMs)],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
timeout: 120_000,
|
||||
},
|
||||
);
|
||||
assert(
|
||||
result.status === 0,
|
||||
`legacy runtime probe failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyRuntimeIgnoresLegacyJson(nowMs: number): Promise<void> {
|
||||
const beforeDoctor = await listCommitments({ nowMs });
|
||||
assert(beforeDoctor.length === 0, "runtime imported legacy JSON without doctor");
|
||||
}
|
||||
|
||||
async function verifyDoctorImportAndRuntimeIsolation() {
|
||||
await withStateDir("commitments-doctor", async (stateDir) => {
|
||||
const nowMs = Date.parse("2026-04-29T17:00:00.000Z");
|
||||
const cfg = { commitments: { enabled: true } };
|
||||
const sourcePath = path.join(stateDir, "commitments", "commitments.json");
|
||||
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
@@ -140,13 +164,7 @@ async function verifyDoctorImportAndRuntimeIsolation() {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const beforeDoctor = await listDueCommitmentsForSession({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:qa-channel:commitments",
|
||||
nowMs,
|
||||
});
|
||||
assert(beforeDoctor.length === 0, "runtime imported legacy JSON without doctor");
|
||||
verifyRuntimeIgnoresLegacyJsonInChild(nowMs);
|
||||
await fs.access(sourcePath);
|
||||
|
||||
await runPackagedDoctor(stateDir);
|
||||
@@ -161,16 +179,11 @@ async function verifyDoctorImportAndRuntimeIsolation() {
|
||||
}
|
||||
});
|
||||
|
||||
const due = await listDueCommitmentsForSession({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:qa-channel:commitments",
|
||||
nowMs,
|
||||
});
|
||||
assert(due.length === 1, `unexpected imported due count ${due.length}`);
|
||||
assert(!("sourceUserText" in due[0]), "legacy source user text surfaced after import");
|
||||
const imported = await listCommitments({ nowMs });
|
||||
assert(imported.length === 1, `unexpected imported commitment count ${imported.length}`);
|
||||
assert(!("sourceUserText" in imported[0]), "legacy source user text surfaced after import");
|
||||
assert(
|
||||
!("sourceAssistantText" in due[0]),
|
||||
!("sourceAssistantText" in imported[0]),
|
||||
"legacy source assistant text surfaced after import",
|
||||
);
|
||||
});
|
||||
@@ -223,7 +236,11 @@ async function verifyExpiryTransition() {
|
||||
});
|
||||
}
|
||||
|
||||
await verifyExtractionRemainsRetired();
|
||||
await verifyDoctorImportAndRuntimeIsolation();
|
||||
await verifyExpiryTransition();
|
||||
console.log("OK");
|
||||
if (process.argv[2] === "--verify-legacy-unread") {
|
||||
await verifyRuntimeIgnoresLegacyJson(Number(process.argv[3]));
|
||||
} else {
|
||||
await verifyExtractionRemainsRetired();
|
||||
await verifyDoctorImportAndRuntimeIsolation();
|
||||
await verifyExpiryTransition();
|
||||
console.log("OK");
|
||||
}
|
||||
|
||||
@@ -50,10 +50,8 @@ function messageText(content: unknown): string {
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function verifyRuntimeContextTranscriptShape(root: string) {
|
||||
const sessionFile = path.join(root, ".openclaw", "agents", "main", "sessions", "runtime.jsonl");
|
||||
await fs.mkdir(path.dirname(sessionFile), { recursive: true });
|
||||
const sessionManager = SessionManager.open(sessionFile);
|
||||
async function verifyRuntimeContextTranscriptShape() {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const effectivePrompt = [
|
||||
"visible ask",
|
||||
"",
|
||||
@@ -233,8 +231,7 @@ async function verifyDoctorRepair(root: string) {
|
||||
})) as TranscriptEntry[];
|
||||
const ids = entries.map((entryValue) => (entryValue as { id?: string }).id).filter(Boolean);
|
||||
assert(
|
||||
JSON.stringify(ids) ===
|
||||
JSON.stringify(["broken-session", "parent", "plain-user", "plain-assistant"]),
|
||||
JSON.stringify(ids) === JSON.stringify(["broken", "parent", "plain-user", "plain-assistant"]),
|
||||
`doctor kept wrong active branch: ${JSON.stringify(ids)}`,
|
||||
);
|
||||
assert(
|
||||
@@ -256,7 +253,7 @@ async function main() {
|
||||
setEnvValue("OPENCLAW_STATE_DIR", stateDir);
|
||||
setEnvValue("OPENCLAW_CONFIG_PATH", path.join(stateDir, "openclaw.json"));
|
||||
try {
|
||||
await verifyRuntimeContextTranscriptShape(root);
|
||||
await verifyRuntimeContextTranscriptShape();
|
||||
await verifyDoctorRepair(root);
|
||||
console.log("session runtime context Docker E2E passed");
|
||||
} finally {
|
||||
|
||||
@@ -26,7 +26,7 @@ file_list_is_docsish_only() {
|
||||
if ! path_is_docsish "$path"; then
|
||||
return 1
|
||||
fi
|
||||
done <<<"$files"
|
||||
done < <(printf '%s\n' "$files")
|
||||
|
||||
[ "$saw_any" = "true" ]
|
||||
}
|
||||
|
||||
@@ -298,40 +298,47 @@ describe("Engine contract tests", () => {
|
||||
});
|
||||
|
||||
it("delegateCompactionToRuntime returns successor sessionTarget without sessionFile", async () => {
|
||||
compactEmbeddedAgentSessionDirectMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
reason: undefined,
|
||||
result: {
|
||||
summary: "summary",
|
||||
firstKeptEntryId: "entry-1",
|
||||
tokensBefore: 100,
|
||||
tokensAfter: 40,
|
||||
details: undefined,
|
||||
sessionId: "s3-successor",
|
||||
sessionFile: "sqlite:main:s3-successor:/tmp/openclaw-agent.sqlite",
|
||||
},
|
||||
});
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "context-successor-target-"));
|
||||
const storePath = path.join(root, "openclaw-agent.sqlite");
|
||||
try {
|
||||
compactEmbeddedAgentSessionDirectMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
reason: undefined,
|
||||
result: {
|
||||
summary: "summary",
|
||||
firstKeptEntryId: "entry-1",
|
||||
tokensBefore: 100,
|
||||
tokensAfter: 40,
|
||||
details: undefined,
|
||||
sessionId: "s3-successor",
|
||||
sessionFile: `sqlite:main:s3-successor:${storePath}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await delegateCompactionToRuntime({
|
||||
sessionId: "s3",
|
||||
sessionKey: "agent:main:s3",
|
||||
tokenBudget: 4096,
|
||||
runtimeContext: {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.result).toMatchObject({
|
||||
sessionId: "s3-successor",
|
||||
sessionTarget: {
|
||||
agentId: "main",
|
||||
sessionId: "s3-successor",
|
||||
const result = await delegateCompactionToRuntime({
|
||||
sessionId: "s3",
|
||||
sessionKey: "agent:main:s3",
|
||||
storePath: "/tmp/openclaw-agent.sqlite",
|
||||
},
|
||||
});
|
||||
expect(result.result).not.toHaveProperty("sessionFile");
|
||||
tokenBudget: 4096,
|
||||
runtimeContext: {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.result).toMatchObject({
|
||||
sessionId: "s3-successor",
|
||||
sessionTarget: {
|
||||
agentId: "main",
|
||||
sessionId: "s3-successor",
|
||||
sessionKey: "agent:main:s3",
|
||||
storePath,
|
||||
},
|
||||
});
|
||||
expect(result.result).not.toHaveProperty("sessionFile");
|
||||
} finally {
|
||||
closeOpenClawAgentDatabasesForTest();
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("allows the caller key to rebind to a legacy successor session", async () => {
|
||||
|
||||
@@ -243,7 +243,7 @@ async function main() {
|
||||
arguments: {
|
||||
session_key: "agent:main:main",
|
||||
after_cursor: 0,
|
||||
timeout_ms: 10_000,
|
||||
timeout_ms: 120_000,
|
||||
},
|
||||
}),
|
||||
gateway.request<{ runId?: string; status?: string }>("chat.send", {
|
||||
@@ -284,11 +284,15 @@ async function main() {
|
||||
);
|
||||
|
||||
const channelMessage = `hello from docker ${randomUUID()}`;
|
||||
await gateway.request("chat.send", {
|
||||
const channelRun = await gateway.request<{ runId?: string; status?: string }>("chat.send", {
|
||||
sessionKey: "agent:main:main",
|
||||
message: channelMessage,
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
assert(
|
||||
channelRun.status === "started" && typeof channelRun.runId === "string",
|
||||
`channel chat.send did not start: ${JSON.stringify(channelRun)}`,
|
||||
);
|
||||
const rawGatewayUserMessage = await waitFor(
|
||||
"raw gateway user session.message",
|
||||
() =>
|
||||
@@ -370,6 +374,15 @@ async function main() {
|
||||
);
|
||||
}
|
||||
assert(helpNotification.content === channelMessage, "expected Claude channel content");
|
||||
const channelRunResult = await gateway.request<{ status?: string }>(
|
||||
"agent.wait",
|
||||
{ runId: channelRun.runId, timeoutMs: 240_000 },
|
||||
{ timeoutMs: 245_000 },
|
||||
);
|
||||
assert(
|
||||
channelRunResult.status === "ok",
|
||||
`agent.wait failed for ${channelRun.runId}: ${JSON.stringify(channelRunResult)}`,
|
||||
);
|
||||
|
||||
await mcp.notification({
|
||||
method: "notifications/claude/channel/permission_request",
|
||||
|
||||
@@ -11,8 +11,8 @@ describe("Docker E2E client scripts", () => {
|
||||
const source = readScript("scripts/e2e/commitments-safety-docker-client.ts");
|
||||
|
||||
expect(source).toContain("../../dist/commitments/runtime.js");
|
||||
expect(source).toContain("../../dist/commitments/runtime.test-support.js");
|
||||
expect(source).toContain("../../dist/commitments/store.js");
|
||||
expect(source).toContain("resetCommitmentExtractionRuntimeForTests");
|
||||
expect(source).toContain("verifyExtractionRemainsRetired()");
|
||||
expect(source).toContain("verifyDoctorImportAndRuntimeIsolation()");
|
||||
expect(source).toContain("verifyExpiryTransition()");
|
||||
@@ -27,7 +27,8 @@ describe("Docker E2E client scripts", () => {
|
||||
expect(source).toContain(
|
||||
"../../dist/agents/embedded-agent-runner/run/runtime-context-prompt.js",
|
||||
);
|
||||
expect(source).toContain("verifyRuntimeContextTranscriptShape(root)");
|
||||
expect(source).toContain("SessionManager.inMemory()");
|
||||
expect(source).toContain("verifyRuntimeContextTranscriptShape()");
|
||||
expect(source).toContain("verifyDoctorRepair(root)");
|
||||
expect(source).toContain("<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>");
|
||||
expect(source).toContain("openclaw.runtime-context");
|
||||
|
||||
@@ -245,7 +245,7 @@ function runNpmTelegramInputValidation(overrides: Record<string, string>) {
|
||||
function runNpmTelegramArtifactValidation(params: {
|
||||
currentRunId: string;
|
||||
producerRunId: string;
|
||||
producerStatus: "completed" | "in_progress" | "pending" | "queued";
|
||||
producerStatus: "completed" | "in_progress" | "pending" | "queued" | "requested" | "waiting";
|
||||
producerConclusion: "success" | null;
|
||||
}) {
|
||||
const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e");
|
||||
@@ -3251,8 +3251,7 @@ describe("package artifact reuse", () => {
|
||||
'--arg digest "sha256:${ARTIFACT_DIGEST}"',
|
||||
"actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}",
|
||||
'if [[ "$ARTIFACT_RUN_ID" == "$GITHUB_RUN_ID" ]]',
|
||||
'.status == "pending"',
|
||||
'.status == "queued" or .status == "in_progress"',
|
||||
'.status == "pending" or .status == "queued" or .status == "requested" or .status == "waiting" or .status == "in_progress"',
|
||||
".conclusion == null",
|
||||
"Package Telegram artifact predates the active producer run attempt.",
|
||||
'.status == "completed"',
|
||||
@@ -3309,7 +3308,7 @@ describe("package artifact reuse", () => {
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
});
|
||||
|
||||
it("accepts active artifacts while GitHub reports the workflow as pending", () => {
|
||||
it("accepts active artifacts while GitHub still reports the workflow as pending", () => {
|
||||
const result = runNpmTelegramArtifactValidation({
|
||||
currentRunId: "123",
|
||||
producerConclusion: null,
|
||||
|
||||
Reference in New Issue
Block a user