openclaw/test/scripts/check-wrapper-shadowing.test.ts
Mert Başar 92f9c60adc
fix(agents): show model reroutes in subagent completions (#135531)
Related: #87051
Related: #131220

## What Problem This Solves

When a dynamic subagent falls back after the requested model fails, its parent can receive a successful result without the model route that actually completed the work. The exact child terminal receipt already has both routes.

## Why This Change Was Made

The terminal producer now derives one bounded, redacted route fact from its exact-run receipt and stores it beside the visible child reply. The existing parent completion and requester-settle paths carry that fact as private context. Local and nested parents can report it. External channel parents receive an instruction to keep it private. Raw direct-delivery fallback sends only the child result.

The change keeps the terminal receipt as the only routing record. It changes no fallback policy, model selection, credentials, configuration, or database schema.

The PR also retains the contributor's bounded source-scanner repair for the two affected repository guards.

## User Impact

An operator can see the requested route and the model that completed an eligible private subagent result. Shared and raw external delivery paths do not receive the route fact.

## Evidence

- Red baseline: `ce5d04fac7`. An isolated Gateway sent a dynamic child to a synthetic missing OpenAI model. The provider returned `model_not_found`, the fallback completed on stable public `openai/gpt-5.6-luna`, and the terminal receipt contained both routes. The child result and parent completion omitted the route fact.
- Green exact head: `c6e23fe0ae1bd6e122565b11b2bd1638ec09fa1d`. The same isolated Gateway flow again proved the forced rejection and fallback. The child result remained assistant text only. The parent history and final response contained the exact receipt-owned route fact.
- Regression tests: the pre-fix requester-settle projection test fails because its parent prompt lacks the route fact. The repaired test passes. The pending-delivery retry test also fails with the stale reply precedence and passes with completion-owned precedence.
- Focused checks passed: 219 lifecycle, requester-settle, SQLite restart, and receipt tests; 5 external direct-delivery privacy tests; targeted Oxlint; formatting; and `git diff --check`.
- The local live proof is required because the exact boundary includes an authenticated real provider rejection and completion. Exact-head hosted CI remains the Blacksmith gate.
- Codex contract checked directly: `ModelReroutedNotification` carries `from_model` and `to_model`; OpenClaw's Codex projector records `toModel` as `responseModel`. This PR uses that producer response model, not configured fallback or session metadata.
- Production runtime: +159/-44, net +115. Tooling: +41/-51, net -10. Tests and test support: +237/-38, net +199. The runtime growth carries the receipt-owned fact through existing protocol and parent boundaries; no second routing record or alternate delivery path was added.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-09-03 03:34:34 +05:30

67 lines
2.2 KiB
TypeScript

import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { collectRepositoryWrapperShadowing } from "../../scripts/check-wrapper-shadowing.mts";
import { withTempDir } from "../../src/test-utils/temp-dir.js";
const guardScriptPath = fileURLToPath(
new URL("../../scripts/check-wrapper-shadowing.mts", import.meta.url),
);
type GuardFixture = Record<string, string>;
async function runFixture(files: GuardFixture) {
return await withTempDir("openclaw-wrapper-shadowing-", async (repoRoot) => {
await Promise.all(
Object.entries(files).map(async ([repoPath, content]) => {
const filePath = path.join(repoRoot, repoPath);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, content);
}),
);
return await collectRepositoryWrapperShadowing(repoRoot);
});
}
const directViolation: GuardFixture = {
"src/inner.js": "export function runTask() { return 'inner'; }\n",
"src/outer.ts": [
'import { runTask as runTaskInner } from "./inner.js";',
"export function runTask() {",
" prepareTask();",
" return runTaskInner();",
"}",
].join("\n"),
};
describe("wrapper shadowing guard", () => {
it("fails for a same-name wrapper around an imported implementation", async () => {
const result = await runFixture(directViolation);
expect(result).toEqual([{ name: "runTask", wrapped: "src/inner.js", wrapper: "src/outer.ts" }]);
});
it("passes for a pure re-export", async () => {
const result = await runFixture({
"src/inner.ts": "export function runTask() { return 'inner'; }\n",
"src/outer.ts": 'export { runTask } from "./inner.js";\n',
});
expect(result).toEqual([]);
});
it("rejects debt-baseline updates with the wrapper trailer", () => {
const result = spawnSync(
process.execPath,
["--import", "tsx", guardScriptPath, "--update-debt-baseline"],
{ encoding: "utf8" },
);
expect(result.status).toBe(2);
expect(result.stderr.trimEnd().split("\n").at(-1)).toBe(
"[check-wrapper-shadowing] FAILED (exit 2)",
);
});
});