





















1+import { describe, expect, it, vi } from "vitest";
2+import {
3+foldPostCoreFinalizeIntoResult,
4+type PostCoreFinalizeSpawner,
5+runPostCoreFinalizeAfterGatewayUpdate,
6+} from "./update-post-core-finalize.js";
7+import type { UpdateRunResult } from "./update-runner.js";
8+9+function gitOkResult(overrides: Partial<UpdateRunResult> = {}): UpdateRunResult {
10+return {
11+status: "ok",
12+mode: "git",
13+root: "/srv/openclaw",
14+before: { sha: "aaa", version: "2026.5.3" },
15+after: { sha: "bbb", version: "2026.6.1" },
16+steps: [],
17+durationMs: 10,
18+ ...overrides,
19+};
20+}
21+22+const ENTRYPOINT = "/srv/openclaw/dist/index.mjs";
23+const resolveEntrypointOk = async () => ENTRYPOINT;
24+25+describe("runPostCoreFinalizeAfterGatewayUpdate", () => {
26+it("skips non-git update modes", async () => {
27+const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>();
28+for (const result of [
29+gitOkResult({ mode: "pnpm" }),
30+gitOkResult({ status: "error" }),
31+gitOkResult({ status: "skipped" }),
32+gitOkResult({ root: undefined }),
33+]) {
34+const outcome = await runPostCoreFinalizeAfterGatewayUpdate({
35+ result,
36+resolveEntrypoint: resolveEntrypointOk,
37+ spawnFinalize,
38+});
39+expect(outcome).toEqual({ status: "skipped", reason: "not-git-update" });
40+}
41+expect(spawnFinalize).not.toHaveBeenCalled();
42+});
43+44+it("skips when no built entrypoint is found", async () => {
45+const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>();
46+const outcome = await runPostCoreFinalizeAfterGatewayUpdate({
47+result: gitOkResult(),
48+resolveEntrypoint: async () => undefined,
49+ spawnFinalize,
50+});
51+expect(outcome).toEqual({ status: "skipped", reason: "entrypoint-missing" });
52+expect(spawnFinalize).not.toHaveBeenCalled();
53+});
54+55+it("spawns `update finalize` against the rebuilt binary and reports success", async () => {
56+const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>(async () => ({ code: 0 }));
57+const outcome = await runPostCoreFinalizeAfterGatewayUpdate({
58+result: gitOkResult(),
59+channel: "stable",
60+timeoutMs: 120_000,
61+resolveEntrypoint: resolveEntrypointOk,
62+ spawnFinalize,
63+});
64+expect(outcome).toEqual({ status: "ok", entrypoint: ENTRYPOINT });
65+expect(spawnFinalize).toHaveBeenCalledTimes(1);
66+const call = spawnFinalize.mock.calls[0]![0];
67+// Reconcile runs through the designed finalizer; never restarts (RPC owns restart).
68+expect(call.argv).toEqual([
69+expect.any(String),
70+ENTRYPOINT,
71+"update",
72+"finalize",
73+"--json",
74+"--yes",
75+"--no-restart",
76+"--channel",
77+"stable",
78+"--timeout",
79+"120",
80+]);
81+// Host-compat resolution is pinned to the just-installed core version.
82+expect(call.env.OPENCLAW_COMPATIBILITY_HOST_VERSION).toBe("2026.6.1");
83+});
84+85+it("omits channel/timeout flags when not provided", async () => {
86+const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>(async () => ({ code: 0 }));
87+await runPostCoreFinalizeAfterGatewayUpdate({
88+result: gitOkResult(),
89+resolveEntrypoint: resolveEntrypointOk,
90+ spawnFinalize,
91+});
92+const argv = spawnFinalize.mock.calls[0]![0].argv;
93+expect(argv).not.toContain("--channel");
94+expect(argv).not.toContain("--timeout");
95+});
96+97+it("reports error on a non-zero finalize exit", async () => {
98+const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>(async () => ({
99+code: 1,
100+stderr: "convergence failed",
101+}));
102+const outcome = await runPostCoreFinalizeAfterGatewayUpdate({
103+result: gitOkResult(),
104+resolveEntrypoint: resolveEntrypointOk,
105+ spawnFinalize,
106+});
107+expect(outcome).toEqual({
108+status: "error",
109+reason: "nonzero-exit",
110+entrypoint: ENTRYPOINT,
111+exitCode: 1,
112+message: "convergence failed",
113+});
114+});
115+116+it("reports error when the finalize spawn throws", async () => {
117+const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>(async () => {
118+throw new Error("ENOENT");
119+});
120+const outcome = await runPostCoreFinalizeAfterGatewayUpdate({
121+result: gitOkResult(),
122+resolveEntrypoint: resolveEntrypointOk,
123+ spawnFinalize,
124+});
125+expect(outcome).toEqual({
126+status: "error",
127+reason: "spawn-failed",
128+entrypoint: ENTRYPOINT,
129+message: "ENOENT",
130+});
131+});
132+});
133+134+describe("foldPostCoreFinalizeIntoResult", () => {
135+it("leaves the result unchanged for ok/skipped outcomes", () => {
136+const result = gitOkResult();
137+expect(foldPostCoreFinalizeIntoResult(result, { status: "ok", entrypoint: ENTRYPOINT })).toBe(
138+result,
139+);
140+expect(
141+foldPostCoreFinalizeIntoResult(result, { status: "skipped", reason: "not-git-update" }),
142+).toBe(result);
143+});
144+145+it("flips status to error so the RPC restart gate is skipped", () => {
146+const result = gitOkResult();
147+const folded = foldPostCoreFinalizeIntoResult(result, {
148+status: "error",
149+reason: "nonzero-exit",
150+entrypoint: ENTRYPOINT,
151+exitCode: 2,
152+message: "boom",
153+});
154+expect(folded.status).toBe("error");
155+expect(folded.reason).toBe("post-core-plugin-finalize-failed");
156+expect(folded.steps.at(-1)).toMatchObject({
157+name: "post-core plugin finalize",
158+exitCode: 2,
159+stderrTail: "boom",
160+});
161+// Core update metadata is preserved for the sentinel.
162+expect(folded.after).toEqual(result.after);
163+});
164+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。