|
| 1 | +import fs from "node:fs/promises"; |
| 2 | +import os from "node:os"; |
| 3 | +import path from "node:path"; |
1 | 4 | import { beforeEach, describe, expect, it, vi } from "vitest"; |
2 | 5 | import type { SkillCommandSpec } from "../../agents/skills.js"; |
3 | 6 | import type { SessionEntry } from "../../config/sessions.js"; |
@@ -47,6 +50,16 @@ const createTypingController = (): TypingController => ({ |
47 | 50 | cleanup: vi.fn(), |
48 | 51 | }); |
49 | 52 |
|
| 53 | +async function writeSessionStore( |
| 54 | + storeTemplate: string, |
| 55 | + agentId: string, |
| 56 | + entries: Record<string, unknown>, |
| 57 | +) { |
| 58 | + const storePath = storeTemplate.replaceAll("{agentId}", agentId); |
| 59 | + await fs.mkdir(path.dirname(storePath), { recursive: true }); |
| 60 | + await fs.writeFile(storePath, JSON.stringify(entries, null, 2), "utf-8"); |
| 61 | +} |
| 62 | + |
50 | 63 | const createHandleInlineActionsInput = (params: { |
51 | 64 | ctx: ReturnType<typeof buildTestCtx>; |
52 | 65 | typing: TypingController; |
@@ -807,4 +820,144 @@ describe("handleInlineActions", () => { |
807 | 820 | expect(messageExecute).not.toHaveBeenCalled(); |
808 | 821 | expect(sessionsExecute).not.toHaveBeenCalled(); |
809 | 822 | }); |
| 823 | + |
| 824 | + it("applies subagent policy to ACP envelope inline dispatch sessions", async () => { |
| 825 | + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-inline-acp-policy-")); |
| 826 | + try { |
| 827 | + const storeTemplate = path.join(tmpDir, "sessions-{agentId}.json"); |
| 828 | + await writeSessionStore(storeTemplate, "main", { |
| 829 | + "agent:main:acp:leaf": { |
| 830 | + sessionId: "session-acp-leaf", |
| 831 | + updatedAt: Date.now(), |
| 832 | + spawnedBy: "agent:main:subagent:parent", |
| 833 | + spawnDepth: 2, |
| 834 | + subagentRole: "leaf", |
| 835 | + subagentControlScope: "none", |
| 836 | + }, |
| 837 | + }); |
| 838 | + |
| 839 | + const typing = createTypingController(); |
| 840 | + const toolExecute = vi.fn(async () => ({ content: "spawned" })); |
| 841 | + createOpenClawToolsMock.mockReturnValue([ |
| 842 | + { |
| 843 | + name: "sessions_spawn", |
| 844 | + execute: toolExecute, |
| 845 | + }, |
| 846 | + ]); |
| 847 | + |
| 848 | + const ctx = buildTestCtx({ |
| 849 | + Body: "/spawn_subagent investigate", |
| 850 | + CommandBody: "/spawn_subagent investigate", |
| 851 | + }); |
| 852 | + const skillCommands: SkillCommandSpec[] = [ |
| 853 | + { |
| 854 | + name: "spawn_subagent", |
| 855 | + skillName: "spawn-subagent", |
| 856 | + description: "Spawn a subagent", |
| 857 | + dispatch: { |
| 858 | + kind: "tool", |
| 859 | + toolName: "sessions_spawn", |
| 860 | + argMode: "raw", |
| 861 | + }, |
| 862 | + sourceFilePath: "/tmp/plugin/commands/spawn-subagent.md", |
| 863 | + }, |
| 864 | + ]; |
| 865 | + |
| 866 | + const result = await handleInlineActions( |
| 867 | + createHandleInlineActionsInput({ |
| 868 | + ctx, |
| 869 | + typing, |
| 870 | + cleanedBody: "/spawn_subagent investigate", |
| 871 | + command: { |
| 872 | + isAuthorizedSender: true, |
| 873 | + senderId: "sender-1", |
| 874 | + senderIsOwner: true, |
| 875 | + abortKey: "sender-1", |
| 876 | + rawBodyNormalized: "/spawn_subagent investigate", |
| 877 | + commandBodyNormalized: "/spawn_subagent investigate", |
| 878 | + }, |
| 879 | + overrides: { |
| 880 | + cfg: { |
| 881 | + commands: { text: true }, |
| 882 | + session: { store: storeTemplate }, |
| 883 | + agents: { defaults: { subagents: { maxSpawnDepth: 2 } } }, |
| 884 | + }, |
| 885 | + sessionKey: "agent:main:acp:leaf", |
| 886 | + allowTextCommands: true, |
| 887 | + skillCommands, |
| 888 | + }, |
| 889 | + }), |
| 890 | + ); |
| 891 | + |
| 892 | + expect(result).toEqual({ |
| 893 | + kind: "reply", |
| 894 | + reply: { text: "❌ Tool not available: sessions_spawn" }, |
| 895 | + }); |
| 896 | + expect(toolExecute).not.toHaveBeenCalled(); |
| 897 | + } finally { |
| 898 | + await fs.rm(tmpDir, { recursive: true, force: true }); |
| 899 | + } |
| 900 | + }); |
| 901 | + |
| 902 | + it("passes sandboxed runtime state into inline tool construction", async () => { |
| 903 | + const typing = createTypingController(); |
| 904 | + const toolExecute = vi.fn(async () => ({ content: "listed" })); |
| 905 | + createOpenClawToolsMock.mockReturnValue([ |
| 906 | + { |
| 907 | + name: "sessions_list", |
| 908 | + execute: toolExecute, |
| 909 | + }, |
| 910 | + ]); |
| 911 | + |
| 912 | + const ctx = buildTestCtx({ |
| 913 | + Body: "/list_sessions now", |
| 914 | + CommandBody: "/list_sessions now", |
| 915 | + }); |
| 916 | + const skillCommands: SkillCommandSpec[] = [ |
| 917 | + { |
| 918 | + name: "list_sessions", |
| 919 | + skillName: "list-sessions", |
| 920 | + description: "List sessions", |
| 921 | + dispatch: { |
| 922 | + kind: "tool", |
| 923 | + toolName: "sessions_list", |
| 924 | + argMode: "raw", |
| 925 | + }, |
| 926 | + sourceFilePath: "/tmp/plugin/commands/list-sessions.md", |
| 927 | + }, |
| 928 | + ]; |
| 929 | + |
| 930 | + const result = await handleInlineActions( |
| 931 | + createHandleInlineActionsInput({ |
| 932 | + ctx, |
| 933 | + typing, |
| 934 | + cleanedBody: "/list_sessions now", |
| 935 | + command: { |
| 936 | + isAuthorizedSender: true, |
| 937 | + senderId: "sender-1", |
| 938 | + senderIsOwner: true, |
| 939 | + abortKey: "sender-1", |
| 940 | + rawBodyNormalized: "/list_sessions now", |
| 941 | + commandBodyNormalized: "/list_sessions now", |
| 942 | + }, |
| 943 | + overrides: { |
| 944 | + cfg: { |
| 945 | + commands: { text: true }, |
| 946 | + agents: { defaults: { sandbox: { mode: "all" } } }, |
| 947 | + }, |
| 948 | + sessionKey: "agent:main:thread", |
| 949 | + allowTextCommands: true, |
| 950 | + skillCommands, |
| 951 | + }, |
| 952 | + }), |
| 953 | + ); |
| 954 | + |
| 955 | + expect(result).toEqual({ kind: "reply", reply: { text: "✅ Done." } }); |
| 956 | + expect(createOpenClawToolsMock).toHaveBeenCalledWith( |
| 957 | + expect.objectContaining({ |
| 958 | + sandboxed: true, |
| 959 | + }), |
| 960 | + ); |
| 961 | + expect(toolExecute).toHaveBeenCalled(); |
| 962 | + }); |
810 | 963 | }); |
0 commit comments