File size: 4,329 Bytes
34810d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import { access } from "node:fs/promises";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { WidgetHtmlInputError } from "../plugin-sdk/widget-html.js";
import type { WidgetPresenter } from "../plugins/plugin-registration.types.js";
import { resetPluginRuntimeStateForTest } from "../plugins/runtime.js";
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { resolveCanvasDocumentsDir } from "./documents.js";
import { registerTestWidgetContentKind } from "./widget-tool.content-kinds.test-support.js";
import { createShowWidgetTool } from "./widget-tool.js";
import { createBoardPutCaller } from "./widget-tool.test-support.js";

const tempDirs = useAutoCleanupTempDirTracker(afterEach);

beforeEach(() => resetPluginRuntimeStateForTest());
afterEach(() => {
  closeOpenClawAgentDatabasesForTest();
  closeOpenClawStateDatabaseForTest();
  resetPluginRuntimeStateForTest();
});

describe("show_widget script syntax gate", () => {
  it.each(["inline", "pinned", "current_channel", "node_panel"] as const)(
    "rejects broken scripts before any side effect on %s",
    async (route) => {
      const stateDir = tempDirs.make("openclaw-widget-syntax-");
      const { mock: gateway, callGateway } = createBoardPutCaller();
      const present = vi.fn<WidgetPresenter["present"]>();
      const availability = vi.fn<WidgetPresenter["availability"]>();
      const presenters: WidgetPresenter[] =
        route === "current_channel"
          ? [
              {
                target: "current_channel",
                description: "Test channel",
                capabilities: { sourceKinds: ["html"] },
                match: () => true,
                availability,
                present,
              },
            ]
          : [{ target: "node_panel", description: "Test panel", availability, present }];
      const tool = createShowWidgetTool({
        stateDir,
        sessionId: "syntax",
        agentSessionKey: "agent:main:syntax",
        callGateway,
        presenters,
      });
      const result = tool.execute("broken", {
        title: "Broken widget",
        widget_code: "<p>Widget</p>\n<script>const a='x\n'+b;</script>",
        pin: route === "pinned",
        ...(route === "node_panel" ? { presentation: { target: "node_panel" } } : {}),
      });
      await expect(result).rejects.toThrow(WidgetHtmlInputError);
      await expect(result).rejects.toThrow(
        "widget_code has a JavaScript syntax error in inline script 1 at line 2, column 16: Unterminated string constant. Offending line: <script>const a='x. Fix the script and call show_widget again.",
      );
      expect(gateway).not.toHaveBeenCalled();
      expect(present).not.toHaveBeenCalled();
      expect(availability).not.toHaveBeenCalled();
      await expect(access(resolveCanvasDocumentsDir(stateDir))).rejects.toThrow();
    },
  );

  it("leaves registered source validation to the content kind", async () => {
    registerTestWidgetContentKind("diagram", () => "<p>Rendered diagram</p>");
    const stateDir = tempDirs.make("openclaw-widget-syntax-");
    const tool = createShowWidgetTool({ stateDir, sessionId: "registered-syntax" });
    const result = await tool.execute("registered", {
      title: "Diagram",
      kind: "diagram",
      widget_code: "diagram:<script>const =</script>",
    });
    const text = result.content.find((item) => item.type === "text")?.text;
    expect(JSON.parse(text ?? "null")).toMatchObject({ kind: "canvas" });
  });

  it("still hosts a valid script as a canvas widget", async () => {
    const stateDir = tempDirs.make("openclaw-widget-syntax-");
    const tool = createShowWidgetTool({ stateDir, sessionId: "valid-syntax" });
    const result = await tool.execute("valid", {
      title: "Working widget",
      widget_code: "<script>document.body.textContent = 'Working';</script>",
    });
    const text = result.content.find((item) => item.type === "text")?.text;
    expect(JSON.parse(text ?? "null")).toMatchObject({ kind: "canvas" });
    await expect(access(resolveCanvasDocumentsDir(stateDir))).resolves.toBeUndefined();
  });
});