
























@@ -17,6 +17,7 @@ vi.mock("./runtime-api.js", () => ({
1717}));
18181919import plugin from "./index.js";
20+import { createTokenjuiceAgentToolResultMiddleware } from "./tool-result-middleware.js";
20212122describe("tokenjuice plugin", () => {
2223beforeEach(() => {
@@ -53,4 +54,172 @@ describe("tokenjuice plugin", () => {
5354expect(typeof registration?.[0]).toBe("function");
5455expect(registration?.[1]).toEqual({ runtimes: ["openclaw", "codex"] });
5556});
57+58+it("synthesises exec fields when bash provides metadata-only details (no status)", async () => {
59+let received:
60+| {
61+details: unknown;
62+}
63+| undefined;
64+tokenjuiceFactory.mockImplementationOnce(
65+(api: { on: (event: string, handler: unknown) => void }) => {
66+api.on("tool_result", async (event: typeof received) => {
67+received = event;
68+});
69+},
70+);
71+72+const middleware = createTokenjuiceAgentToolResultMiddleware();
73+await middleware(
74+{
75+toolCallId: "tool-call-tokenjuice-bash-meta",
76+toolName: "bash",
77+args: { command: "cat /tmp/out.txt", workdir: "/tmp/openclaw-tokenjuice-test" },
78+result: {
79+content: [{ type: "text", text: "file contents\n" }],
80+details: {
81+truncation: { reason: "max_bytes" },
82+fullOutputPath: "/tmp/out.txt",
83+},
84+},
85+isError: false,
86+},
87+{ runtime: "openclaw" },
88+);
89+90+expect(received?.details).toMatchObject({
91+status: "completed",
92+aggregated: "file contents\n",
93+exitCode: 0,
94+truncation: { reason: "max_bytes" },
95+fullOutputPath: "/tmp/out.txt",
96+});
97+});
98+99+it("passes through bash details that already have a status field unchanged", async () => {
100+let received:
101+| {
102+details: unknown;
103+}
104+| undefined;
105+tokenjuiceFactory.mockImplementationOnce(
106+(api: { on: (event: string, handler: unknown) => void }) => {
107+api.on("tool_result", async (event: typeof received) => {
108+received = event;
109+});
110+},
111+);
112+113+const existingDetails = {
114+status: "completed",
115+aggregated: "pre-built output",
116+exitCode: 0,
117+cwd: "/existing/cwd",
118+};
119+120+const middleware = createTokenjuiceAgentToolResultMiddleware();
121+await middleware(
122+{
123+toolCallId: "tool-call-tokenjuice-bash-existing",
124+toolName: "bash",
125+args: { command: "echo hi", workdir: "/tmp" },
126+result: {
127+content: [{ type: "text", text: "hi\n" }],
128+details: existingDetails,
129+},
130+isError: false,
131+},
132+{ runtime: "openclaw" },
133+);
134+135+expect(received?.details).toBe(existingDetails);
136+});
137+138+it.each([
139+["exit code", { exitCode: 7 }, "failed", 7],
140+["success flag", { success: false }, "failed", 1],
141+["ok flag", { ok: false }, "failed", 1],
142+["timeout flag", { timedOut: true }, "failed", 1],
143+["error value", { error: "command failed" }, "failed", 1],
144+["successful exit code", { exitCode: 0 }, "completed", 0],
145+["successful flag", { success: true }, "completed", 0],
146+])(
147+"adds a canonical status while preserving bash details with a %s",
148+async (_label, existingDetails, status, exitCode) => {
149+let received:
150+| {
151+details: unknown;
152+}
153+| undefined;
154+tokenjuiceFactory.mockImplementationOnce(
155+(api: { on: (event: string, handler: unknown) => void }) => {
156+api.on("tool_result", async (event: typeof received) => {
157+received = event;
158+});
159+},
160+);
161+162+const middleware = createTokenjuiceAgentToolResultMiddleware();
163+await middleware(
164+{
165+toolCallId: "tool-call-tokenjuice-bash-terminal",
166+toolName: "bash",
167+args: { command: "exit 7", workdir: "/tmp" },
168+result: {
169+content: [{ type: "text", text: "failed\n" }],
170+details: existingDetails,
171+},
172+isError: false,
173+},
174+{ runtime: "openclaw" },
175+);
176+177+expect(received?.details).toMatchObject({
178+ ...existingDetails,
179+ status,
180+ exitCode,
181+});
182+},
183+);
184+185+it("normalizes bash results without details before passing them to tokenjuice", async () => {
186+let received:
187+| {
188+toolName: string;
189+input: Record<string, unknown>;
190+content: unknown;
191+details: unknown;
192+isError?: boolean;
193+}
194+| undefined;
195+tokenjuiceFactory.mockImplementationOnce(
196+(api: { on: (event: string, handler: unknown) => void }) => {
197+api.on("tool_result", async (event: typeof received) => {
198+received = event;
199+return { content: [{ type: "text", text: "compacted" }] };
200+});
201+},
202+);
203+204+const middleware = createTokenjuiceAgentToolResultMiddleware();
205+const result = await middleware(
206+{
207+toolCallId: "tool-call-tokenjuice-bash",
208+toolName: "bash",
209+args: { command: "printf 'hello\\n'", workdir: "/tmp/openclaw-tokenjuice-test" },
210+result: { content: [{ type: "text", text: "hello\n" }], details: undefined },
211+isError: false,
212+},
213+{ runtime: "openclaw" },
214+);
215+216+expect(received?.toolName).toBe("bash");
217+expect(received?.details).toMatchObject({
218+status: "completed",
219+aggregated: "hello\n",
220+exitCode: 0,
221+});
222+expect(received?.details).not.toHaveProperty("cwd");
223+expect(result?.result.content).toEqual([{ type: "text", text: "compacted" }]);
224+});
56225});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。