Download src/link-understanding/runner.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/link-understanding/runner.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/link-understanding/runner.test.ts
-
curl -L -o runner.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/link-understanding/runner.test.ts
12.7 kB
| // Link-understanding runner tests cover guarded fetches, command execution, scoping, and template behavior. | |
| import { beforeEach, describe, expect, it, vi } from "vitest"; | |
| import type { MsgContext } from "../auto-reply/templating.js"; | |
| import type { OpenClawConfig } from "../config/types.openclaw.js"; | |
| import type { LinkModelConfig } from "../config/types.tools.js"; | |
| import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; | |
| import { runCommandWithTimeout } from "../process/exec.js"; | |
| import { applyLinkUnderstanding } from "./apply.js"; | |
| import { runLinkUnderstanding } from "./runner.js"; | |
| const mocks = vi.hoisted(() => ({ | |
| fetchWithSsrFGuard: vi.fn(), | |
| runCommandWithTimeout: vi.fn(), | |
| })); | |
| vi.mock("../infra/net/fetch-guard.js", async () => { | |
| const actual = await vi.importActual<typeof import("../infra/net/fetch-guard.js")>( | |
| "../infra/net/fetch-guard.js", | |
| ); | |
| return { | |
| ...actual, | |
| fetchWithSsrFGuard: mocks.fetchWithSsrFGuard, | |
| }; | |
| }); | |
| vi.mock("../process/exec.js", async () => { | |
| const actual = await vi.importActual<typeof import("../process/exec.js")>("../process/exec.js"); | |
| return { | |
| ...actual, | |
| runCommandWithTimeout: mocks.runCommandWithTimeout, | |
| }; | |
| }); | |
| function cfg(entry: LinkModelConfig) { | |
| return { | |
| tools: { | |
| links: { | |
| enabled: true, | |
| models: [entry], | |
| }, | |
| }, | |
| } as OpenClawConfig; | |
| } | |
| function ctx(body: string): MsgContext { | |
| return { Body: body } as MsgContext; | |
| } | |
| function mockGuardedFetch(body = "guarded content", finalUrl = "https://example.com/final") { | |
| const release = vi.fn(async () => {}); | |
| mocks.fetchWithSsrFGuard.mockResolvedValueOnce({ | |
| response: new Response(body), | |
| finalUrl, | |
| release, | |
| }); | |
| return release; | |
| } | |
| function mockCommand(stdout = "summary") { | |
| mocks.runCommandWithTimeout.mockResolvedValueOnce({ | |
| code: 0, | |
| killed: false, | |
| signal: null, | |
| stderr: "", | |
| stdout, | |
| termination: "exit", | |
| }); | |
| } | |
| describe("runLinkUnderstanding", () => { | |
| beforeEach(() => { | |
| mocks.fetchWithSsrFGuard.mockReset(); | |
| mocks.runCommandWithTimeout.mockReset(); | |
| }); | |
| it("applies shared media scope rules to link message context", async () => { | |
| const result = await runLinkUnderstanding({ | |
| cfg: { | |
| tools: { | |
| links: { | |
| enabled: true, | |
| scope: { | |
| default: "allow", | |
| rules: [ | |
| { | |
| action: "deny", | |
| match: { channel: "slack", chatType: "channel", keyPrefix: "agent:main:" }, | |
| }, | |
| ], | |
| }, | |
| models: [{ type: "cli", command: "summarize" }], | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| ctx: { | |
| Body: "see https://example.com/page", | |
| ChatType: "channel", | |
| Provider: "discord", | |
| SessionKey: "agent:main:slack:channel:C123", | |
| Surface: "slack", | |
| } as MsgContext, | |
| }); | |
| expect(result).toEqual([]); | |
| expect(fetchWithSsrFGuard).not.toHaveBeenCalled(); | |
| expect(runCommandWithTimeout).not.toHaveBeenCalled(); | |
| }); | |
| it("fetches links through the SSRF guard before passing content to CLI stdin", async () => { | |
| const controller = new AbortController(); | |
| const release = mockGuardedFetch("page body", "https://example.com/final"); | |
| mockCommand("summarized page"); | |
| const result = await runLinkUnderstanding({ | |
| cfg: cfg({ type: "cli", command: "summarize", args: ["--source", "{{LinkUrl}}"] }), | |
| ctx: ctx("see https://example.com/page"), | |
| signal: controller.signal, | |
| }); | |
| expect(result).toEqual(["summarized page"]); | |
| expect(fetchWithSsrFGuard).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| auditContext: "link-understanding", | |
| mode: "strict", | |
| url: "https://example.com/page", | |
| signal: controller.signal, | |
| }), | |
| ); | |
| expect(runCommandWithTimeout).toHaveBeenCalledWith(["summarize", "--source"], { | |
| env: { | |
| OPENCLAW_LINK_FINAL_URL: "https://example.com/final", | |
| OPENCLAW_LINK_URL: "https://example.com/page", | |
| }, | |
| input: "page body", | |
| signal: controller.signal, | |
| killProcessTree: true, | |
| timeoutMs: 30000, | |
| }); | |
| expect(release).toHaveBeenCalledOnce(); | |
| }); | |
| it.each([ | |
| { | |
| agentText: "prepared transcript", | |
| BodyForAgent: "stale alias", | |
| expected: "prepared transcript", | |
| }, | |
| { BodyForAgent: "SDK transcript", expected: "SDK transcript" }, | |
| { agentText: "", BodyForAgent: "stale alias", expected: "" }, | |
| { expected: "transport envelope" }, | |
| ])( | |
| "preserves the prepared model text $expected through link enrichment", | |
| async ({ expected, ...text }) => { | |
| mockGuardedFetch("page body"); | |
| mockCommand("summarized page"); | |
| const context: MsgContext = { | |
| Body: "transport envelope", | |
| RawBody: "see https://example.com/page", | |
| CommandBody: "see https://example.com/page", | |
| ...text, | |
| }; | |
| await applyLinkUnderstanding({ | |
| cfg: cfg({ type: "cli", command: "summarize" }), | |
| ctx: context, | |
| }); | |
| expect(context.Body).toBe("transport envelope\n\nsummarized page"); | |
| expect(context.agentText).toBe( | |
| expected ? `${expected}\n\nsummarized page` : "summarized page", | |
| ); | |
| expect(context.BodyForAgent).toBe(context.agentText); | |
| expect(context).toMatchObject({ | |
| RawBody: "see https://example.com/page", | |
| CommandBody: "see https://example.com/page", | |
| rawText: "see https://example.com/page", | |
| commandText: "see https://example.com/page", | |
| LinkUnderstanding: ["summarized page"], | |
| }); | |
| }, | |
| ); | |
| it("leaves context untouched when guarded link fetch produces no output", async () => { | |
| mocks.fetchWithSsrFGuard.mockRejectedValueOnce(new Error("blocked by DNS policy")); | |
| const context: MsgContext = { | |
| Body: "transport envelope", | |
| agentText: "prepared transcript", | |
| BodyForAgent: "SDK transcript", | |
| CommandBody: "see https://example.com/page", | |
| }; | |
| const before = structuredClone(context); | |
| await applyLinkUnderstanding({ | |
| cfg: cfg({ type: "cli", command: "summarize" }), | |
| ctx: context, | |
| }); | |
| expect(context).toEqual(before); | |
| expect(runCommandWithTimeout).not.toHaveBeenCalled(); | |
| }); | |
| it("does not run configured curl fetchers against attacker-controlled URLs", async () => { | |
| mockGuardedFetch("guarded page body"); | |
| const result = await runLinkUnderstanding({ | |
| cfg: cfg({ | |
| type: "cli", | |
| command: "curl", | |
| args: ["-s", "-L", "{{LinkUrl}}"], | |
| }), | |
| ctx: ctx("see http://192.168.1.64.nip.io:8888/aws-iam-credentials"), | |
| }); | |
| expect(result).toEqual(["guarded page body"]); | |
| expect(fetchWithSsrFGuard).toHaveBeenCalledOnce(); | |
| expect(runCommandWithTimeout).not.toHaveBeenCalled(); | |
| }); | |
| it.each([ | |
| [ | |
| "skips links rejected by the guarded fetch DNS policy", | |
| "http://169.254.169.254.nip.io/latest/meta-data/", | |
| "Blocked: resolves to private/internal/special-use IP address", | |
| ], | |
| [ | |
| "skips links rejected by the guarded fetch redirect policy", | |
| "https://public.example/redirect-to-metadata", | |
| "redirect target resolves to private network", | |
| ], | |
| ])("%s", async (_name, url, errorMessage) => { | |
| mocks.fetchWithSsrFGuard.mockRejectedValueOnce(new Error(errorMessage)); | |
| const result = await runLinkUnderstanding({ | |
| cfg: cfg({ type: "cli", command: "summarize" }), | |
| ctx: ctx(`see ${url}`), | |
| }); | |
| expect(result).toEqual([]); | |
| expect(runCommandWithTimeout).not.toHaveBeenCalled(); | |
| }); | |
| it.each([ | |
| [ | |
| "uses the global link-tools timeout for fetches when configured", | |
| { timeoutSeconds: 15 }, | |
| 15000, | |
| ], | |
| ["falls back to the largest model timeout for fetches when no global timeout is set", {}, 9000], | |
| ] as const)("%s", async (_name, timeoutConfig, timeoutMs) => { | |
| mockGuardedFetch("page body", "https://example.com/final"); | |
| mockCommand("summarized page"); | |
| await runLinkUnderstanding({ | |
| cfg: { | |
| tools: { | |
| links: { | |
| enabled: true, | |
| ...timeoutConfig, | |
| models: [ | |
| { type: "cli", command: "summarize-fast", timeoutSeconds: 1 }, | |
| { type: "cli", command: "summarize-slow", timeoutSeconds: 9 }, | |
| ], | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| ctx: ctx("see https://example.com/page"), | |
| }); | |
| expect(fetchWithSsrFGuard).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| timeoutMs, | |
| url: "https://example.com/page", | |
| }), | |
| ); | |
| }); | |
| it("skips pre-aborted work without changing inbound context", async () => { | |
| const controller = new AbortController(); | |
| controller.abort(); | |
| const context = ctx("see https://example.com/page"); | |
| const original = { ...context }; | |
| await expect( | |
| applyLinkUnderstanding({ | |
| ctx: context, | |
| cfg: cfg({ type: "cli", command: "summarize" }), | |
| signal: controller.signal, | |
| }), | |
| ).resolves.toBeUndefined(); | |
| expect(fetchWithSsrFGuard).not.toHaveBeenCalled(); | |
| expect(runCommandWithTimeout).not.toHaveBeenCalled(); | |
| expect(context).toEqual(original); | |
| }); | |
| it("passes the signal to each CLI entry during ordinary fallback", async () => { | |
| const controller = new AbortController(); | |
| mockGuardedFetch("page body", "https://example.com/final"); | |
| mockCommand(""); | |
| mockCommand("second summary"); | |
| const result = await runLinkUnderstanding({ | |
| cfg: { | |
| tools: { | |
| links: { | |
| models: [{ command: "summarize-a" }, { command: "summarize-b" }], | |
| }, | |
| }, | |
| }, | |
| ctx: ctx("see https://example.com/page"), | |
| signal: controller.signal, | |
| }); | |
| expect(result).toEqual(["second summary"]); | |
| for (const [index, command] of ["summarize-a", "summarize-b"].entries()) { | |
| expect(runCommandWithTimeout).toHaveBeenNthCalledWith( | |
| index + 1, | |
| [command], | |
| expect.objectContaining({ signal: controller.signal }), | |
| ); | |
| } | |
| }); | |
| it.each([ | |
| { outcome: "successful exit", code: 0, stdout: "late summary", termination: "exit" }, | |
| { outcome: "failed exit", code: 1, stdout: "", termination: "exit" }, | |
| { outcome: "signal termination", code: null, stdout: "", termination: "signal" }, | |
| ])("cancellation overrides $outcome without fallback or context changes", async (result) => { | |
| const controller = new AbortController(); | |
| const reason = new Error("reply canceled"); | |
| mockGuardedFetch("first body", "https://example.com/first"); | |
| mocks.runCommandWithTimeout.mockImplementationOnce(async () => { | |
| controller.abort(reason); | |
| return { ...result, killed: false, signal: null, stderr: "" }; | |
| }); | |
| const context = ctx("see https://example.com/first and https://example.com/second"); | |
| const original = { ...context }; | |
| await expect( | |
| applyLinkUnderstanding({ | |
| cfg: { | |
| tools: { | |
| links: { | |
| models: [{ command: "summarize-a" }, { command: "summarize-b" }], | |
| }, | |
| }, | |
| }, | |
| ctx: context, | |
| signal: controller.signal, | |
| }), | |
| ).rejects.toMatchObject({ name: "AbortError", cause: reason }); | |
| expect(fetchWithSsrFGuard).toHaveBeenCalledOnce(); | |
| expect(runCommandWithTimeout).toHaveBeenCalledOnce(); | |
| expect(context).toEqual(original); | |
| }); | |
| it("rethrows a guarded fetch AbortError without starting the CLI", async () => { | |
| const abortError = Object.assign(new Error("This operation was aborted"), { | |
| name: "AbortError", | |
| }); | |
| mocks.fetchWithSsrFGuard.mockRejectedValueOnce(abortError); | |
| await expect( | |
| runLinkUnderstanding({ | |
| cfg: cfg({ type: "cli", command: "summarize" }), | |
| ctx: ctx("see https://example.com/page"), | |
| signal: new AbortController().signal, | |
| }), | |
| ).rejects.toBe(abortError); | |
| expect(runCommandWithTimeout).not.toHaveBeenCalled(); | |
| }); | |
| it("gives caller cancellation precedence over an ordinary fetch error", async () => { | |
| const controller = new AbortController(); | |
| const reason = new Error("reply canceled"); | |
| mocks.fetchWithSsrFGuard.mockImplementationOnce(async () => { | |
| controller.abort(reason); | |
| throw new Error("connect ECONNREFUSED"); | |
| }); | |
| await expect( | |
| runLinkUnderstanding({ | |
| cfg: cfg({ type: "cli", command: "summarize" }), | |
| ctx: ctx("see https://example.com/page"), | |
| signal: controller.signal, | |
| }), | |
| ).rejects.toMatchObject({ name: "AbortError", cause: reason }); | |
| expect(runCommandWithTimeout).not.toHaveBeenCalled(); | |
| }); | |
| }); | |