From 43834997528540d0fd2c5b792eb18c0dbdbb47e1 Mon Sep 17 00:00:00 2001 From: RoboClaw Date: Tue, 22 Sep 2026 05:14:43 -0700 Subject: [PATCH] test(gateway): bind restored requester fixture provenance (#155621) Bind the restored-requester E2E seed to canonical physical requester and controller store identities, then verify the persisted SQLite round trip. Preserve the original four cases, deadlines and assertions. Capture bounded, redacted first-wait diagnostics and settle all Gateway/model cleanup owners. Test-fixture correction only; no production provenance guard, schema or dependency changes. Validation: exact-head CI 35719796768 passed. Reviewed production-plus-fixture composite de1bb285 passed all four real built-Gateway loopback E2E cases in native run 35722722579, with fulfilled fixture cleanup and verified host closure. This is not authenticated external-provider or release acceptance. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> --- ...eway-restored-requester-settle.e2e.test.ts | 104 ++++++++++++++++-- 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/test/gateway-restored-requester-settle.e2e.test.ts b/test/gateway-restored-requester-settle.e2e.test.ts index 4bdfe0d10e92..8076a37edf09 100644 --- a/test/gateway-restored-requester-settle.e2e.test.ts +++ b/test/gateway-restored-requester-settle.e2e.test.ts @@ -8,9 +8,11 @@ import { saveSubagentRegistryToSqlite, } from "../src/agents/subagents/registry/subagent-registry.store.sqlite.js"; import type { SubagentRunRecord } from "../src/agents/subagents/registry/subagent-registry.types.js"; +import { resolvePhysicalSessionStorePath } from "../src/config/sessions/session-store-path.js"; import type { OpenClawConfig } from "../src/config/types.openclaw.js"; import type { SessionsListResult } from "../src/gateway/session-utils.types.js"; import { connectGatewayClient, disconnectGatewayClient } from "../src/gateway/test-helpers.e2e.js"; +import { redactSensitiveText } from "../src/logging/redact.js"; import type { Deferred } from "../src/shared/deferred.js"; import { closeOpenClawStateDatabaseForTest } from "../src/state/openclaw-state-db.js"; import { writeOpenAiResponsesSse } from "./helpers/openai-responses-sse.js"; @@ -45,8 +47,29 @@ afterEach(async () => { for (const server of modelServers) { server.releaseAll(); } - await Promise.allSettled(instances.splice(0).map((instance) => instance.cleanup())); - await Promise.allSettled(modelServers.splice(0).map((server) => server.close())); + const ownedInstances = instances.splice(0); + const gatewayCleanup = await Promise.allSettled( + ownedInstances.map((instance) => instance.cleanup()), + ); + const modelCleanup = await Promise.allSettled( + modelServers.splice(0).map((server) => server.close()), + ); + console.info( + "[restored-requester] cleanup", + JSON.stringify({ + gateways: ownedInstances.map((instance, index) => ({ + name: instance.name, + status: gatewayCleanup[index]?.status ?? "unavailable", + })), + modelServers: modelCleanup.map((result) => result.status), + }), + ); + const cleanupErrors = [...gatewayCleanup, ...modelCleanup].flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (cleanupErrors.length > 0) { + throw new AggregateError(cleanupErrors, "Restored requester fixture cleanup failed"); + } }); describe("Gateway restored requester settlement", () => { @@ -66,7 +89,7 @@ describe("Gateway restored requester settlement", () => { env: { OPENCLAW_SKIP_PROVIDERS: undefined, OPENCLAW_TEST_MINIMAL_GATEWAY: undefined }, }); instances.push(instance); - await seedRestoredRequesters(instance, 1); + await seedRestoredRequesters(instance, 1, cfg); await instance.startGateway(); const client = await connectGatewayClient({ url: instance.url, @@ -78,7 +101,11 @@ describe("Gateway restored requester settlement", () => { await client.request("sessions.list", { agentId: "main" }) ).sessions.find((row) => row.key === sessionKey); try { - await vi.waitFor(() => expect(modelServer.requestCount()).toBe(1), { timeout: 30_000 }); + await vi + .waitFor(() => expect(modelServer.requestCount()).toBe(1), { timeout: 30_000 }) + .catch((error: unknown) => { + throw gatewayDiagnosticError(instance, error); + }); const initial = await session(); expect(initial).toMatchObject({ status: "running", hasActiveRun: true }); // This elapsed wait is the regression itself: the real model socket must @@ -159,9 +186,10 @@ describe("Gateway restored requester settlement", () => { async () => { const modelServer = await startHeldModelServer(); modelServers.push(modelServer); + const cfg = createTestConfig(modelServer.url); const instance = await createOpenClawTestInstance({ name: "gateway-restored-requester-settle", - config: createTestConfig(modelServer.url), + config: cfg, env: { OPENCLAW_SKIP_PROVIDERS: undefined, OPENCLAW_TEST_MINIMAL_GATEWAY: undefined, @@ -169,13 +197,17 @@ describe("Gateway restored requester settlement", () => { }); instances.push(instance); - await seedRestoredRequesters(instance, 3); + await seedRestoredRequesters(instance, 3, cfg); await instance.startGateway(); - await vi.waitFor( - () => expect(modelServer.countRequestsContaining(RESTORED_WAKE_MARKER)).toBe(2), - { interval: 20, timeout: 30_000 }, - ); + await vi + .waitFor(() => expect(modelServer.countRequestsContaining(RESTORED_WAKE_MARKER)).toBe(2), { + interval: 20, + timeout: 30_000, + }) + .catch((error: unknown) => { + throw gatewayDiagnosticError(instance, error); + }); expect(modelServer.active(), instance.logs()).toBe(2); expect(modelServer.peakRestored(), instance.logs()).toBe(2); @@ -198,16 +230,51 @@ describe("Gateway restored requester settlement", () => { ); }); -async function seedRestoredRequesters(instance: OpenClawTestInstance, count: number) { +function gatewayDiagnosticError(instance: OpenClawTestInstance, cause: unknown): Error { + const readiness = instance.readiness.map( + ({ outcome, elapsedMs, attempts, lastProbe, child }) => ({ + outcome, + elapsedMs, + attempts, + lastProbe, + child, + }), + ); + // Snapshot the existing bounded child buffers before normal fixture cleanup removes state. + let diagnostic = redactSensitiveText( + `${instance.name}\n${JSON.stringify({ readiness })}\n${instance.logs()}`, + { mode: "tools" }, + ); + for (const token of [instance.gatewayToken, instance.hookToken]) { + if (token) { + diagnostic = diagnostic.replaceAll(token, "[fixture token]"); + } + } + return new Error(diagnostic, { cause }); +} + +async function seedRestoredRequesters( + instance: OpenClawTestInstance, + count: number, + cfg: OpenClawConfig, +) { instance.state.applyEnv(); try { const endedAt = Date.now(); const restoredRuns = Array.from({ length: count }, (_, index): SubagentRunRecord => { const runId = `run-gateway-restored-settle-${index}`; + const requesterSessionKey = `agent:main:gateway-restored-requester-${index}`; + const requesterStorePath = resolvePhysicalSessionStorePath( + { sessionKey: requesterSessionKey, agentId: "main", env: instance.env }, + cfg, + ); return { runId, childSessionKey: `agent:main:subagent:gateway-restored-settle-${index}`, - requesterSessionKey: `agent:main:gateway-restored-requester-${index}`, + requesterSessionKey, + requesterAgentId: "main", + requesterStorePath, + controllerStorePath: requesterStorePath, requesterDisplayKey: `gateway-restored-requester-${index}`, task: "resume a durable requester wake through the Gateway CLI", cleanup: "keep", @@ -246,6 +313,19 @@ async function seedRestoredRequesters(instance: OpenClawTestInstance, count: num defaultSessionId: `gateway-restored-requester-${index}`, }); } + const persistedRuns = loadSubagentRegistryFromSqlite(); + for (const entry of restoredRuns) { + // Direct SQLite fixtures must preserve the physical identity stamped by normal launch. + const storePath = resolvePhysicalSessionStorePath( + { sessionKey: entry.requesterSessionKey, agentId: "main", env: instance.env }, + cfg, + ); + expect(persistedRuns.get(entry.runId)).toMatchObject({ + requesterAgentId: "main", + requesterStorePath: storePath, + controllerStorePath: storePath, + }); + } } finally { // Keep this one state lease through the Gateway run and retained-result reads; // instance.cleanup owns restoration after every process has stopped.