mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 17:51:12 +00:00
* fix(crestodian): keep onboarding RPCs restart-safe * fix(profiles): isolate approval state migrations * fix(crestodian): bypass configured gateway setup * test(crestodian): type onboarding mocks * fix(onboarding): require inference before Crestodian * fix(onboarding): enforce verified inference handoff * fix(macos): reset setup on gateway endpoint edits * chore(i18n): refresh native source inventory * fix(gateway): keep socket on request cancellation * test(packaging): require workspace templates * fix(onboarding): bind setup to verified inference * fix(onboarding): align inference gate contracts * fix(crestodian): classify concurrent policy rejection * test(crestodian): expect registry restoration * fix(onboarding): bind setup to configured gateways * fix(codex): preserve startup phase deadlines * test(crestodian): match fail-closed policy ordering * test(onboarding): assert bound gateway handoff * fix(codex): bind runtime resolution to spawn cwd * test(crestodian): assert policy rejection order * fix(cli): preserve gateway routing across restarts * fix(macos): fail closed during gateway edits * test(macos): cover gateway route generation races * chore: keep release notes out of onboarding PR * fix(ci): refresh onboarding generated checks * style(swift): align gateway channel formatting * fix(ci): refresh plugin SDK surface budgets * fix(ci): resync native string inventory * refactor(swift): split gateway channel support * test(doctor): isolate plugin compatibility registry * test(macos): isolate gateway onboarding fixtures * test(macos): assert gateway lease health ordering * fix(codex): reconcile computer-use startup changes
140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { CodexAppServerRpcError, type CodexAppServerClient } from "./client.js";
|
|
import { resumeCodexAppServerThread } from "./thread-resume.js";
|
|
|
|
function resumeResponse(threadId: string, restoredTurns = 0) {
|
|
return {
|
|
thread: {
|
|
id: threadId,
|
|
sessionId: "session-1",
|
|
forkedFromId: null,
|
|
preview: "",
|
|
ephemeral: false,
|
|
modelProvider: "openai",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
status: { type: "idle" },
|
|
path: null,
|
|
cwd: "/repo",
|
|
cliVersion: "0.139.0",
|
|
source: "unknown",
|
|
agentNickname: null,
|
|
agentRole: null,
|
|
gitInfo: null,
|
|
name: null,
|
|
turns: Array.from({ length: restoredTurns }, (_, index) => ({
|
|
id: `turn-${index}`,
|
|
items: [],
|
|
status: "completed",
|
|
error: null,
|
|
})),
|
|
},
|
|
model: "gpt-5.5-codex",
|
|
modelProvider: "openai",
|
|
serviceTier: null,
|
|
cwd: "/repo",
|
|
instructionSources: [],
|
|
approvalPolicy: "never",
|
|
approvalsReviewer: "user",
|
|
sandbox: { type: "dangerFullAccess" },
|
|
permissionProfile: null,
|
|
reasoningEffort: null,
|
|
};
|
|
}
|
|
|
|
function createClient(requestImpl: (params: unknown) => unknown) {
|
|
const request = vi.fn(async (_method: string, params: unknown) => await requestImpl(params));
|
|
const client = { request } as unknown as CodexAppServerClient;
|
|
return {
|
|
client,
|
|
request,
|
|
};
|
|
}
|
|
|
|
describe("resumeCodexAppServerThread", () => {
|
|
it("resumes the requested thread and keeps the client leased", async () => {
|
|
const { client, request } = createClient(async () => resumeResponse("thread-1", 2));
|
|
const abandonClient = vi.fn(async () => undefined);
|
|
|
|
const response = await resumeCodexAppServerThread({
|
|
client,
|
|
abandonClient,
|
|
request: { threadId: "thread-1", excludeTurns: true },
|
|
});
|
|
|
|
expect(response.thread.id).toBe("thread-1");
|
|
expect(request).toHaveBeenCalledWith("thread/resume", expect.anything(), expect.anything());
|
|
expect(abandonClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("leaves a proven RPC rejection on the reusable client", async () => {
|
|
const rejection = new CodexAppServerRpcError(
|
|
{ code: -32_000, message: "thread not found" },
|
|
"thread/resume",
|
|
);
|
|
const { client } = createClient(async () => {
|
|
throw rejection;
|
|
});
|
|
const abandonClient = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
resumeCodexAppServerThread({
|
|
client,
|
|
abandonClient,
|
|
request: { threadId: "thread-1", excludeTurns: true },
|
|
}),
|
|
).rejects.toBe(rejection);
|
|
expect(abandonClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps the shared client after cancellation before the resume write", async () => {
|
|
const rejection = Object.assign(new Error("thread/resume aborted"), {
|
|
code: "CODEX_APP_SERVER_LOCAL_REQUEST_CANCELLED",
|
|
mayHaveWritten: false,
|
|
});
|
|
const { client } = createClient(async () => {
|
|
throw rejection;
|
|
});
|
|
const abandonClient = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
resumeCodexAppServerThread({
|
|
client,
|
|
abandonClient,
|
|
request: { threadId: "thread-1", excludeTurns: true },
|
|
}),
|
|
).rejects.toBe(rejection);
|
|
expect(abandonClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retires the exact client when resume acceptance is indeterminate", async () => {
|
|
const { client } = createClient(async () => {
|
|
throw new Error("thread/resume timed out");
|
|
});
|
|
const abandonClient = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
resumeCodexAppServerThread({
|
|
client,
|
|
abandonClient,
|
|
request: { threadId: "thread-1", excludeTurns: true },
|
|
}),
|
|
).rejects.toThrow("thread/resume timed out");
|
|
expect(abandonClient).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("retires the exact client when the response names another thread", async () => {
|
|
const { client } = createClient(async () => resumeResponse("thread-2"));
|
|
const abandonClient = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
resumeCodexAppServerThread({
|
|
client,
|
|
abandonClient,
|
|
request: { threadId: "thread-1", excludeTurns: true },
|
|
}),
|
|
).rejects.toThrow("returned thread-2 for thread-1");
|
|
expect(abandonClient).toHaveBeenCalledOnce();
|
|
});
|
|
});
|