mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(codex): retain completed command output with Codex 0.158.0 (#160487)
This commit is contained in:
parent
96fdbb11b5
commit
9190ad7c12
23 changed files with 280 additions and 112 deletions
|
|
@ -333,7 +333,7 @@ or legacy MCP/tool restrictions prevent automatic replacement and cache refresh.
|
|||
## Remote marketplaces
|
||||
|
||||
Remote marketplace support was introduced in Codex 0.146.1 and remains
|
||||
available in OpenClaw's pinned Codex 0.155.1. OpenClaw passes the opaque remote
|
||||
available in OpenClaw's pinned Codex 0.158.0. OpenClaw passes the opaque remote
|
||||
plugin ID returned by Codex to `plugin/read` and `plugin/install`; a
|
||||
human-readable plugin name is not a valid substitute.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ How OpenClaw starts and reaches the Codex app-server, and every `appServer` fiel
|
|||
## App-server transport
|
||||
|
||||
For ordinary harness turns, OpenClaw starts the managed Codex binary shipped
|
||||
with the official plugin (currently `@openai/codex` `0.155.1`):
|
||||
with the official plugin (currently `@openai/codex` `0.158.0`):
|
||||
|
||||
```bash
|
||||
codex app-server --listen stdio://
|
||||
|
|
@ -197,7 +197,7 @@ If the normal app-server runtime would be `danger-full-access`, enabling
|
|||
permission profile instead. Codex-managed network enforcement is sandboxed
|
||||
networking, so a full-access profile would not protect outbound traffic.
|
||||
|
||||
The plugin manages stable Codex app-server `0.155.1`. Explicit custom
|
||||
The plugin manages stable Codex app-server `0.158.0`. Explicit custom
|
||||
executables, remote app-servers, and macOS desktop binaries must report a
|
||||
parseable semantic version of `0.149.0` or newer. Older, malformed, and
|
||||
unversioned handshakes are rejected. Newer versions log a compatibility warning
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ The stable default is fail-closed: active OpenClaw sandboxing disables native
|
|||
Codex execution surfaces that would otherwise run from the Codex app-server
|
||||
host. Use `appServer.experimental.sandboxExecServer: true` only when you want
|
||||
to try Codex's remote environment support with OpenClaw's sandbox backend.
|
||||
This preview path uses the pinned Codex `0.155.1` app-server.
|
||||
This preview path uses the pinned Codex `0.158.0` app-server.
|
||||
|
||||
```json5
|
||||
{
|
||||
|
|
|
|||
|
|
@ -77,25 +77,23 @@ response remains authoritative even if it contains no visible models; HTTP
|
|||
`401` and `403` return an empty catalog rather than exposing fallback models.
|
||||
|
||||
<Note>
|
||||
The current bundled harness is `@openai/codex` `0.155.1`. A live `model/list`
|
||||
probe against that app-server, using an isolated Codex home authenticated with
|
||||
a ChatGPT account, returned this public subset of catalog metadata:
|
||||
The current bundled harness is `@openai/codex` `0.158.0`. A live `model/list`
|
||||
probe against that app-server, authenticated with a ChatGPT account, returned
|
||||
this public subset of catalog metadata on September 28, 2026:
|
||||
|
||||
| Model id | Input modalities | Reasoning efforts | Default effort |
|
||||
| --------------- | ---------------- | ------------------------------------ | -------------- |
|
||||
| `gpt-6-astra` | text, image | low, medium, high, xhigh, max, ultra | medium |
|
||||
| `gpt-6-astra` | text, image | low, medium, high, xhigh, max, ultra | low |
|
||||
| `gpt-6-sol` | text, image | low, medium, high, xhigh, max, ultra | medium |
|
||||
| `gpt-6-luna` | text, image | low, medium, high, xhigh, max | medium |
|
||||
| `gpt-5.6-luna` | text, image | low, medium, high, xhigh, max | medium |
|
||||
| `gpt-5.6-sol` | text, image | low, medium, high, xhigh, max, ultra | low |
|
||||
| `gpt-5.6-sol` | text, image | low, medium, high, xhigh, max, ultra | medium |
|
||||
| `gpt-5.6-terra` | text, image | low, medium, high, xhigh, max, ultra | medium |
|
||||
|
||||
The check reused the same isolated home and catalog cache from `0.154.0`,
|
||||
without clearing the cache. The earlier app-server omitted GPT-6 Sol and Luna;
|
||||
`0.155.1` listed both for the same account. This snapshot does not establish
|
||||
access for other accounts. Available model IDs, input modalities, and reasoning
|
||||
efforts remain account-scoped. Run `/codex models` after starting or upgrading
|
||||
the gateway to inspect the actual public picker for your account.
|
||||
This snapshot does not establish access for other accounts or attribute catalog
|
||||
changes to the app-server version. Available model IDs, input modalities, and
|
||||
reasoning efforts remain account-scoped. Run `/codex models` after starting or
|
||||
upgrading the gateway to inspect the actual public picker for your account.
|
||||
|
||||
OpenClaw reasoning controls preserve supported native levels, including `ultra`.
|
||||
Codex owns Ultra's proactive delegation and model-specific inference effort;
|
||||
|
|
|
|||
|
|
@ -536,8 +536,8 @@ same child result after the parent replies.
|
|||
|
||||
- The official `@openclaw/codex` plugin installed. Include `codex` in
|
||||
`plugins.allow` if your config uses an allowlist.
|
||||
- Managed Codex app-server `0.155.1`. The plugin ships and manages
|
||||
`@openai/codex` `0.155.1` by default, so a `codex` command on `PATH` does not
|
||||
- Managed Codex app-server `0.158.0`. The plugin ships and manages
|
||||
`@openai/codex` `0.158.0` by default, so a `codex` command on `PATH` does not
|
||||
affect normal startup. Explicit custom, remote, and macOS desktop-owned
|
||||
app-servers must report a parseable semantic version of `0.149.0` or newer.
|
||||
Newer versions continue with a compatibility warning and normal runtime
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ working.
|
|||
- `plugins.entries.codex.enabled` is `true`.
|
||||
- `plugins.entries.codex.config.codexPlugins.enabled` is `true`.
|
||||
- Codex app-server reports `0.149.0` or newer. The official plugin ships
|
||||
`@openai/codex` `0.155.1`; newer custom, remote, and macOS desktop-owned
|
||||
`@openai/codex` `0.158.0`; newer custom, remote, and macOS desktop-owned
|
||||
binaries continue with a compatibility warning and normal runtime validation.
|
||||
- The target Codex app-server can see the expected marketplace, plugin, and
|
||||
app inventory.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@openai/codex": "0.155.1",
|
||||
"@openai/codex": "0.158.0",
|
||||
"@openclaw/fs-safe": "0.21.1",
|
||||
"semver": "7.8.5",
|
||||
"smol-toml": "1.8.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/** Exact Codex app-server version shipped by the OpenClaw Codex bridge. */
|
||||
export const CODEX_APP_SERVER_VERSION = "0.155.1";
|
||||
export const CODEX_APP_SERVER_VERSION = "0.158.0";
|
||||
/** Inclusive runtime compatibility floor for external app-server binaries. */
|
||||
export const MIN_SUPPORTED_CODEX_APP_SERVER_VERSION = "0.149.0";
|
||||
export const MANAGED_CODEX_APP_SERVER_PACKAGE = "@openai/codex";
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ function classifyOpenAiFailoverCode(code: string | undefined) {
|
|||
const OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
|
||||
// Keep synchronized with extensions/codex's exact @openai/codex dependency;
|
||||
// the provider contract test fails when that managed-runtime pin changes.
|
||||
const OPENAI_CODEX_CLIENT_VERSION = "0.155.1";
|
||||
const OPENAI_CODEX_CLIENT_VERSION = "0.158.0";
|
||||
const OPENAI_CODEX_MODELS_ENDPOINT = `${OPENAI_CODEX_RESPONSES_BASE_URL}/models?client_version=${OPENAI_CODEX_CLIENT_VERSION}`;
|
||||
const OPENAI_MODELS_CACHE_TTL_MS = 60_000;
|
||||
const OPENAI_CODEX_MODELS_CACHE_TTL_MS = 60_000;
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ export default {
|
|||
).stdout.trim();
|
||||
const current = await readProcess(row.pid);
|
||||
expect(current?.startTimeTicks).toBe(row.startTimeTicks);
|
||||
expect(version).toBe("codex-cli 0.155.1");
|
||||
expect(version).toBe("codex-cli 0.158.0");
|
||||
binaryIdentities.push({
|
||||
pid: row.pid,
|
||||
startTimeTicks: row.startTimeTicks,
|
||||
|
|
|
|||
62
pnpm-lock.yaml
generated
62
pnpm-lock.yaml
generated
|
|
@ -169,7 +169,7 @@ overrides:
|
|||
chevrotain>lodash-es: 4.18.1
|
||||
'@chevrotain/cst-dts-gen>lodash-es': 4.18.1
|
||||
'@chevrotain/gast>lodash-es': 4.18.1
|
||||
'@agentclientprotocol/codex-acp@1.12.0>@openai/codex': 0.155.1
|
||||
'@agentclientprotocol/codex-acp@1.12.0>@openai/codex': 0.158.0
|
||||
'@codemirror/commands@6.11.1>@codemirror/view': 6.43.12
|
||||
'@anthropic-ai/sdk': 0.127.0
|
||||
'@anthropic-ai/vertex-sdk@0.19.10>google-auth-library': 11.1.0
|
||||
|
|
@ -925,8 +925,8 @@ importers:
|
|||
extensions/codex:
|
||||
dependencies:
|
||||
'@openai/codex':
|
||||
specifier: 0.155.1
|
||||
version: 0.155.1
|
||||
specifier: 0.158.0
|
||||
version: 0.158.0
|
||||
'@openclaw/fs-safe':
|
||||
specifier: 0.21.1
|
||||
version: 0.21.1
|
||||
|
|
@ -4562,43 +4562,43 @@ packages:
|
|||
resolution: {integrity: sha512-3zcN5Q3yEmeyxXBzqB6fXPQFzYa2ROsGFSr69W0ArXIAGJqxl/aFECOVPD2kbkYPm0U/EHxFKgclK3UA9WQg5A==}
|
||||
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
|
||||
|
||||
'@openai/codex@0.155.1':
|
||||
resolution: {integrity: sha512-02fAAGyBtlA1zPjEo3kTj/bOSYbPz5DvjLwRZJdV7weFFEDzNFOMjQGmZ/+5CuirYV0hE+AZTrnjzwXYU4AdAQ==}
|
||||
'@openai/codex@0.158.0':
|
||||
resolution: {integrity: sha512-GBhcKpQmVLsCtEP5mUf6WFye6QQgTKotwsXrYPM0GFmEsoOSahik6hkf2FHabX9kZBl0Y0/PQowLu7uYMKT8dg==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
'@openai/codex@0.155.1-darwin-arm64':
|
||||
resolution: {integrity: sha512-cYxzGcRRoBrncyHlR8ed4yXwcoVJZC1pipGULSyJkGFKXJw/Uu57BklvzayuAptjJIipamnOk32CfUkk1F0bLw==}
|
||||
'@openai/codex@0.158.0-darwin-arm64':
|
||||
resolution: {integrity: sha512-0OKSjlWY1j4Ld1fT87QttNw3Y2SthcXi4GcrWSHjleZg1n86eG3+shJl4Pv+siUmsBJhWlNmm6rRO4Yv8ZyQLg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.155.1-darwin-x64':
|
||||
resolution: {integrity: sha512-FDpc+PdELYlyDnhd76Ckm6jNLF+1n3x34Ygd4QLQger810Vkxx/InQ5LY5jwkecJYKcbvyhMmuxTspaj1dLZrA==}
|
||||
'@openai/codex@0.158.0-darwin-x64':
|
||||
resolution: {integrity: sha512-FrX1o3APrL7F6QkO8z08Rq8lJitH2sNI7pkebA02eYA103hDs3fyy9d32zeLLQJUeAhmZA4pV9xJR4m7cVyNpQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.155.1-linux-arm64':
|
||||
resolution: {integrity: sha512-X3fRXm2orhJ3KeB8LgKym4XDUiQaqaOGuaa181bcHTsQI7C8m6tcQQbQsKDzT/2IibikzgYL82jvsgMbq43jww==}
|
||||
'@openai/codex@0.158.0-linux-arm64':
|
||||
resolution: {integrity: sha512-T9AgGcoU7HNsxJ4prT/R9YvztyEmz9kWP/VlT2cbCop4PVwiqfG9YMJtLo4j2ScewvSaTl4sQFwla+fwGpoRZg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.155.1-linux-x64':
|
||||
resolution: {integrity: sha512-atv3HF0mubqB0J/XkQ2JopqKzXJ+/7aQtTB2MkJ9MrraujMIz8zbCCLylLkN3PzpVGTJzzQFN/wD1oq8oJPJKg==}
|
||||
'@openai/codex@0.158.0-linux-x64':
|
||||
resolution: {integrity: sha512-mY12GZPM8TuOWVGxCyNV2NSA8t1uIupm9Nhu9VeQVEvvxBKN08U8bLH+vn7oISUHZPC8bwhROIbo/ZUxqV6XMg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.155.1-win32-arm64':
|
||||
resolution: {integrity: sha512-k5x8VO1aF8Xx/nuh1P31TeBgs11WA6i2GJiHqx5YCndFbWzOzUz6aeBaq9+PO6Qfe/Ivennh3I1FKJBU6Q8mpg==}
|
||||
'@openai/codex@0.158.0-win32-arm64':
|
||||
resolution: {integrity: sha512-Jw1u0q0+5PG97jPkINxE3UCFtsYBN8Af+IjjM0zlCO675Sv5lNsXU2K9aIaXwVeQIKw8q2lbPAR4SEkq2rSOoA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@openai/codex@0.155.1-win32-x64':
|
||||
resolution: {integrity: sha512-MO+cCZrgU0Ec7lJP/5NsTe5obJ9/qtRMkQUK0jYWTY1omxLA3lp5IOD2IAmsejlEJB931XRo51LZ7hl178CDjA==}
|
||||
'@openai/codex@0.158.0-win32-x64':
|
||||
resolution: {integrity: sha512-IaUmY11Zdqa/Zok6kE0Z5375pXtClKRbS8P1Gh2C73Aq65CaGn9N8gT6BXGC3+XD6yamAIiEQW5xM842UCCGow==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
|
@ -10318,7 +10318,7 @@ snapshots:
|
|||
'@agentclientprotocol/codex-acp@1.12.0':
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 1.4.0(zod@4.6.5)
|
||||
'@openai/codex': 0.155.1
|
||||
'@openai/codex': 0.158.0
|
||||
diff: 9.0.0
|
||||
open: 11.0.4
|
||||
vscode-jsonrpc: 9.0.2
|
||||
|
|
@ -12249,31 +12249,31 @@ snapshots:
|
|||
|
||||
'@npmcli/redact@5.0.0': {}
|
||||
|
||||
'@openai/codex@0.155.1':
|
||||
'@openai/codex@0.158.0':
|
||||
optionalDependencies:
|
||||
'@openai/codex-darwin-arm64': '@openai/codex@0.155.1-darwin-arm64'
|
||||
'@openai/codex-darwin-x64': '@openai/codex@0.155.1-darwin-x64'
|
||||
'@openai/codex-linux-arm64': '@openai/codex@0.155.1-linux-arm64'
|
||||
'@openai/codex-linux-x64': '@openai/codex@0.155.1-linux-x64'
|
||||
'@openai/codex-win32-arm64': '@openai/codex@0.155.1-win32-arm64'
|
||||
'@openai/codex-win32-x64': '@openai/codex@0.155.1-win32-x64'
|
||||
'@openai/codex-darwin-arm64': '@openai/codex@0.158.0-darwin-arm64'
|
||||
'@openai/codex-darwin-x64': '@openai/codex@0.158.0-darwin-x64'
|
||||
'@openai/codex-linux-arm64': '@openai/codex@0.158.0-linux-arm64'
|
||||
'@openai/codex-linux-x64': '@openai/codex@0.158.0-linux-x64'
|
||||
'@openai/codex-win32-arm64': '@openai/codex@0.158.0-win32-arm64'
|
||||
'@openai/codex-win32-x64': '@openai/codex@0.158.0-win32-x64'
|
||||
|
||||
'@openai/codex@0.155.1-darwin-arm64':
|
||||
'@openai/codex@0.158.0-darwin-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.155.1-darwin-x64':
|
||||
'@openai/codex@0.158.0-darwin-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.155.1-linux-arm64':
|
||||
'@openai/codex@0.158.0-linux-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.155.1-linux-x64':
|
||||
'@openai/codex@0.158.0-linux-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.155.1-win32-arm64':
|
||||
'@openai/codex@0.158.0-win32-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.155.1-win32-x64':
|
||||
'@openai/codex@0.158.0-win32-x64':
|
||||
optional: true
|
||||
|
||||
'@openclaw/crabline@0.1.27':
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ overrides:
|
|||
"chevrotain>lodash-es": 4.18.1
|
||||
"@chevrotain/cst-dts-gen>lodash-es": 4.18.1
|
||||
"@chevrotain/gast>lodash-es": 4.18.1
|
||||
"@agentclientprotocol/codex-acp@1.12.0>@openai/codex": 0.155.1
|
||||
"@agentclientprotocol/codex-acp@1.12.0>@openai/codex": 0.158.0
|
||||
"@codemirror/commands@6.11.1>@codemirror/view": 6.43.12
|
||||
"@anthropic-ai/sdk": 0.127.0
|
||||
"@anthropic-ai/vertex-sdk@0.19.10>google-auth-library": 11.1.0
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
runFakeCodexAppServer,
|
||||
} from "../codex-app-server-fixture.mjs";
|
||||
|
||||
const version = "0.155.1";
|
||||
const version = "0.158.0";
|
||||
const requestLog =
|
||||
process.env.OPENCLAW_CODEX_MEDIA_PATH_APP_SERVER_LOG ??
|
||||
"/tmp/openclaw-codex-media-path-app-server.jsonl";
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ describe("Gateway agent and artifact APIs", () => {
|
|||
} as never,
|
||||
});
|
||||
expect(
|
||||
attachManagedOutgoingMediaToMessage({ messageId, blocks: managedBlocks, stateDir }),
|
||||
await attachManagedOutgoingMediaToMessage({ messageId, blocks: managedBlocks, stateDir }),
|
||||
).toBe(true);
|
||||
|
||||
await disconnectGatewayClient(client);
|
||||
|
|
@ -484,10 +484,10 @@ describe("Gateway agent and artifact APIs", () => {
|
|||
await expect(
|
||||
client.request("artifacts.get", {
|
||||
sessionKey,
|
||||
agentId: "other",
|
||||
agentId: createdAgent.agentId,
|
||||
artifactId: artifact.id,
|
||||
}),
|
||||
).rejects.toThrow(/artifact not found/i);
|
||||
).rejects.toThrow('agent "artifact-agent" does not match session key agent "main"');
|
||||
|
||||
await expect(
|
||||
client.request("agents.delete", {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ async function connectWithFakeTime(params: {
|
|||
tickIntervalMs: number;
|
||||
}): Promise<{ client: GatewayClient; socket: WebSocket }> {
|
||||
const socketReady = createDeferred<WebSocket>();
|
||||
const helloReady = createDeferred<void>();
|
||||
const helloReady = createDeferred();
|
||||
const url = await listen((socket, request) => {
|
||||
if (request.method === "connect") {
|
||||
sendHello(socket, request.id, params.tickIntervalMs);
|
||||
|
|
@ -142,6 +142,7 @@ afterEach(async () => {
|
|||
client.stop();
|
||||
}
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
if (server) {
|
||||
for (const socket of server.clients) {
|
||||
socket.terminate();
|
||||
|
|
@ -155,7 +156,7 @@ afterEach(async () => {
|
|||
|
||||
describe("GatewayClient transport defaults", () => {
|
||||
it("uses a 30 second default request timeout", async () => {
|
||||
const requestReady = createDeferred<void>();
|
||||
const requestReady = createDeferred();
|
||||
const { client } = await connectWithFakeTime({
|
||||
tickIntervalMs: 60_000,
|
||||
onRequest: (_socket, request) => {
|
||||
|
|
@ -229,7 +230,8 @@ describe("GatewayClient transport defaults", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("reconnects after 1/2/4 second delays capped at 30 seconds", async () => {
|
||||
it("jitters exponential reconnect delays within the 30 second cap", async () => {
|
||||
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
||||
const sockets: WebSocket[] = [];
|
||||
const closeEvents: Array<{ code: number; reason: string }> = [];
|
||||
const firstSocket = createDeferred<WebSocket>();
|
||||
|
|
@ -254,7 +256,8 @@ describe("GatewayClient transport defaults", () => {
|
|||
await flushSocketIo();
|
||||
initialSocket.close(1012, "retry");
|
||||
|
||||
const expectedDelays = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000, 30_000];
|
||||
// A midpoint draw observes jitter, including its shifted interval at the cap.
|
||||
const expectedDelays = [1_100, 2_200, 4_400, 8_800, 17_600, 27_500, 27_500];
|
||||
for (const [index, delayMs] of expectedDelays.entries()) {
|
||||
await waitForCondition(() => closeEvents.length >= index + 1, `close event ${index + 1}`);
|
||||
const connectionCount = sockets.length;
|
||||
|
|
|
|||
|
|
@ -300,9 +300,9 @@ async function readFailureEvidence(params: {
|
|||
return JSON.stringify({ stability, requests, gatewayLogs });
|
||||
}
|
||||
|
||||
describe("Gateway repeated-request provider timeout", () => {
|
||||
describe("Gateway repeated-request recovery", () => {
|
||||
it(
|
||||
"continues after a provider timeout and settles failure before draining one queued followup",
|
||||
"recovers semantic stagnation and settles the active run before draining one queued followup",
|
||||
{ timeout: 330_000 },
|
||||
async () => {
|
||||
gatewayOwner = createQaLiveLaneGateway();
|
||||
|
|
@ -391,14 +391,14 @@ describe("Gateway repeated-request provider timeout", () => {
|
|||
expect(queued).toMatchObject({ status: "started" });
|
||||
expect(typeof queued.runId).toBe("string");
|
||||
|
||||
// The provider deadline starts before model-call observation, so its diagnostic
|
||||
// duration can be shorter than timeoutSeconds. The failure kind owns timeout evidence.
|
||||
// Repeated-request recovery owns semantic stagnation independently of the
|
||||
// current provider request's deadline. Observe its settlement before queue drain.
|
||||
const events = await waitForStability(
|
||||
gateway,
|
||||
baselineSeq,
|
||||
(records) =>
|
||||
records.some(
|
||||
(event) => event.type === "model.call.error" && event.failureKind === "timeout",
|
||||
(event) => event.type === "session.recovery.completed" && event.outcome === "aborted",
|
||||
),
|
||||
250_000,
|
||||
);
|
||||
|
|
@ -410,15 +410,18 @@ describe("Gateway repeated-request provider timeout", () => {
|
|||
);
|
||||
const completed = events.filter((event) => event.type === "session.recovery.completed");
|
||||
|
||||
// The heartbeat can report the same stall again while the provider owns its
|
||||
// request deadline. Every report must still respect the no-progress bound.
|
||||
// Heartbeats may observe a stall more than once; recovery still owns one abort.
|
||||
expect(stalled.length).toBeGreaterThan(0);
|
||||
for (const event of stalled) {
|
||||
expect(event.ageMs).toEqual(expect.any(Number));
|
||||
expect(event.ageMs as number).toBeGreaterThanOrEqual(QA_RECOVERY_BOUND_MS);
|
||||
}
|
||||
expect(requested).toEqual([]);
|
||||
expect(completed).toEqual([]);
|
||||
expect(requested).toEqual([
|
||||
expect.objectContaining({ action: "abort", reason: RECOVERY_REASON }),
|
||||
]);
|
||||
expect(completed).toEqual([
|
||||
expect.objectContaining({ action: "abort_embedded_run", outcome: "aborted" }),
|
||||
]);
|
||||
expect(
|
||||
events.filter((event) => event.type === "model.call.started").length,
|
||||
).toBeGreaterThanOrEqual(5);
|
||||
|
|
@ -444,11 +447,22 @@ describe("Gateway repeated-request provider timeout", () => {
|
|||
},
|
||||
);
|
||||
expect(historyContainsQueuedReply(history)).toBe(true);
|
||||
const queuedTerminal = (await gateway.call(
|
||||
"agent.wait",
|
||||
{ runId: queued.runId, timeoutMs: 30_000 },
|
||||
{ timeoutMs: 35_000 },
|
||||
)) as GatewayChatRun;
|
||||
// agent.wait returns pending immediately while the queue owns this run,
|
||||
// even after its reply is visible. Observe settlement within the same budget.
|
||||
const queuedDeadline = Date.now() + 30_000;
|
||||
let queuedTerminal: GatewayChatRun;
|
||||
do {
|
||||
const remainingMs = Math.max(1, queuedDeadline - Date.now());
|
||||
queuedTerminal = (await gateway.call(
|
||||
"agent.wait",
|
||||
{ runId: queued.runId, timeoutMs: remainingMs },
|
||||
{ timeoutMs: remainingMs },
|
||||
)) as GatewayChatRun;
|
||||
if (queuedTerminal.status !== "pending") {
|
||||
break;
|
||||
}
|
||||
await sleep(Math.max(0, Math.min(100, queuedDeadline - Date.now())));
|
||||
} while (Date.now() < queuedDeadline);
|
||||
expect(queuedTerminal.status).toBe("ok");
|
||||
const mockBaseUrl = harness?.mock?.baseUrl;
|
||||
if (!mockBaseUrl) {
|
||||
|
|
@ -456,18 +470,26 @@ describe("Gateway repeated-request provider timeout", () => {
|
|||
}
|
||||
const requests = await readClassifiedMockRequests(mockBaseUrl);
|
||||
const recoveryRequests = requests.filter((request) => request.prompt === "recovery");
|
||||
expect(recoveryRequests.length).toBeGreaterThanOrEqual(5);
|
||||
expect(recoveryRequests[0]?.model).toBe("gpt-5.6-luna");
|
||||
expect(recoveryRequests[1]?.model).toBe(recoveryRequests[0]?.model);
|
||||
expect(recoveryRequests).toEqual(
|
||||
expect.arrayContaining([
|
||||
// Invisible replay-safe errors exhaust the primary model's retry budget;
|
||||
// the diagnostic owner then aborts the stalled fallback request.
|
||||
// The mock records its planned response before the delay; lifecycle checks
|
||||
// above prove the actual abort and completion.
|
||||
expect(recoveryRequests).toEqual([
|
||||
...Array.from({ length: 4 }, () =>
|
||||
expect.objectContaining({
|
||||
continuation: true,
|
||||
model: "gpt-5.6-luna",
|
||||
continuation: false,
|
||||
outcome: "error",
|
||||
errorCode: "response_failed_no_details",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
),
|
||||
expect.objectContaining({
|
||||
model: "gpt-5.6-luna-alt",
|
||||
continuation: false,
|
||||
outcome: "error",
|
||||
errorCode: "response_failed_no_details",
|
||||
}),
|
||||
]);
|
||||
expect(requests.filter((request) => request.prompt === "queued")).toEqual([
|
||||
expect.objectContaining({ outcome: "success" }),
|
||||
]);
|
||||
|
|
@ -475,10 +497,10 @@ describe("Gateway repeated-request provider timeout", () => {
|
|||
const finalEvents = (await readStability(gateway, baselineSeq)).events ?? [];
|
||||
expect(
|
||||
finalEvents.filter((event) => event.type === "session.recovery.requested"),
|
||||
).toHaveLength(0);
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
finalEvents.filter((event) => event.type === "session.recovery.completed"),
|
||||
).toHaveLength(0);
|
||||
).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
connectGatewayClient,
|
||||
disconnectGatewayClient,
|
||||
startGatewayWithClient,
|
||||
} from "../../../../src/gateway/test-helpers.e2e.js";
|
||||
import { installGatewayTestHooks } from "../../../../src/gateway/test-helpers.js";
|
||||
import {
|
||||
getGatewayTestPort,
|
||||
installGatewayTestHooks,
|
||||
startTestGatewayServer,
|
||||
} from "../../../../src/gateway/test-helpers.js";
|
||||
import { prepareDeviceAuthStore } from "../../../../src/infra/device-auth-store.js";
|
||||
import { loadOrCreateProcessDeviceIdentityAsync } from "../../../../src/infra/device-identity-async.js";
|
||||
import { withTimeout } from "../../../../src/infra/fs-safe.js";
|
||||
|
|
@ -13,7 +15,13 @@ import { withTimeout } from "../../../../src/infra/fs-safe.js";
|
|||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
const TOKEN = `rpc-identity-presence-${process.pid}`;
|
||||
let started: Awaited<ReturnType<typeof startGatewayWithClient>> | undefined;
|
||||
let started:
|
||||
| {
|
||||
port: number;
|
||||
server: Awaited<ReturnType<typeof startTestGatewayServer>>;
|
||||
client: Awaited<ReturnType<typeof connectGatewayClient>>;
|
||||
}
|
||||
| undefined;
|
||||
let observer: Awaited<ReturnType<typeof connectGatewayClient>> | undefined;
|
||||
let stateDir = "";
|
||||
type ObserverEvent = {
|
||||
|
|
@ -39,12 +47,23 @@ beforeAll(async () => {
|
|||
}
|
||||
await prepareDeviceAuthStore({});
|
||||
await loadOrCreateProcessDeviceIdentityAsync();
|
||||
started = await startGatewayWithClient({
|
||||
cfg: { gateway: { auth: { mode: "token", token: TOKEN } } },
|
||||
configPath: path.join(stateDir, "openclaw.json"),
|
||||
token: TOKEN,
|
||||
clientDisplayName: "rpc-identity-presence-bootstrap",
|
||||
// Suite hooks must retain this server's state through their per-test reset.
|
||||
const port = await getGatewayTestPort();
|
||||
const server = await startTestGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token: TOKEN },
|
||||
});
|
||||
try {
|
||||
const client = await connectGatewayClient({
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
token: TOKEN,
|
||||
clientDisplayName: "rpc-identity-presence-bootstrap",
|
||||
});
|
||||
started = { port, server, client };
|
||||
} catch (error) {
|
||||
await server.close();
|
||||
throw error;
|
||||
}
|
||||
observer = await connectGatewayClient({
|
||||
url: `ws://127.0.0.1:${started.port}`,
|
||||
token: TOKEN,
|
||||
|
|
|
|||
|
|
@ -452,7 +452,10 @@ describe("Gateway timeout recovery subagent delivery", () => {
|
|||
// successor can start. All barriers share the original completion budget.
|
||||
await expect.poll(readChildRuns, { timeout: remainingMs() }).toEqual([
|
||||
expect.objectContaining({
|
||||
execution: expect.objectContaining({ status: "terminal", outcome: { status: "ok" } }),
|
||||
execution: expect.objectContaining({
|
||||
status: "terminal",
|
||||
outcome: expect.objectContaining({ status: "ok" }),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
provider.releaseCompaction();
|
||||
|
|
@ -482,7 +485,10 @@ describe("Gateway timeout recovery subagent delivery", () => {
|
|||
// The terminal reply may precede the native outbox delivery commit.
|
||||
await expect.poll(readChildRuns, { timeout: 10_000 }).toEqual([
|
||||
expect.objectContaining({
|
||||
execution: expect.objectContaining({ status: "terminal", outcome: { status: "ok" } }),
|
||||
execution: expect.objectContaining({
|
||||
status: "terminal",
|
||||
outcome: expect.objectContaining({ status: "ok" }),
|
||||
}),
|
||||
delivery: expect.objectContaining({ status: "delivered" }),
|
||||
}),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -75,15 +75,21 @@ if (storageFaultFile) {
|
|||
return statement;
|
||||
};
|
||||
}
|
||||
if (fixture.protocol !== "http:" || fixture.hostname !== "127.0.0.1" || !clockFile) {
|
||||
throw new Error("Quota fixture requires a loopback HTTP origin and a clock file");
|
||||
if (
|
||||
!["http:", "https:"].includes(fixture.protocol) ||
|
||||
fixture.hostname !== "127.0.0.1" ||
|
||||
!clockFile
|
||||
) {
|
||||
throw new Error("Quota fixture requires a loopback HTTP(S) origin and a clock file");
|
||||
}
|
||||
|
||||
const realNow = Date.now.bind(Date);
|
||||
const refreshReceipt = options.get("refreshReceipt");
|
||||
let authEventCount = 0;
|
||||
const recordAuth = (event) => {
|
||||
if (!refreshReceipt || authEventCount > 256) return;
|
||||
if (!refreshReceipt || authEventCount > 256) {
|
||||
return;
|
||||
}
|
||||
appendFileSync(
|
||||
refreshReceipt,
|
||||
`${JSON.stringify({
|
||||
|
|
@ -97,14 +103,18 @@ if (refreshReceipt) {
|
|||
const spawn = childProcess.spawn;
|
||||
childProcess.spawn = function (...args) {
|
||||
const child = Reflect.apply(spawn, this, args);
|
||||
if (!Array.isArray(args[1]) || !args[1].includes("app-server") || !child.stdout) return child;
|
||||
if (!Array.isArray(args[1]) || !args[1].includes("app-server") || !child.stdout) {
|
||||
return child;
|
||||
}
|
||||
recordAuth({ kind: "native-observer" });
|
||||
const observe = createQuotaNativeAuthObserver(recordAuth);
|
||||
const emit = child.stdout.emit;
|
||||
// Observe only delivery to the existing consumer; adding a data listener
|
||||
// here would start flowing before the production transport is attached.
|
||||
child.stdout.emit = function (event, ...values) {
|
||||
if (event === "data") observe(values[0]);
|
||||
if (event === "data") {
|
||||
observe(values[0]);
|
||||
}
|
||||
return Reflect.apply(emit, this, [event, ...values]);
|
||||
};
|
||||
return child;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,59 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { rawDataToString } from "@openclaw/gateway-client/websocket-data";
|
||||
import { expect, it, vi, type TestContext } from "vitest";
|
||||
import WebSocket from "ws";
|
||||
import * as testInstance from "../../../helpers/openclaw-test-instance.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js";
|
||||
import { createQuotaResetFixture, MARKER, startQuotaProvider } from "./quota-reset.test-support.js";
|
||||
|
||||
it("trusts only the fixture certificate for auxiliary HTTPS requests", async (context) => {
|
||||
const root = useAutoCleanupTempDirTracker(context.onTestFinished).make("quota-provider-tls-");
|
||||
const key = join(root, "key.pem");
|
||||
const cert = join(root, "cert.pem");
|
||||
execFileSync(
|
||||
"openssl",
|
||||
[
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-keyout",
|
||||
key,
|
||||
"-out",
|
||||
cert,
|
||||
"-days",
|
||||
"1",
|
||||
"-subj",
|
||||
"/CN=localhost",
|
||||
"-addext",
|
||||
"subjectAltName=IP:127.0.0.1",
|
||||
"-addext",
|
||||
"basicConstraints=critical,CA:FALSE",
|
||||
],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
const provider = await startQuotaProvider("codex_rate_limits", MARKER, {
|
||||
key: readFileSync(key),
|
||||
cert: readFileSync(cert),
|
||||
});
|
||||
context.onTestFinished(() => provider.stop());
|
||||
await expect(fetch(`${provider.baseUrl}/v1/responses`)).rejects.toMatchObject({
|
||||
cause: { code: "DEPTH_ZERO_SELF_SIGNED_CERT" },
|
||||
});
|
||||
const response = await provider.fetch("/v1/responses", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ model: "gpt-5.6-luna", input: [] }),
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.text()).toContain('"type":"response.completed"');
|
||||
expect(provider.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("prints bounded readiness receipts from the quota failure hook before Gateway logs", async (context) => {
|
||||
const instance = await testInstance.createOpenClawTestInstance({
|
||||
name: "quota-failure-receipt",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { createServer, type IncomingMessage } from "node:http";
|
||||
import { createServer as createHttpsServer } from "node:https";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { zstdDecompressSync } from "node:zlib";
|
||||
import { rawDataToString } from "@openclaw/gateway-client/websocket-data";
|
||||
import { asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { Agent, fetch as fetchProvider } from "undici";
|
||||
import { expect } from "vitest";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { createExternalAuthRuntime } from "../../../../src/agents/auth-profiles/external-auth.js";
|
||||
|
|
@ -112,7 +115,11 @@ function assistantTexts(history: ChatHistory): string[] {
|
|||
);
|
||||
}
|
||||
|
||||
export async function startQuotaProvider(source: BlockSource, responseText: string) {
|
||||
export async function startQuotaProvider(
|
||||
source: BlockSource,
|
||||
responseText: string,
|
||||
tls?: { key: Buffer; cert: Buffer },
|
||||
) {
|
||||
let phase: Phase = "healthy";
|
||||
let nextSuccessObserver: { observe: () => void; model: string; path: string } | undefined;
|
||||
let nextUsageHold: { arrived: Deferred<HeldProviderResponse>; released: Deferred } | undefined;
|
||||
|
|
@ -298,7 +305,8 @@ export async function startQuotaProvider(source: BlockSource, responseText: stri
|
|||
{ type: "response.completed", response },
|
||||
];
|
||||
};
|
||||
const server = createServer((request, response) => {
|
||||
const server = tls ? createHttpsServer(tls) : createServer();
|
||||
server.on("request", (request, response) => {
|
||||
void (async () => {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of request) {
|
||||
|
|
@ -332,7 +340,16 @@ export async function startQuotaProvider(source: BlockSource, responseText: stri
|
|||
response.writeHead(status, { "content-type": "application/json", ...headers });
|
||||
response.end(JSON.stringify(value));
|
||||
};
|
||||
if (requestPath === "/core-wham/usage" || requestPath === "/backend-api/wham/usage") {
|
||||
if (requestPath === "/backend-api/wham/accounts/check") {
|
||||
json(200, {
|
||||
accounts: [ACCOUNT_ID, "quota-alternate-account"].map((id) => ({
|
||||
id,
|
||||
workspace_backend_origin: "NO_CONSTRAINT",
|
||||
account_routing_override: "NO_CONSTRAINT",
|
||||
})),
|
||||
default_account_id: ACCOUNT_ID,
|
||||
});
|
||||
} else if (requestPath === "/core-wham/usage" || requestPath === "/backend-api/wham/usage") {
|
||||
// Preserve the native block source only on the original quota failure.
|
||||
if (
|
||||
requestPath === "/core-wham/usage" &&
|
||||
|
|
@ -498,8 +515,13 @@ export async function startQuotaProvider(source: BlockSource, responseText: stri
|
|||
if (!address || typeof address === "string") {
|
||||
throw new Error("Provider did not bind loopback");
|
||||
}
|
||||
const baseUrl = `${tls ? "https" : "http"}://127.0.0.1:${address.port}`;
|
||||
const dispatcher = new Agent(tls ? { connect: { ca: tls.cert } } : {});
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
baseUrl,
|
||||
fetch(requestPath: string, init?: Parameters<typeof fetchProvider>[1]) {
|
||||
return fetchProvider(`${baseUrl}${requestPath}`, { ...init, dispatcher });
|
||||
},
|
||||
requests,
|
||||
upgrades,
|
||||
responses,
|
||||
|
|
@ -535,6 +557,7 @@ export async function startQuotaProvider(source: BlockSource, responseText: stri
|
|||
return { arrived: hold.arrived.promise, release: () => hold.released.resolve() };
|
||||
},
|
||||
async stop() {
|
||||
await dispatcher.destroy();
|
||||
for (const socket of sockets.clients) {
|
||||
socket.terminate();
|
||||
}
|
||||
|
|
@ -580,7 +603,37 @@ export async function createQuotaResetFixture(
|
|||
) {
|
||||
const tempDirs = useAutoCleanupTempDirTracker((cleanup) => context.onTestFinished(cleanup));
|
||||
const root = tempDirs.make("openclaw-quota-reset-");
|
||||
const provider = await startQuotaProvider(source, responseText);
|
||||
// Native workspace routing requires an HTTPS backend; retain certificate verification.
|
||||
const caPath = path.join(root, "provider-ca.pem");
|
||||
const keyPath = path.join(root, "provider-key.pem");
|
||||
let tls: { key: Buffer; cert: Buffer } | undefined;
|
||||
if (runtime === "codex") {
|
||||
execFileSync(
|
||||
"openssl",
|
||||
[
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-keyout",
|
||||
keyPath,
|
||||
"-out",
|
||||
caPath,
|
||||
"-days",
|
||||
"1",
|
||||
"-subj",
|
||||
"/CN=localhost",
|
||||
"-addext",
|
||||
"subjectAltName=DNS:localhost,IP:127.0.0.1",
|
||||
"-addext",
|
||||
"basicConstraints=critical,CA:FALSE",
|
||||
],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
tls = { key: await fs.readFile(keyPath), cert: await fs.readFile(caPath) };
|
||||
}
|
||||
const provider = await startQuotaProvider(source, responseText, tls);
|
||||
context.onTestFinished(() => provider.stop());
|
||||
const nativeLogFile = path.join(root, "native.private.log");
|
||||
const refreshReceipt = path.join(root, "refresh-receipt.jsonl");
|
||||
|
|
@ -629,6 +682,7 @@ export async function createQuotaResetFixture(
|
|||
: [process.execPath, "--import", preload.href],
|
||||
startTimeoutMs: 120_000,
|
||||
env: {
|
||||
...(tls ? { NODE_EXTRA_CA_CERTS: caPath, CODEX_CA_CERTIFICATE: caPath } : {}),
|
||||
OPENCLAW_TEST_MINIMAL_GATEWAY: "0",
|
||||
OPENCLAW_SKIP_PROVIDERS: undefined,
|
||||
OPENCLAW_AGENT_HARNESS_FALLBACK: "none",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
SkillsLibraryReceipt,
|
||||
SkillsLibraryReadResult,
|
||||
} from "../../../../packages/gateway-protocol/src/schema/skill-library.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../../../../src/infra/tmp-openclaw-dir.js";
|
||||
import { runQaGatewayFixture } from "../../../helpers/qa-gateway-cleanup.js";
|
||||
import { MODEL_REF, PROOF_TIMEOUT_MS } from "./cloud-worker-midturn-loss-fixture.js";
|
||||
import {
|
||||
|
|
@ -325,7 +326,13 @@ describe("skill library mock-provider E2E through real Gateway and node worker",
|
|||
const workerSkillDir = workerFirst.directory;
|
||||
expect(outside(workerSkillDir, await fs.realpath(instance.stateDir))).toBe(true);
|
||||
expect(outside(workerSkillDir, remoteCwd)).toBe(true);
|
||||
expect(outside(workerSkillDir, await fs.realpath(node.stateDir))).toBe(false);
|
||||
// Scoped turn inputs live in the secure scratch root, separate from node credentials.
|
||||
const materializationDir = path.dirname(workerSkillDir);
|
||||
expect(path.dirname(materializationDir)).toBe(
|
||||
await fs.realpath(resolvePreferredOpenClawTmpDir()),
|
||||
);
|
||||
expect(path.basename(materializationDir)).toMatch(/^skill-resources-[a-f0-9]{16}$/u);
|
||||
expect(outside(workerSkillDir, await fs.realpath(node.stateDir))).toBe(true);
|
||||
expect(workerSkillDir).not.toBe(await fs.realpath(localFirst.directory));
|
||||
|
||||
// Authoring belongs to the authenticated invoker, even when the session pins Alice's A.
|
||||
|
|
@ -451,9 +458,9 @@ describe("skill library mock-provider E2E through real Gateway and node worker",
|
|||
expect((await turn(alice, localKey, "local-still-pinned")).reference).toBe(
|
||||
"ALICE-RESOURCE-1\n",
|
||||
);
|
||||
expect((await turn(bob, remoteKey, "bob-still-pinned")).reference).toBe(
|
||||
"ALICE-RESOURCE-1\n",
|
||||
);
|
||||
const workerStillPinned = await turn(bob, remoteKey, "bob-still-pinned");
|
||||
expect(workerStillPinned.reference).toBe("ALICE-RESOURCE-1\n");
|
||||
expect(workerStillPinned.directory).toBe(workerSkillDir);
|
||||
const newKey = await createSession("new-default");
|
||||
expect((await turn(alice, newKey, "new-default")).reference).toBe("ALICE-RESOURCE-2\n");
|
||||
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ describe.each(["automatic", "saved-clear", "automatic-during-catalog"] as const)
|
|||
},
|
||||
{ model: "gpt-5.5", path: "/v1/responses" },
|
||||
);
|
||||
const auxiliary = await fetch(`${provider.baseUrl}/v1/responses`, {
|
||||
const auxiliary = await provider.fetch("/v1/responses", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue