Files
fusion/packages/engine/src/__tests__/openclaw-runtime-integration.test.ts
gsxdsm bbaa254dc3 test: add the missing debug to 27 logger mocks (206 → 4 failures) (#2573)
**Test-infrastructure fix.** 29 test files. No production code, no
altered assertions, no widened timeouts.

Now **3 commits** (#2584 merged into this branch): the logger-mock
sweep, a cron-runner follow-up from review, and the 4 residual failures
the sweep deliberately deferred.

**Whole branch: 764 tests, 0 failures** across the touched set.

---

## Commit 1 — the missing `debug` on 27 logger mocks

`createLogger`'s real shape is `{ log, debug, warn, error }`. 27 engine
test files mock `../logger.js` with logger-shaped literals that **omit
`debug`**, so any production path reaching `log.debug` threw:

```
TypeError: schedulerLog.debug is not a function
TypeError: runtimeLog.debug is not a function
TypeError: log.debug is not a function      (SelfHealingManager.start)
```

Measured, same commit, same 27 files:

| | Failed | Passed |
|---|---|---|
| before | **206** | 558 |
| after | **4** | 760 |

**202 failures fixed by one missing mock export.** Per-file: `notifier`
36→0, `plugin-runner` 56→0, `grok-runtime-routing` 14→0,
`self-healing-completion-fanout` 1→0. That last one also leaked an
unhandled rejection out of `startMaintenance`, which vitest warns "might
cause false positive tests" elsewhere in the file.

*A note on the number:* a full `engine-default` run went 283 → 106
across my two sessions, but `main` moved in between (U11 landed), so
that spread is **not** attributable here. 206 → 4 is the honest figure:
same commit, same file set, only this diff varying.

## Commit 2 — cron-runner's factory (greptile P1)

My regex required `log: vi.fn()`; `cron-runner.test.ts` uses `log:
cronLoggerSpies.log`, so the `createLogger` factory's returned literal
never matched and the logger production received still lacked `debug`.

**Measured before claiming a live fix, and the numbers don't support
that part:** `cronLoggerSpies.debug.mock.calls.length` is **0** across
all 155 tests, and the suite is 155 passed both before and after. The
described failure mode — `tick()` hitting `log.debug`, throwing, and
being swallowed by its own error handler — is **not reachable today**,
because no test exercises those three branches (`cron-runner.ts:377`,
`:385`, `:410`). The fix is defensive, not curative. The real gap it
surfaced is **missing coverage** for schedule dedupe / scope mismatch /
lost atomic claim, which I did not write blind to close a thread.

## Commit 3 — the 4 residuals

**`notification-service` (3):** messages moved to DEBUG in production
(`:580`, `:846`) while tests asserted `schedulerLog.log`.

The token case needed more than a relocation. It asserted
`expect(schedulerLog.log).not.toHaveBeenCalledWith(containing("new-token"))`.
Moving only the *positive* assertion to `debug` would leave the secrecy
check watching a channel the message no longer uses — a token could leak
through `debug` and the test would still pass. The negative now runs
across all four channels. **Verified it bites:** interpolating the token
into the debug line fails the test.

**`openclaw-runtime-integration` (1):** `../pi.js` mock missing
`wrapToolsWithOutputBudget` (same class as #2547); this suite exercises
a non-pi runtime, exactly where that wrapper applies.

**Not swept repo-wide, and the measurement is why.** 37 `pi.js` mocks
omit that export. Patching 30 moved the set from **11 failed to 10** —
thirty files of churn for one test. Reverted. Commit 1 earned its
27-file diff with 202 fixes; this one earned nothing, and a no-op sweep
is just future merge conflicts for other workers on this program.

---

## Why none of this is appeasement

AGENTS.md forbids making a red test pass by loosening it. This does the
opposite: the mocks were **wrong** — they claimed to stand in for
`createLogger` while missing part of its interface. Nothing was relaxed;
stubs were completed, and the one assertion I did move got **stronger**
(four channels instead of one).

## Also deliberately not done

Extending `scripts/check-mock-completeness.mjs` to catch this class.
Measured first: a naive rule over relative intra-package mocks flags
**147** factories of which **146 are green** — almost pure false
positives. The barrel heuristic works because `cliSrc` gives a tight
import surface; that doesn't transfer. A gate that noisy gets ignored,
which is worse than no gate.

## How this was found

While characterizing U9's review lane. These files were pre-existing
baseline noise under mutation runs — and that noise is exactly what made
my own safeguard baseline (#2511, corrected in #2520) report two false
verdicts. **A red suite does not merely lack coverage; it makes every
nearby measurement untrustworthy.**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:50:46 -07:00

217 lines
7.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { AgentRuntime } from "../agent-runtime.js";
import { resolveRuntime } from "../runtime-resolution.js";
import { createResolvedAgentSession } from "../agent-session-helpers.js";
import type { PluginRunner } from "../plugin-runner.js";
import type { PluginRuntimeRegistration } from "@fusion/core";
const mockCreateFnAgent = vi.hoisted(() => vi.fn());
vi.mock("../logger.js", () => ({
createLogger: vi.fn(() => ({
log: vi.fn(), debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})),
}));
vi.mock("../pi.js", () => ({
/*
FNXC:TestInfrastructure 2026-07-29-15:30:
`wrapCustomToolsForPluginRuntime` (agent-session-helpers.ts:104) applies this as
the outermost tool wrapper on every NON-PI runtime path — which is exactly what
this suite exercises — so omitting it threw "No wrapToolsWithOutputBudget export
is defined on the ../pi.js mock" at session construction. Identity stub: the real
function returns tools byte-identical for a null budget, and this suite asserts
runtime selection/metadata, not output budgeting.
*/
wrapToolsWithOutputBudget: vi.fn((tools: unknown[]) => tools),
createFnAgent: mockCreateFnAgent,
promptWithFallback: vi.fn().mockResolvedValue(undefined),
describeModel: vi.fn().mockReturnValue("pi/default"),
// FNXC: pi.js tool-policy wrappers (wrapToolsWithRtkRewrite, wrapToolsWithPermanentAgentGating, wrapToolsWithActionGate) are now imported by agent-session-helpers.ts (wrapCustomToolsForPluginRuntime, called from createResolvedAgentSession). Mocks pass tools through unchanged.
wrapToolsWithRtkRewrite: vi.fn((tools) => tools),
wrapToolsWithPermanentAgentGating: vi.fn((tools) => tools),
wrapToolsWithActionGate: vi.fn((tools) => tools),
}));
function isAgentRuntime(value: unknown): value is AgentRuntime {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as AgentRuntime).createSession === "function" &&
typeof (value as AgentRuntime).promptWithFallback === "function" &&
typeof (value as AgentRuntime).describeModel === "function"
);
}
function createMockPluginRunner(overrides: Partial<PluginRunner> = {}): PluginRunner {
return {
getPluginRuntimes: vi.fn().mockReturnValue([]),
getRuntimeById: vi.fn().mockReturnValue(undefined),
createRuntimeContext: vi.fn().mockResolvedValue({
pluginId: "fusion-plugin-openclaw-runtime",
taskStore: {},
settings: {},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: vi.fn(),
}),
...overrides,
} as unknown as PluginRunner;
}
function createOpenClawRegistration(factoryImpl?: () => unknown): {
pluginId: string;
runtime: PluginRuntimeRegistration;
} {
return {
pluginId: "fusion-plugin-openclaw-runtime",
runtime: {
metadata: {
runtimeId: "openclaw",
name: "OpenClaw Runtime",
description: "OpenClaw-backed AI session using the local OpenClaw gateway",
version: "0.1.0",
},
factory: vi.fn().mockImplementation(async () =>
factoryImpl
? factoryImpl()
: {
id: "openclaw",
name: "OpenClaw Runtime",
createSession: vi.fn().mockResolvedValue({
session: { runtime: "openclaw", prompt: vi.fn() },
sessionFile: "/tmp/openclaw.session.json",
}),
promptWithFallback: vi.fn().mockResolvedValue(undefined),
describeModel: vi.fn().mockReturnValue("openclaw/main"),
},
),
},
};
}
describe("OpenClaw runtime integration via engine resolution pipeline", () => {
beforeEach(() => {
vi.clearAllMocks();
mockCreateFnAgent.mockResolvedValue({
session: { runtime: "pi", prompt: vi.fn() },
sessionFile: "/tmp/pi.session.json",
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("resolves OpenClaw runtime through PluginRunner lookup when runtimeHint is openclaw", async () => {
const registration = createOpenClawRegistration();
const pluginRunner = createMockPluginRunner({
getRuntimeById: vi.fn().mockReturnValue(registration),
});
const resolved = await resolveRuntime({
sessionPurpose: "executor",
runtimeHint: "openclaw",
pluginRunner,
});
expect(resolved.runtimeId).toBe("openclaw");
expect(resolved.wasConfigured).toBe(true);
expect(resolved.runtime.id).toBe("openclaw");
expect(resolved.runtime.name).toBe("OpenClaw Runtime");
expect(pluginRunner.getRuntimeById).toHaveBeenCalledWith("openclaw");
expect(pluginRunner.createRuntimeContext).toHaveBeenCalledWith("fusion-plugin-openclaw-runtime");
});
it("returns a runtime object that conforms to AgentRuntime", async () => {
const registration = createOpenClawRegistration();
const pluginRunner = createMockPluginRunner({
getRuntimeById: vi.fn().mockReturnValue(registration),
});
const resolved = await resolveRuntime({
sessionPurpose: "executor",
runtimeHint: "openclaw",
pluginRunner,
});
expect(isAgentRuntime(resolved.runtime)).toBe(true);
});
it("createResolvedAgentSession uses OpenClaw runtime and reports configured runtime metadata", async () => {
const runtimeSession = { runtime: "openclaw", prompt: vi.fn() };
const createSession = vi.fn().mockResolvedValue({
session: runtimeSession,
sessionFile: "/tmp/openclaw.session.json",
});
const registration = createOpenClawRegistration(() => ({
id: "openclaw",
name: "OpenClaw Runtime",
createSession,
promptWithFallback: vi.fn().mockResolvedValue(undefined),
describeModel: vi.fn().mockReturnValue("openclaw/main"),
}));
const pluginRunner = createMockPluginRunner({
getRuntimeById: vi.fn().mockReturnValue(registration),
});
const customTool = {
name: "fn_task_show",
label: "fn_task_show",
description: "show",
parameters: { type: "object" },
execute: vi.fn(),
} as any;
const result = await createResolvedAgentSession({
sessionPurpose: "executor",
runtimeHint: "openclaw",
pluginRunner,
cwd: "/tmp/project",
systemPrompt: "You are helpful",
tools: "coding",
customTools: [customTool],
});
expect(result.runtimeId).toBe("openclaw");
expect(result.wasConfigured).toBe(true);
expect(result.session).toBe(runtimeSession);
expect(result.sessionFile).toBe("/tmp/openclaw.session.json");
expect(createSession).toHaveBeenCalledWith(expect.objectContaining({
cwd: "/tmp/project",
systemPrompt: "You are helpful",
tools: "coding",
customTools: [customTool],
}));
});
it("falls back to default pi runtime when OpenClaw factory throws", async () => {
const registration = createOpenClawRegistration(() => {
throw new Error("factory exploded");
});
const pluginRunner = createMockPluginRunner({
getRuntimeById: vi.fn().mockReturnValue(registration),
});
const result = await createResolvedAgentSession({
sessionPurpose: "executor",
runtimeHint: "openclaw",
pluginRunner,
cwd: "/tmp/project",
systemPrompt: "Use fallback",
});
expect(result.runtimeId).toBe("pi");
expect(result.wasConfigured).toBe(false);
expect(mockCreateFnAgent).toHaveBeenCalledWith(expect.objectContaining({
cwd: "/tmp/project",
systemPrompt: "Use fallback",
}));
});
});