


























1+import { describe, expect, it } from "vitest";
2+import { resolveModelAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js";
3+import type { OpenClawConfig } from "../config/types.openclaw.js";
4+import { parseAgentSessionKey } from "../routing/session-key.js";
5+6+/**
7+ * Catalog #18 — `openclaw sessions --json` reports `agentRuntime.id: "pi"` for
8+ * ACP sessions because `resolveAgentRuntimeMetadata` only consults agent-config
9+ * policies (env / agent / defaults / implicit fallback to "pi"). The session
10+ * key clearly carries the ACP runtime indicator (the `:acp:` segment), but
11+ * `sessions.ts:294` ignores it and just calls `resolveAgentRuntimeMetadata(cfg, agentId)`.
12+ *
13+ * Empirical observation from a deployed openclaw container against a copilot
14+ * agent that has no explicit `agentRuntime.id` policy:
15+ *
16+ * {
17+ * "key": "agent:copilot:acp:86b7b5af-3773-4a56-b244-069d6c5d3db9",
18+ * "agentId": "copilot",
19+ * "agentRuntime": { "id": "pi", "source": "implicit" },
20+ * "kind": "direct"
21+ * }
22+ *
23+ * That is wrong: this session is plainly ACP, not PI. The runtime field is
24+ * supposed to be a faithful classifier of how this session is actually being
25+ * run; instead, every ACP session in the JSON output is mislabelled as `pi`.
26+ *
27+ * This test mirrors the exact computation `sessionsCommand` performs at
28+ * `src/commands/sessions.ts:294` and proves the bug in two parts:
29+ *
30+ * - RED: ACP-keyed session resolves to `id: "pi"`, `source: "implicit"`.
31+ * - GREEN control: a non-ACP `agent:main:main` session resolves to the
32+ * same implicit-pi metadata, which IS correct in that case. The control
33+ * proves the assertion infrastructure is not masking the RED case.
34+ *
35+ * Fix shape (see the third test): when the session key is ACP-style,
36+ * agentRuntime.id should report `acpx` (or whatever runtime id is actually
37+ * driving the session) so that the JSON faithfully classifies the session.
38+ * The fix likely belongs at the caller (sessions.ts:294 and the other
39+ * call sites in `src/gateway/server-methods/sessions.ts`,
40+ * `src/gateway/session-utils.ts`) so it can pass session-key context to
41+ * `resolveAgentRuntimeMetadata`, OR `resolveAgentRuntimeMetadata` itself
42+ * gains an optional `sessionKey` parameter and applies a session-key-aware
43+ * override.
44+ */
45+46+const ACP_SESSION_KEY = "agent:copilot:acp:86b7b5af-3773-4a56-b244-069d6c5d3db9";
47+const NON_ACP_SESSION_KEY = "agent:main:main";
48+49+/**
50+ * Build a minimal `OpenClawConfig` that mirrors the deployed scenario:
51+ * - a copilot agent exists in the agents.list
52+ * - it has NO explicit `agentRuntime.id` policy
53+ * - no top-level `agents.defaults.agentRuntime` either
54+ *
55+ * Result: `resolveAgentRuntimeMetadata(cfg, "copilot")` falls through to the
56+ * implicit "pi" branch — which is the bug under test.
57+ */
58+function buildConfigWithoutAgentRuntimePolicy(): OpenClawConfig {
59+return {
60+agents: {
61+list: [
62+{
63+id: "copilot",
64+// Intentionally no `agentRuntime` field, no `runtime` descriptor.
65+},
66+{
67+id: "main",
68+},
69+],
70+// No `defaults.agentRuntime` either.
71+defaults: {},
72+},
73+} as OpenClawConfig;
74+}
75+76+/**
77+ * Mirror the per-row computation from `src/commands/sessions.ts:290-298`:
78+ * const agentId = parseAgentSessionKey(row.key)?.agentId ?? target.agentId;
79+ * const agentRuntime = resolveModelAgentRuntimeMetadata({ cfg, agentId, sessionKey: row.key });
80+ *
81+ * Returns the same shape that ends up serialized to `--json` output.
82+ * After commit 02fe0d8978, the production path goes through resolveModelAgentRuntimeMetadata
83+ * (not resolveAgentRuntimeMetadata which is now a stub returning { id: "auto", source: "implicit" }).
84+ */
85+function computeSessionAgentRuntime(params: {
86+cfg: OpenClawConfig;
87+sessionKey: string;
88+fallbackAgentId: string;
89+/** Mirrors `entry?.acp != null` passed from loaded session rows. */
90+acpRuntime?: boolean;
91+/** Mirrors `entry?.acp?.backend` passed from the session store entry. */
92+acpBackend?: string;
93+}): ReturnType<typeof resolveModelAgentRuntimeMetadata> {
94+const agentId = parseAgentSessionKey(params.sessionKey)?.agentId ?? params.fallbackAgentId;
95+return resolveModelAgentRuntimeMetadata({
96+cfg: params.cfg,
97+ agentId,
98+sessionKey: params.sessionKey,
99+acpRuntime: params.acpRuntime,
100+acpBackend: params.acpBackend,
101+});
102+}
103+104+describe("sessions --json agentRuntime classifier (catalog #18)", () => {
105+it("RED→GREEN: ACP session key is no longer misclassified (overlay applies)", () => {
106+const cfg = buildConfigWithoutAgentRuntimePolicy();
107+const agentRuntime = computeSessionAgentRuntime({
108+ cfg,
109+sessionKey: ACP_SESSION_KEY,
110+fallbackAgentId: "copilot",
111+acpRuntime: true,
112+});
113+114+// The bug was: the session key plainly contains `:acp:` and yet the
115+// resolved metadata said id="pi", source="implicit".
116+// After the fix (applyAcpRuntimeOverlay in resolveModelAgentRuntimeMetadata),
117+// the ACP session key overrides the runtime to id="acpx", source="session-key".
118+expect(
119+agentRuntime.id,
120+`ACP session ${ACP_SESSION_KEY} should no longer be misclassified as "auto" or "pi". ` +
121+`Got "${agentRuntime.id}". resolveModelAgentRuntimeMetadata must pass sessionKey to ` +
122+`applyAcpRuntimeOverlay so ACP sessions are classified as "acpx".`,
123+).not.toBe("auto");
124+expect(
125+agentRuntime.source,
126+`ACP session ${ACP_SESSION_KEY} resolved with source="${agentRuntime.source}". ` +
127+`For an ACP-keyed session, the source should not be "implicit" — ` +
128+`the session key itself is an explicit signal that the runtime is ACP.`,
129+).not.toBe("implicit");
130+});
131+132+it("GREEN control: non-ACP session is NOT overridden by ACP overlay", () => {
133+const cfg = buildConfigWithoutAgentRuntimePolicy();
134+const agentRuntime = computeSessionAgentRuntime({
135+ cfg,
136+sessionKey: NON_ACP_SESSION_KEY,
137+fallbackAgentId: "main",
138+});
139+140+// For a non-ACP session, the overlay must NOT fire — the result must
141+// not be "acpx" and source must not be "session-key". The control
142+// proves the overlay is gated on the `:acp:` segment in the session key.
143+// (The concrete id — "codex" for the default openai/gpt-5.5 provider —
144+// is determined by resolveAgentHarnessPolicy's Codex-routing rule;
145+// what matters here is the absence of the ACP override.)
146+expect(agentRuntime.id).not.toBe("acpx");
147+expect(agentRuntime.source).not.toBe("session-key");
148+});
149+150+it("FIX-SHAPE expectation: ACP session should resolve to 'acpx'", () => {
151+// What "fixed" should look like once the bug is addressed.
152+// RED today; GREEN once the fix lands.
153+//
154+// Note: the exact id ("acpx" vs another label) is a design choice for
155+// the fix author. What matters is that it is meaningfully different
156+// from "pi" and reflects the actual runtime driving the session.
157+// If the fix picks a different label, update this assertion to match —
158+// the structural point (session-key-aware classification) is the
159+// load-bearing part.
160+const cfg = buildConfigWithoutAgentRuntimePolicy();
161+const agentRuntime = computeSessionAgentRuntime({
162+ cfg,
163+sessionKey: ACP_SESSION_KEY,
164+fallbackAgentId: "copilot",
165+acpRuntime: true,
166+});
167+168+expect(
169+agentRuntime.id,
170+`ACP session ${ACP_SESSION_KEY} should resolve to runtime id "acpx" (or the canonical ACP runtime label). ` +
171+`Got "${agentRuntime.id}". Fix candidates: ` +
172+`(a) override at the call site in src/commands/sessions.ts:294 once isAcpSessionKey(row.key) is true, or ` +
173+`(b) extend resolveAgentRuntimeMetadata to accept an optional sessionKey and apply the override centrally.`,
174+).toBe("acpx");
175+});
176+177+it("backend override: ACP session with entry.acp.backend set reports that backend id, NOT 'acpx'", () => {
178+// When the session entry carries an explicit acp.backend (e.g. a registered
179+// non-default backend), the overlay must reflect the actual backend instead
180+// of the generic "acpx" fallback.
181+const cfg = buildConfigWithoutAgentRuntimePolicy();
182+const agentRuntime = computeSessionAgentRuntime({
183+ cfg,
184+sessionKey: ACP_SESSION_KEY,
185+fallbackAgentId: "copilot",
186+acpRuntime: true,
187+acpBackend: "custom-backend",
188+});
189+190+expect(agentRuntime.id).toBe("custom-backend");
191+expect(agentRuntime.source).toBe("session-key");
192+});
193+194+it("backend fallback: ACP session with entry.acp but no backend falls back to 'acpx'", () => {
195+// When the session entry has ACP metadata but no acp.backend, the overlay
196+// must fall back to the canonical "acpx" id.
197+const cfg = buildConfigWithoutAgentRuntimePolicy();
198+const agentRuntime = computeSessionAgentRuntime({
199+ cfg,
200+sessionKey: ACP_SESSION_KEY,
201+fallbackAgentId: "copilot",
202+acpRuntime: true,
203+// acpBackend intentionally omitted — mirrors entry with no acp.backend
204+});
205+206+expect(agentRuntime.id).toBe("acpx");
207+expect(agentRuntime.source).toBe("session-key");
208+});
209+210+it("GREEN control: ACP-shaped bridge session without entry.acp is NOT overridden", () => {
211+const cfg = buildConfigWithoutAgentRuntimePolicy();
212+const agentRuntime = computeSessionAgentRuntime({
213+ cfg,
214+sessionKey: ACP_SESSION_KEY,
215+fallbackAgentId: "copilot",
216+acpRuntime: false,
217+});
218+219+expect(agentRuntime.id).not.toBe("acpx");
220+expect(agentRuntime.source).not.toBe("session-key");
221+});
222+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。