Skip to content

Commit 6aa38e8

Browse files
authored
fix(e2e): use isolated gateway state for inference switch (#9216)
<!-- markdownlint-disable MD041 --> ## Summary The OpenClaw inference-switch E2E target installed OpenShell under an isolated home but looked for managed gateway state under the test process's ambient home. Pass the target home to the existing owned gateway-state verifier so the target uses its recorded Docker-driver gateway instead of falling through to unavailable user-systemd services. Failure evidence: E2E run 31895698451, job 95038810397. Ownership was claimed on #9166 before implementation: #9166 (comment) ## Related Issue Related to #9166. ## Changes - Thread the target home through managed gateway PID and runtime-marker discovery. - Store the temporary host-verification resolver under the same isolated home. - Pass the OpenClaw inference-switch target's installation home while preserving the existing default for other callers. - Add support coverage proving target-home managed state is selected without probing the user service. ## Type of Change - [x] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Quality Gates - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [ ] Docs updated for user-facing behavior changes - [x] Docs not applicable — justification: this corrects internal E2E fixture state discovery and changes no supported command, configuration, API, policy, default, error, or runtime behavior. - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: independent Codex Desktop security review at commit under review `d47cc72d8` passed all nine required categories with no finding; the three reviewed file blobs are unchanged at latest PR commit `87a58c462`. - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: ## Documentation Writer Review - [x] Documentation writer subagent reviewed the completed changes - Result: `no-docs-needed` - Evidence: At commit under review `87a58c462`, the complete diff against base `de29c02a1` changes only internal E2E fixtures and tests. It passes the isolated test home to the existing owned gateway-state verifier and stores verifier artifacts under that home; it does not change supported product behavior or a user-visible surface. - Agent: Codex Desktop (`/root/openclaw_docs_review`) <!-- docs-review-head-sha: 87a58c4 --> <!-- docs-review-agents-blob-sha: e30afb2 --> ## DGX Station Hardware Evidence - [ ] Tested on DGX Station - Tested commit: - Station profile/scenario: - Result: - Supporting evidence: ## Verification - [x] PR description includes a `Signed-off-by:` line and every commit appears as `Verified` in GitHub - [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or `npm run validate:pr` passed after refreshing `origin/main` when hooks were skipped or unavailable - [x] Targeted behavior tests pass for the current change set, or tests are marked not applicable above — at latest PR commit `87a58c462`, `npx vitest run --project e2e-support test/e2e/support/compatible-anthropic-switch.test.ts`: 11 passed, 1 Linux-only skipped on macOS. - [ ] Applicable broad gate passed — not applicable to this three-file internal E2E fixture correction; `npm run validate:pr`, CLI typecheck, Oxfmt, Oxlint, repository checks, semantic-phase coverage, size/conditional checks, and `git diff --check` passed after merging main `de29c02a1`. - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [ ] `npm run docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) --- Signed-off-by: Senthil Ravichandran <senthilr@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Expanded end-to-end coverage for gateway behavior across custom home directories. * Improved validation of gateway process ownership, executable resolution, and state-directory handling. * Strengthened host-verification setup and cleanup checks across managed and active service scenarios. * Updated provider-switching tests to verify behavior in isolated environments. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: Senthil Ravichandran <senthilr@nvidia.com>
1 parent de29c02 commit 6aa38e8

3 files changed

Lines changed: 45 additions & 19 deletions

File tree

test/e2e/fixtures/compatible-anthropic-switch.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ function readOwnedGatewayStateFile(filePath: string, currentUid: number): string
123123
}
124124
}
125125

126-
function managedOpenShellGatewayPid(): number | null {
127-
const stateDirectory = resolveDockerDriverGatewayStateDir(process.env, os.homedir());
126+
function managedOpenShellGatewayPid(homeDir: string): number | null {
127+
const stateDirectory = resolveDockerDriverGatewayStateDir(process.env, homeDir);
128128
const pidPath = path.join(stateDirectory, "openshell-gateway.pid");
129129
const markerPath = path.join(stateDirectory, "runtime.json");
130130
const pidPathExists = pathExists(pidPath);
@@ -186,8 +186,8 @@ function managedOpenShellGatewayPid(): number | null {
186186
return pid;
187187
}
188188

189-
async function activeOpenShellGatewayPid(host: HostCliClient): Promise<number> {
190-
const managedPid = managedOpenShellGatewayPid();
189+
async function activeOpenShellGatewayPid(host: HostCliClient, homeDir: string): Promise<number> {
190+
const managedPid = managedOpenShellGatewayPid(homeDir);
191191
if (managedPid !== null) return managedPid;
192192
for (const serviceName of GATEWAY_SERVICE_NAMES) {
193193
const result = await host.command(
@@ -219,11 +219,12 @@ async function activeOpenShellGatewayPid(host: HostCliClient): Promise<number> {
219219
export async function installGatewayHostVerificationAlias(
220220
host: HostCliClient,
221221
cleanup: { add(name: string, run: () => Promise<void> | void): void },
222+
homeDir: string = os.homedir(),
222223
): Promise<void> {
223-
const gatewayPid = await activeOpenShellGatewayPid(host);
224+
const gatewayPid = await activeOpenShellGatewayPid(host, homeDir);
224225
const ownerToken = randomBytes(16).toString("hex");
225226
const fixtureDirectory = fs.mkdtempSync(
226-
path.join(os.homedir(), ".nemoclaw-gateway-resolver-"),
227+
path.join(homeDir, ".nemoclaw-gateway-resolver-"),
227228
);
228229
const resolverSource = path.join(fixtureDirectory, "hosts");
229230
const ownedLine = `127.0.0.1 ${OPENSHELL_HOST_ALIAS} # nemoclaw-gateway-host-verifier:${ownerToken}`;

test/e2e/live/openclaw-inference-switch.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,7 @@ test("openclaw-inference-switch: switches route and preserves live OpenClaw beha
10931093

10941094
if (SWITCH_PROVIDER === "compatible-anthropic-endpoint" && SWITCH_MOCK_ANTHROPIC === "1") {
10951095
mockProvider = await startMockAnthropicProvider();
1096-
await installGatewayHostVerificationAlias(host, cleanup);
1096+
await installGatewayHostVerificationAlias(host, cleanup, home);
10971097
await artifacts.writeJson("mock-anthropic-provider.json", {
10981098
endpointUrl: mockProvider.endpointUrl,
10991099
});

test/e2e/support/compatible-anthropic-switch.test.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ const INVALID_MANAGED_GATEWAY_STATE_CASES = [
5454
},
5555
] as const;
5656

57+
function mockGatewayProcess(pid: number, gatewayBin: string): void {
58+
const statSync = fs.statSync;
59+
vi.spyOn(fs, "statSync").mockImplementation(((target) =>
60+
String(target) === `/proc/${pid}`
61+
? ({ uid: process.getuid?.() ?? 0 } as fs.Stats)
62+
: statSync(target)) as typeof fs.statSync);
63+
const realpathSync = fs.realpathSync;
64+
const gatewayExecutablePaths = new Set([`/proc/${pid}/exe`, gatewayBin]);
65+
vi.spyOn(fs, "realpathSync").mockImplementation(((target) =>
66+
gatewayExecutablePaths.has(String(target))
67+
? gatewayBin
68+
: realpathSync(target)) as typeof fs.realpathSync);
69+
}
70+
5771
describe("compatible Anthropic inference switch setup", () => {
5872
afterEach(() => {
5973
vi.unstubAllEnvs();
@@ -99,29 +113,36 @@ describe("compatible Anthropic inference switch setup", () => {
99113
expect(rewrite).not.toHaveBeenCalled();
100114
});
101115

102-
it("uses the managed Docker-driver gateway before the user service (#9166)", async () => {
103-
const stateDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-managed-gateway-test-"));
116+
it("uses managed Docker-driver gateway state from the target home before the user service (#9166)", async () => {
117+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-target-home-gateway-test-"));
118+
const stateDirectory = path.join(
119+
home,
120+
".local",
121+
"state",
122+
"nemoclaw",
123+
"openshell-docker-gateway",
124+
);
104125
const pid = process.pid;
105126
const gatewayBin = "/usr/bin/openshell-gateway";
106-
vi.stubEnv("NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR", stateDirectory);
127+
fs.mkdirSync(stateDirectory, { recursive: true });
128+
vi.stubEnv("NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR", "");
107129
writeDockerDriverGatewayPidFile(path.join(stateDirectory, "openshell-gateway.pid"), pid);
108130
writeDockerDriverGatewayRuntimeMarkerForStateDir(stateDirectory, {
109131
desiredEnv: {},
110132
endpoint: "https://127.0.0.1:8080",
111133
gatewayBin,
112134
pid,
113135
});
114-
const realpathSync = fs.realpathSync;
115-
const gatewayExecutablePaths = new Set([`/proc/${pid}/exe`, gatewayBin]);
116-
vi.spyOn(fs, "realpathSync").mockImplementation(
117-
((target) =>
118-
gatewayExecutablePaths.has(String(target)) ? gatewayBin : realpathSync(target)) as typeof fs.realpathSync,
119-
);
136+
mockGatewayProcess(pid, gatewayBin);
120137
const command = vi.fn().mockResolvedValue({ exitCode: 0, stderr: "", stdout: "" });
121138
const add = vi.fn();
122139

123140
try {
124-
await installGatewayHostVerificationAlias({ command } as unknown as HostCliClient, { add });
141+
await installGatewayHostVerificationAlias(
142+
{ command } as unknown as HostCliClient,
143+
{ add },
144+
home,
145+
);
125146
const cleanupMount = add.mock.calls[0]?.[1] as () => Promise<void>;
126147
await cleanupMount();
127148

@@ -133,7 +154,7 @@ describe("compatible Anthropic inference switch setup", () => {
133154
);
134155
}
135156
} finally {
136-
fs.rmSync(stateDirectory, { force: true, recursive: true });
157+
fs.rmSync(home, { force: true, recursive: true });
137158
}
138159
});
139160

@@ -153,7 +174,11 @@ describe("compatible Anthropic inference switch setup", () => {
153174
const add = vi.fn();
154175

155176
try {
156-
await installGatewayHostVerificationAlias({ command } as unknown as HostCliClient, { add });
177+
await installGatewayHostVerificationAlias(
178+
{ command } as unknown as HostCliClient,
179+
{ add },
180+
stateDirectory,
181+
);
157182
const cleanupMount = add.mock.calls[0]?.[1] as () => Promise<void>;
158183
await cleanupMount();
159184

0 commit comments

Comments
 (0)