@goondan/openharness-base 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/package.json +14 -9
  2. package/dist/extensions/compaction.d.ts +0 -12
  3. package/dist/extensions/compaction.d.ts.map +0 -1
  4. package/dist/extensions/compaction.js +0 -159
  5. package/dist/extensions/compaction.js.map +0 -1
  6. package/dist/extensions/context-message.d.ts +0 -9
  7. package/dist/extensions/context-message.d.ts.map +0 -1
  8. package/dist/extensions/context-message.js +0 -446
  9. package/dist/extensions/context-message.js.map +0 -1
  10. package/dist/extensions/index.d.ts +0 -13
  11. package/dist/extensions/index.d.ts.map +0 -1
  12. package/dist/extensions/index.js +0 -7
  13. package/dist/extensions/index.js.map +0 -1
  14. package/dist/extensions/logging.d.ts +0 -11
  15. package/dist/extensions/logging.d.ts.map +0 -1
  16. package/dist/extensions/logging.js +0 -140
  17. package/dist/extensions/logging.js.map +0 -1
  18. package/dist/extensions/message-window.d.ts +0 -7
  19. package/dist/extensions/message-window.d.ts.map +0 -1
  20. package/dist/extensions/message-window.js +0 -60
  21. package/dist/extensions/message-window.js.map +0 -1
  22. package/dist/extensions/required-tools-guard.d.ts +0 -9
  23. package/dist/extensions/required-tools-guard.d.ts.map +0 -1
  24. package/dist/extensions/required-tools-guard.js +0 -74
  25. package/dist/extensions/required-tools-guard.js.map +0 -1
  26. package/dist/extensions/tool-search.d.ts +0 -10
  27. package/dist/extensions/tool-search.d.ts.map +0 -1
  28. package/dist/extensions/tool-search.js +0 -198
  29. package/dist/extensions/tool-search.js.map +0 -1
  30. package/dist/harness.yaml +0 -503
  31. package/dist/index.d.ts.map +0 -1
  32. package/dist/index.js.map +0 -1
  33. package/dist/manifests/base.d.ts +0 -8
  34. package/dist/manifests/base.d.ts.map +0 -1
  35. package/dist/manifests/base.js +0 -352
  36. package/dist/manifests/base.js.map +0 -1
  37. package/dist/manifests/index.d.ts +0 -3
  38. package/dist/manifests/index.d.ts.map +0 -1
  39. package/dist/manifests/index.js +0 -2
  40. package/dist/manifests/index.js.map +0 -1
  41. package/dist/tools/bash.d.ts +0 -8
  42. package/dist/tools/bash.d.ts.map +0 -1
  43. package/dist/tools/bash.js +0 -119
  44. package/dist/tools/bash.js.map +0 -1
  45. package/dist/tools/file-system.d.ts +0 -12
  46. package/dist/tools/file-system.d.ts.map +0 -1
  47. package/dist/tools/file-system.js +0 -117
  48. package/dist/tools/file-system.js.map +0 -1
  49. package/dist/tools/http-fetch.d.ts +0 -8
  50. package/dist/tools/http-fetch.d.ts.map +0 -1
  51. package/dist/tools/http-fetch.js +0 -149
  52. package/dist/tools/http-fetch.js.map +0 -1
  53. package/dist/tools/index.d.ts +0 -7
  54. package/dist/tools/index.d.ts.map +0 -1
  55. package/dist/tools/index.js +0 -7
  56. package/dist/tools/index.js.map +0 -1
  57. package/dist/tools/json-query.d.ts +0 -12
  58. package/dist/tools/json-query.d.ts.map +0 -1
  59. package/dist/tools/json-query.js +0 -176
  60. package/dist/tools/json-query.js.map +0 -1
  61. package/dist/tools/text-transform.d.ts +0 -16
  62. package/dist/tools/text-transform.d.ts.map +0 -1
  63. package/dist/tools/text-transform.js +0 -127
  64. package/dist/tools/text-transform.js.map +0 -1
  65. package/dist/tools/wait.d.ts +0 -6
  66. package/dist/tools/wait.d.ts.map +0 -1
  67. package/dist/tools/wait.js +0 -32
  68. package/dist/tools/wait.js.map +0 -1
  69. package/dist/types.d.ts +0 -4
  70. package/dist/types.d.ts.map +0 -1
  71. package/dist/types.js +0 -6
  72. package/dist/types.js.map +0 -1
  73. package/dist/utils.d.ts +0 -17
  74. package/dist/utils.d.ts.map +0 -1
  75. package/dist/utils.js +0 -159
  76. package/dist/utils.js.map +0 -1
  77. package/src/__tests__/basic-system-prompt.test.ts +0 -186
  78. package/src/__tests__/compaction-summarize.test.ts +0 -282
  79. package/src/__tests__/logging.test.ts +0 -200
  80. package/src/__tests__/message-window.test.ts +0 -194
  81. package/src/__tests__/required-tools-guard.test.ts +0 -207
  82. package/src/__tests__/tool-search.test.ts +0 -187
  83. package/src/__tests__/tools.test.ts +0 -332
  84. package/src/extensions/basic-system-prompt.ts +0 -48
  85. package/src/extensions/compaction-summarize.ts +0 -104
  86. package/src/extensions/logging.ts +0 -42
  87. package/src/extensions/message-window.ts +0 -23
  88. package/src/extensions/required-tools-guard.ts +0 -24
  89. package/src/extensions/tool-search.ts +0 -38
  90. package/src/index.ts +0 -16
  91. package/src/tools/bash.ts +0 -38
  92. package/src/tools/file-system.ts +0 -83
  93. package/src/tools/http-fetch.ts +0 -64
  94. package/src/tools/json-query.ts +0 -71
  95. package/src/tools/text-transform.ts +0 -59
  96. package/src/tools/wait.ts +0 -46
  97. package/tsconfig.json +0 -8
  98. package/vitest.config.ts +0 -7
@@ -1,200 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { Logging } from "../extensions/logging.js";
3
- import type { ExtensionApi, ConversationState } from "@goondan/openharness-types";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Helpers
7
- // ---------------------------------------------------------------------------
8
-
9
- function makeMockConversationState(): ConversationState {
10
- return {
11
- messages: [],
12
- events: [],
13
- emit: vi.fn(),
14
- restore: vi.fn(),
15
- };
16
- }
17
-
18
- function makeMockApi(conversation: ConversationState): {
19
- api: ExtensionApi;
20
- eventListeners: Map<string, Array<(payload: unknown) => void>>;
21
- } {
22
- const eventListeners = new Map<string, Array<(payload: unknown) => void>>();
23
-
24
- const api: ExtensionApi = {
25
- pipeline: {
26
- register: vi.fn() as unknown as ExtensionApi["pipeline"]["register"],
27
- },
28
- tools: {
29
- register: vi.fn(),
30
- remove: vi.fn(),
31
- list: vi.fn(() => []),
32
- },
33
- on: vi.fn((event: string, listener: (payload: unknown) => void) => {
34
- if (!eventListeners.has(event)) {
35
- eventListeners.set(event, []);
36
- }
37
- eventListeners.get(event)!.push(listener);
38
- }),
39
- conversation,
40
- runtime: {
41
- agent: {
42
- name: "test-agent",
43
- model: { provider: "openai", model: "gpt-4o" },
44
- extensions: [],
45
- tools: [],
46
- },
47
- agents: {},
48
- connections: {},
49
- },
50
- };
51
-
52
- return { api, eventListeners };
53
- }
54
-
55
- function emit(
56
- eventListeners: Map<string, Array<(payload: unknown) => void>>,
57
- event: string,
58
- payload: unknown,
59
- ) {
60
- eventListeners.get(event)?.forEach((l) => l(payload));
61
- }
62
-
63
- // ---------------------------------------------------------------------------
64
- // Tests
65
- // ---------------------------------------------------------------------------
66
-
67
- describe("Logging", () => {
68
- it("creates an Extension with name 'logging'", () => {
69
- const ext = Logging();
70
- expect(ext.name).toBe("logging");
71
- });
72
-
73
- it("subscribes to all core events on register", () => {
74
- const conversation = makeMockConversationState();
75
- const { api } = makeMockApi(conversation);
76
-
77
- const ext = Logging();
78
- ext.register(api);
79
-
80
- expect(api.on).toHaveBeenCalledWith("turn.start", expect.any(Function));
81
- expect(api.on).toHaveBeenCalledWith("turn.done", expect.any(Function));
82
- expect(api.on).toHaveBeenCalledWith("turn.error", expect.any(Function));
83
- expect(api.on).toHaveBeenCalledWith("step.start", expect.any(Function));
84
- expect(api.on).toHaveBeenCalledWith("step.done", expect.any(Function));
85
- expect(api.on).toHaveBeenCalledWith("tool.start", expect.any(Function));
86
- expect(api.on).toHaveBeenCalledWith("tool.done", expect.any(Function));
87
- });
88
-
89
- it("logs turn.start event with custom logger", () => {
90
- const conversation = makeMockConversationState();
91
- const { api, eventListeners } = makeMockApi(conversation);
92
- const logger = vi.fn();
93
-
94
- const ext = Logging({ logger });
95
- ext.register(api);
96
-
97
- emit(eventListeners, "turn.start", { turnId: "t1" });
98
-
99
- expect(logger).toHaveBeenCalledOnce();
100
- expect(logger.mock.calls[0][0]).toContain("turn.start");
101
- });
102
-
103
- it("logs turn.done event", () => {
104
- const conversation = makeMockConversationState();
105
- const { api, eventListeners } = makeMockApi(conversation);
106
- const logger = vi.fn();
107
-
108
- const ext = Logging({ logger });
109
- ext.register(api);
110
-
111
- emit(eventListeners, "turn.done", { turnId: "t1", status: "completed" });
112
-
113
- expect(logger).toHaveBeenCalledOnce();
114
- expect(logger.mock.calls[0][0]).toContain("turn.done");
115
- });
116
-
117
- it("logs turn.error event", () => {
118
- const conversation = makeMockConversationState();
119
- const { api, eventListeners } = makeMockApi(conversation);
120
- const logger = vi.fn();
121
-
122
- const ext = Logging({ logger });
123
- ext.register(api);
124
-
125
- emit(eventListeners, "turn.error", { error: "oops" });
126
-
127
- expect(logger).toHaveBeenCalledOnce();
128
- expect(logger.mock.calls[0][0]).toContain("turn.error");
129
- });
130
-
131
- it("uses console.log as default logger", () => {
132
- const conversation = makeMockConversationState();
133
- const { api, eventListeners } = makeMockApi(conversation);
134
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
135
-
136
- const ext = Logging();
137
- ext.register(api);
138
-
139
- emit(eventListeners, "turn.start", { turnId: "t1" });
140
-
141
- expect(consoleSpy).toHaveBeenCalledOnce();
142
- consoleSpy.mockRestore();
143
- });
144
-
145
- it("logs step.start event with custom logger", () => {
146
- const conversation = makeMockConversationState();
147
- const { api, eventListeners } = makeMockApi(conversation);
148
- const logger = vi.fn();
149
-
150
- const ext = Logging({ logger });
151
- ext.register(api);
152
-
153
- emit(eventListeners, "step.start", { stepIndex: 0 });
154
-
155
- expect(logger).toHaveBeenCalledOnce();
156
- expect(logger.mock.calls[0][0]).toContain("step.start");
157
- expect(logger.mock.calls[0][0]).toContain("stepIndex");
158
- });
159
-
160
- it("logs step.done event with custom logger", () => {
161
- const conversation = makeMockConversationState();
162
- const { api, eventListeners } = makeMockApi(conversation);
163
- const logger = vi.fn();
164
-
165
- const ext = Logging({ logger });
166
- ext.register(api);
167
-
168
- emit(eventListeners, "step.done", { stepIndex: 0, toolCallCount: 2 });
169
-
170
- expect(logger).toHaveBeenCalledOnce();
171
- expect(logger.mock.calls[0][0]).toContain("step.done");
172
- });
173
-
174
- it("logs tool.start and tool.done events with custom logger", () => {
175
- const conversation = makeMockConversationState();
176
- const { api, eventListeners } = makeMockApi(conversation);
177
- const logger = vi.fn();
178
-
179
- const ext = Logging({ logger });
180
- ext.register(api);
181
-
182
- emit(eventListeners, "tool.start", { toolName: "bash", toolCallId: "tc-1" });
183
- emit(eventListeners, "tool.done", { toolName: "bash", toolCallId: "tc-1", result: { type: "text", text: "ok" } });
184
-
185
- expect(logger).toHaveBeenCalledTimes(2);
186
- expect(logger.mock.calls[0][0]).toContain("tool.start");
187
- expect(logger.mock.calls[0][0]).toContain("bash");
188
- expect(logger.mock.calls[1][0]).toContain("tool.done");
189
- });
190
-
191
- it("does NOT call pipeline.register (event-based, no middleware)", () => {
192
- const conversation = makeMockConversationState();
193
- const { api } = makeMockApi(conversation);
194
-
195
- const ext = Logging();
196
- ext.register(api);
197
-
198
- expect(api.pipeline.register).not.toHaveBeenCalled();
199
- });
200
- });
@@ -1,194 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { MessageWindow } from "../extensions/message-window.js";
3
- import type {
4
- ExtensionApi,
5
- StepMiddleware,
6
- StepContext,
7
- StepResult,
8
- ConversationState,
9
- Message,
10
- MessageEvent,
11
- } from "@goondan/openharness-types";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Helpers
15
- // ---------------------------------------------------------------------------
16
-
17
- function makeMessages(count: number): Message[] {
18
- return Array.from({ length: count }, (_, i) => ({
19
- id: `msg-${i}`,
20
- data: {
21
- role: "user" as const,
22
- content: `Message ${i}`,
23
- },
24
- }));
25
- }
26
-
27
- function makeMockConversationState(
28
- messages: Message[],
29
- ): ConversationState & { emitted: MessageEvent[] } {
30
- const emitted: MessageEvent[] = [];
31
- return {
32
- messages,
33
- events: [],
34
- emitted,
35
- emit: vi.fn((event: MessageEvent) => {
36
- emitted.push(event);
37
- }),
38
- restore: vi.fn(),
39
- };
40
- }
41
-
42
- function makeMockApi(conversation: ConversationState): {
43
- api: ExtensionApi;
44
- registeredMiddleware: Array<{
45
- level: string;
46
- handler: StepMiddleware;
47
- options?: { priority?: number };
48
- }>;
49
- } {
50
- const registeredMiddleware: Array<{
51
- level: string;
52
- handler: StepMiddleware;
53
- options?: { priority?: number };
54
- }> = [];
55
-
56
- const api: ExtensionApi = {
57
- pipeline: {
58
- register: vi.fn(
59
- (level: string, handler: StepMiddleware, options?: { priority?: number }) => {
60
- registeredMiddleware.push({ level, handler, options });
61
- },
62
- ) as unknown as ExtensionApi["pipeline"]["register"],
63
- },
64
- tools: {
65
- register: vi.fn(),
66
- remove: vi.fn(),
67
- list: vi.fn(() => []),
68
- },
69
- on: vi.fn(),
70
- conversation,
71
- runtime: {
72
- agent: {
73
- name: "test-agent",
74
- model: { provider: "openai", model: "gpt-4o" },
75
- extensions: [],
76
- tools: [],
77
- },
78
- agents: {},
79
- connections: {},
80
- },
81
- };
82
-
83
- return { api, registeredMiddleware };
84
- }
85
-
86
- function makeStepContext(conversation: ConversationState): StepContext {
87
- return {
88
- turnId: "turn-1",
89
- agentName: "test-agent",
90
- conversationId: "conv-1",
91
- conversation,
92
- stepNumber: 1,
93
- abortSignal: new AbortController().signal,
94
- input: {
95
- name: "test-event",
96
- content: [{ type: "text", text: "hello" }],
97
- properties: {},
98
- source: {
99
- connector: "test-connector",
100
- connectionName: "test",
101
- receivedAt: new Date().toISOString(),
102
- },
103
- },
104
- llm: { chat: vi.fn().mockResolvedValue({ text: "mock" }) },
105
- };
106
- }
107
-
108
- const stubStepResult: StepResult = {
109
- toolCalls: [],
110
- };
111
-
112
- // ---------------------------------------------------------------------------
113
- // Tests
114
- // ---------------------------------------------------------------------------
115
-
116
- describe("MessageWindow", () => {
117
- it("creates an Extension with name 'message-window'", () => {
118
- const ext = MessageWindow({ maxMessages: 5 });
119
- expect(ext.name).toBe("message-window");
120
- });
121
-
122
- it("registers step middleware via api.pipeline.register", () => {
123
- const conversation = makeMockConversationState([]);
124
- const { api, registeredMiddleware } = makeMockApi(conversation);
125
-
126
- const ext = MessageWindow({ maxMessages: 5 });
127
- ext.register(api);
128
-
129
- expect(api.pipeline.register).toHaveBeenCalledOnce();
130
- expect(registeredMiddleware[0].level).toBe("step");
131
- });
132
-
133
- it("does NOT emit truncate when messages are within maxMessages", async () => {
134
- const messages = makeMessages(3);
135
- const conversation = makeMockConversationState(messages);
136
- const { api, registeredMiddleware } = makeMockApi(conversation);
137
-
138
- const ext = MessageWindow({ maxMessages: 5 });
139
- ext.register(api);
140
-
141
- const middleware = registeredMiddleware[0].handler;
142
- const ctx = makeStepContext(conversation);
143
- const next = vi.fn(async () => stubStepResult);
144
-
145
- await middleware(ctx, next);
146
-
147
- expect(conversation.emit).not.toHaveBeenCalled();
148
- expect(next).toHaveBeenCalledOnce();
149
- });
150
-
151
- it("emits truncate when messages exceed maxMessages", async () => {
152
- const messages = makeMessages(8);
153
- const conversation = makeMockConversationState(messages);
154
- const { api, registeredMiddleware } = makeMockApi(conversation);
155
-
156
- const ext = MessageWindow({ maxMessages: 5 });
157
- ext.register(api);
158
-
159
- const middleware = registeredMiddleware[0].handler;
160
- const ctx = makeStepContext(conversation);
161
- const next = vi.fn(async () => stubStepResult);
162
-
163
- await middleware(ctx, next);
164
-
165
- expect(conversation.emit).toHaveBeenCalledOnce();
166
- const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted[0];
167
- expect(emitted.type).toBe("truncate");
168
- if (emitted.type === "truncate") {
169
- expect(emitted.keepLast).toBe(5);
170
- }
171
- expect(next).toHaveBeenCalledOnce();
172
- });
173
-
174
- it("emits truncate with exact maxMessages count", async () => {
175
- const messages = makeMessages(6);
176
- const conversation = makeMockConversationState(messages);
177
- const { api, registeredMiddleware } = makeMockApi(conversation);
178
-
179
- const ext = MessageWindow({ maxMessages: 5 });
180
- ext.register(api);
181
-
182
- const middleware = registeredMiddleware[0].handler;
183
- const ctx = makeStepContext(conversation);
184
- const next = vi.fn(async () => stubStepResult);
185
-
186
- await middleware(ctx, next);
187
-
188
- expect(conversation.emit).toHaveBeenCalledOnce();
189
- const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted[0];
190
- if (emitted.type === "truncate") {
191
- expect(emitted.keepLast).toBe(5);
192
- }
193
- });
194
- });
@@ -1,207 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { RequiredToolsGuard } from "../extensions/required-tools-guard.js";
3
- import type {
4
- ExtensionApi,
5
- TurnMiddleware,
6
- TurnContext,
7
- TurnResult,
8
- ConversationState,
9
- ToolDefinition,
10
- } from "@goondan/openharness-types";
11
-
12
- // ---------------------------------------------------------------------------
13
- // Helpers
14
- // ---------------------------------------------------------------------------
15
-
16
- function makeMockConversationState(): ConversationState {
17
- return {
18
- messages: [],
19
- events: [],
20
- emit: vi.fn(),
21
- restore: vi.fn(),
22
- };
23
- }
24
-
25
- function makeMockApi(
26
- conversation: ConversationState,
27
- availableTools: ToolDefinition[] = [],
28
- ): {
29
- api: ExtensionApi;
30
- registeredMiddleware: Array<{
31
- level: string;
32
- handler: TurnMiddleware;
33
- options?: { priority?: number };
34
- }>;
35
- } {
36
- const registeredMiddleware: Array<{
37
- level: string;
38
- handler: TurnMiddleware;
39
- options?: { priority?: number };
40
- }> = [];
41
-
42
- const api: ExtensionApi = {
43
- pipeline: {
44
- register: vi.fn(
45
- (level: string, handler: TurnMiddleware, options?: { priority?: number }) => {
46
- registeredMiddleware.push({ level, handler, options });
47
- },
48
- ) as unknown as ExtensionApi["pipeline"]["register"],
49
- },
50
- tools: {
51
- register: vi.fn(),
52
- remove: vi.fn(),
53
- list: vi.fn(() => availableTools as readonly ToolDefinition[]),
54
- },
55
- on: vi.fn(),
56
- conversation,
57
- runtime: {
58
- agent: {
59
- name: "test-agent",
60
- model: { provider: "openai", model: "gpt-4o" },
61
- extensions: [],
62
- tools: [],
63
- },
64
- agents: {},
65
- connections: {},
66
- },
67
- };
68
-
69
- return { api, registeredMiddleware };
70
- }
71
-
72
- function makeTurnContext(conversation: ConversationState): TurnContext {
73
- return {
74
- turnId: "turn-1",
75
- agentName: "test-agent",
76
- conversationId: "conv-1",
77
- conversation,
78
- abortSignal: new AbortController().signal,
79
- input: {
80
- name: "test-event",
81
- content: [{ type: "text", text: "hello" }],
82
- properties: {},
83
- source: {
84
- connector: "test-connector",
85
- connectionName: "test",
86
- receivedAt: new Date().toISOString(),
87
- },
88
- },
89
- llm: { chat: vi.fn().mockResolvedValue({ text: "mock" }) },
90
- };
91
- }
92
-
93
- function makeDummyTool(name: string): ToolDefinition {
94
- return {
95
- name,
96
- description: `Tool ${name}`,
97
- parameters: { type: "object", properties: {} },
98
- handler: async () => ({ type: "text", text: "ok" }),
99
- };
100
- }
101
-
102
- const stubTurnResult: TurnResult = {
103
- turnId: "turn-1",
104
- agentName: "test-agent",
105
- conversationId: "conv-1",
106
- status: "completed",
107
- steps: [],
108
- };
109
-
110
- // ---------------------------------------------------------------------------
111
- // Tests
112
- // ---------------------------------------------------------------------------
113
-
114
- describe("RequiredToolsGuard", () => {
115
- it("creates an Extension with name 'required-tools-guard'", () => {
116
- const ext = RequiredToolsGuard({ tools: ["my_tool"] });
117
- expect(ext.name).toBe("required-tools-guard");
118
- });
119
-
120
- it("registers turn middleware via api.pipeline.register", () => {
121
- const conversation = makeMockConversationState();
122
- const { api, registeredMiddleware } = makeMockApi(conversation);
123
-
124
- const ext = RequiredToolsGuard({ tools: ["my_tool"] });
125
- ext.register(api);
126
-
127
- expect(api.pipeline.register).toHaveBeenCalledOnce();
128
- expect(registeredMiddleware[0].level).toBe("turn");
129
- });
130
-
131
- it("calls next() when all required tools are present", async () => {
132
- const conversation = makeMockConversationState();
133
- const tools = [makeDummyTool("tool_a"), makeDummyTool("tool_b")];
134
- const { api, registeredMiddleware } = makeMockApi(conversation, tools);
135
-
136
- const ext = RequiredToolsGuard({ tools: ["tool_a", "tool_b"] });
137
- ext.register(api);
138
-
139
- const middleware = registeredMiddleware[0].handler;
140
- const ctx = makeTurnContext(conversation);
141
- const next = vi.fn(async () => stubTurnResult);
142
-
143
- const result = await middleware(ctx, next);
144
-
145
- expect(next).toHaveBeenCalledOnce();
146
- expect(result).toBe(stubTurnResult);
147
- });
148
-
149
- it("throws error when required tools are missing", async () => {
150
- const conversation = makeMockConversationState();
151
- const tools = [makeDummyTool("tool_a")];
152
- const { api, registeredMiddleware } = makeMockApi(conversation, tools);
153
-
154
- const ext = RequiredToolsGuard({ tools: ["tool_a", "tool_b"] });
155
- ext.register(api);
156
-
157
- const middleware = registeredMiddleware[0].handler;
158
- const ctx = makeTurnContext(conversation);
159
- const next = vi.fn(async () => stubTurnResult);
160
-
161
- await expect(middleware(ctx, next)).rejects.toThrow("tool_b");
162
- expect(next).not.toHaveBeenCalled();
163
- });
164
-
165
- it("throws when no tools are registered at all", async () => {
166
- const conversation = makeMockConversationState();
167
- const { api, registeredMiddleware } = makeMockApi(conversation, []);
168
-
169
- const ext = RequiredToolsGuard({ tools: ["required_tool"] });
170
- ext.register(api);
171
-
172
- const middleware = registeredMiddleware[0].handler;
173
- const ctx = makeTurnContext(conversation);
174
- const next = vi.fn(async () => stubTurnResult);
175
-
176
- await expect(middleware(ctx, next)).rejects.toThrow("required_tool");
177
- });
178
-
179
- it("passes when no tools are required", async () => {
180
- const conversation = makeMockConversationState();
181
- const { api, registeredMiddleware } = makeMockApi(conversation, []);
182
-
183
- const ext = RequiredToolsGuard({ tools: [] });
184
- ext.register(api);
185
-
186
- const middleware = registeredMiddleware[0].handler;
187
- const ctx = makeTurnContext(conversation);
188
- const next = vi.fn(async () => stubTurnResult);
189
-
190
- await expect(middleware(ctx, next)).resolves.toBe(stubTurnResult);
191
- expect(next).toHaveBeenCalledOnce();
192
- });
193
-
194
- it("error message lists all missing tools", async () => {
195
- const conversation = makeMockConversationState();
196
- const { api, registeredMiddleware } = makeMockApi(conversation, []);
197
-
198
- const ext = RequiredToolsGuard({ tools: ["tool_x", "tool_y", "tool_z"] });
199
- ext.register(api);
200
-
201
- const middleware = registeredMiddleware[0].handler;
202
- const ctx = makeTurnContext(conversation);
203
- const next = vi.fn(async () => stubTurnResult);
204
-
205
- await expect(middleware(ctx, next)).rejects.toThrow(/tool_x.*tool_y.*tool_z/);
206
- });
207
- });