@goondan/openharness-base 0.1.8 → 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.
- package/dist/index.d.ts +84 -0
- package/dist/index.js +537 -0
- package/package.json +14 -9
- package/src/__tests__/basic-system-prompt.test.ts +0 -186
- package/src/__tests__/compaction-summarize.test.ts +0 -282
- package/src/__tests__/logging.test.ts +0 -200
- package/src/__tests__/message-window.test.ts +0 -194
- package/src/__tests__/required-tools-guard.test.ts +0 -207
- package/src/__tests__/tool-search.test.ts +0 -187
- package/src/__tests__/tools.test.ts +0 -332
- package/src/extensions/basic-system-prompt.ts +0 -48
- package/src/extensions/compaction-summarize.ts +0 -104
- package/src/extensions/logging.ts +0 -42
- package/src/extensions/message-window.ts +0 -23
- package/src/extensions/required-tools-guard.ts +0 -24
- package/src/extensions/tool-search.ts +0 -38
- package/src/index.ts +0 -16
- package/src/tools/bash.ts +0 -38
- package/src/tools/file-system.ts +0 -83
- package/src/tools/http-fetch.ts +0 -64
- package/src/tools/json-query.ts +0 -71
- package/src/tools/text-transform.ts +0 -59
- package/src/tools/wait.ts +0 -46
- package/tsconfig.json +0 -8
- package/vitest.config.ts +0 -7
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { BasicSystemPrompt } from "../extensions/basic-system-prompt.js";
|
|
3
|
-
import type {
|
|
4
|
-
ExtensionApi,
|
|
5
|
-
TurnMiddleware,
|
|
6
|
-
TurnContext,
|
|
7
|
-
TurnResult,
|
|
8
|
-
ConversationState,
|
|
9
|
-
Message,
|
|
10
|
-
MessageEvent,
|
|
11
|
-
} from "@goondan/openharness-types";
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Helpers
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
function makeMockConversationState(): ConversationState & { emitted: MessageEvent[] } {
|
|
18
|
-
const emitted: MessageEvent[] = [];
|
|
19
|
-
const messages: Message[] = [];
|
|
20
|
-
return {
|
|
21
|
-
messages,
|
|
22
|
-
events: [],
|
|
23
|
-
emitted,
|
|
24
|
-
emit: vi.fn((event: MessageEvent) => {
|
|
25
|
-
emitted.push(event);
|
|
26
|
-
if (event.type === "append") {
|
|
27
|
-
messages.push(event.message);
|
|
28
|
-
}
|
|
29
|
-
}),
|
|
30
|
-
restore: vi.fn(),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function makeMockApi(conversation: ConversationState): {
|
|
35
|
-
api: ExtensionApi;
|
|
36
|
-
registeredMiddleware: Array<{ level: string; handler: TurnMiddleware; options?: { priority?: number } }>;
|
|
37
|
-
} {
|
|
38
|
-
const registeredMiddleware: Array<{ level: string; handler: TurnMiddleware; options?: { priority?: number } }> = [];
|
|
39
|
-
|
|
40
|
-
const api: ExtensionApi = {
|
|
41
|
-
pipeline: {
|
|
42
|
-
register: vi.fn((level: string, handler: TurnMiddleware, options?: { priority?: number }) => {
|
|
43
|
-
registeredMiddleware.push({ level, handler, options });
|
|
44
|
-
}) as unknown as ExtensionApi["pipeline"]["register"],
|
|
45
|
-
},
|
|
46
|
-
tools: {
|
|
47
|
-
register: vi.fn(),
|
|
48
|
-
remove: vi.fn(),
|
|
49
|
-
list: vi.fn(() => []),
|
|
50
|
-
},
|
|
51
|
-
on: vi.fn(),
|
|
52
|
-
conversation,
|
|
53
|
-
runtime: {
|
|
54
|
-
agent: {
|
|
55
|
-
name: "test-agent",
|
|
56
|
-
model: { provider: "openai", model: "gpt-4o" },
|
|
57
|
-
extensions: [],
|
|
58
|
-
tools: [],
|
|
59
|
-
},
|
|
60
|
-
agents: {},
|
|
61
|
-
connections: {},
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
return { api, registeredMiddleware };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function makeTurnContext(conversation: ConversationState): TurnContext {
|
|
69
|
-
return {
|
|
70
|
-
turnId: "turn-1",
|
|
71
|
-
agentName: "test-agent",
|
|
72
|
-
conversationId: "conv-1",
|
|
73
|
-
conversation,
|
|
74
|
-
abortSignal: new AbortController().signal,
|
|
75
|
-
input: {
|
|
76
|
-
name: "test-event",
|
|
77
|
-
content: [{ type: "text", text: "hello" }],
|
|
78
|
-
properties: {},
|
|
79
|
-
source: {
|
|
80
|
-
connector: "test-connector",
|
|
81
|
-
connectionName: "test",
|
|
82
|
-
receivedAt: new Date().toISOString(),
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
llm: { chat: vi.fn().mockResolvedValue({ text: "mock" }) },
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const stubTurnResult: TurnResult = {
|
|
90
|
-
turnId: "turn-1",
|
|
91
|
-
agentName: "test-agent",
|
|
92
|
-
conversationId: "conv-1",
|
|
93
|
-
status: "completed",
|
|
94
|
-
steps: [],
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// Tests
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
describe("BasicSystemPrompt", () => {
|
|
102
|
-
it("creates an Extension with name 'basic-system-prompt'", () => {
|
|
103
|
-
const ext = BasicSystemPrompt("You are helpful.");
|
|
104
|
-
expect(ext.name).toBe("basic-system-prompt");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("register() calls api.pipeline.register with level 'turn'", () => {
|
|
108
|
-
const conversation = makeMockConversationState();
|
|
109
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
110
|
-
|
|
111
|
-
const ext = BasicSystemPrompt("You are helpful.");
|
|
112
|
-
ext.register(api);
|
|
113
|
-
|
|
114
|
-
expect(api.pipeline.register).toHaveBeenCalledOnce();
|
|
115
|
-
expect(registeredMiddleware).toHaveLength(1);
|
|
116
|
-
expect(registeredMiddleware[0].level).toBe("turn");
|
|
117
|
-
expect(typeof registeredMiddleware[0].handler).toBe("function");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("registers turn middleware with priority 10 (high priority)", () => {
|
|
121
|
-
const conversation = makeMockConversationState();
|
|
122
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
123
|
-
|
|
124
|
-
const ext = BasicSystemPrompt("You are helpful.");
|
|
125
|
-
ext.register(api);
|
|
126
|
-
|
|
127
|
-
expect(registeredMiddleware[0].options?.priority).toBe(10);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("middleware appends a system message and calls next()", async () => {
|
|
131
|
-
const conversation = makeMockConversationState();
|
|
132
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
133
|
-
|
|
134
|
-
const ext = BasicSystemPrompt("You are helpful.");
|
|
135
|
-
ext.register(api);
|
|
136
|
-
|
|
137
|
-
const middleware = registeredMiddleware[0].handler;
|
|
138
|
-
const ctx = makeTurnContext(conversation);
|
|
139
|
-
const next = vi.fn(async () => stubTurnResult);
|
|
140
|
-
|
|
141
|
-
const result = await middleware(ctx, next);
|
|
142
|
-
|
|
143
|
-
expect(next).toHaveBeenCalledOnce();
|
|
144
|
-
expect(result).toBe(stubTurnResult);
|
|
145
|
-
|
|
146
|
-
// Verify system message was emitted
|
|
147
|
-
expect(conversation.emit).toHaveBeenCalledOnce();
|
|
148
|
-
const emittedEvent = conversation.emitted[0];
|
|
149
|
-
expect(emittedEvent.type).toBe("append");
|
|
150
|
-
if (emittedEvent.type === "append") {
|
|
151
|
-
expect(emittedEvent.message.id).toBe("sys-basic-system-prompt");
|
|
152
|
-
expect(emittedEvent.message.data.role).toBe("system");
|
|
153
|
-
expect(emittedEvent.message.data.content).toBe("You are helpful.");
|
|
154
|
-
expect(emittedEvent.message.metadata?.__createdBy).toBe("basic-system-prompt");
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("does not duplicate the system message on subsequent turns", async () => {
|
|
159
|
-
const conversation = makeMockConversationState();
|
|
160
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
161
|
-
|
|
162
|
-
const ext = BasicSystemPrompt("You are helpful.");
|
|
163
|
-
ext.register(api);
|
|
164
|
-
|
|
165
|
-
const middleware = registeredMiddleware[0].handler;
|
|
166
|
-
const next = vi.fn(async () => stubTurnResult);
|
|
167
|
-
|
|
168
|
-
// First turn — should append
|
|
169
|
-
const ctx1 = makeTurnContext(conversation);
|
|
170
|
-
await middleware(ctx1, next);
|
|
171
|
-
expect(conversation.emitted).toHaveLength(1);
|
|
172
|
-
|
|
173
|
-
// Second turn — system message already in conversation.messages, should skip
|
|
174
|
-
const ctx2 = makeTurnContext(conversation);
|
|
175
|
-
await middleware(ctx2, next);
|
|
176
|
-
expect(conversation.emitted).toHaveLength(1); // still 1, no new append
|
|
177
|
-
|
|
178
|
-
// Third turn — still no duplication
|
|
179
|
-
const ctx3 = makeTurnContext(conversation);
|
|
180
|
-
await middleware(ctx3, next);
|
|
181
|
-
expect(conversation.emitted).toHaveLength(1);
|
|
182
|
-
|
|
183
|
-
// next() should have been called every turn
|
|
184
|
-
expect(next).toHaveBeenCalledTimes(3);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { CompactionSummarize } from "../extensions/compaction-summarize.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 makeMockLlmClient() {
|
|
87
|
-
return {
|
|
88
|
-
chat: vi.fn().mockResolvedValue({ text: "LLM-generated summary of the conversation." }),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function makeStepContext(conversation: ConversationState): StepContext {
|
|
93
|
-
return {
|
|
94
|
-
turnId: "turn-1",
|
|
95
|
-
agentName: "test-agent",
|
|
96
|
-
conversationId: "conv-1",
|
|
97
|
-
conversation,
|
|
98
|
-
stepNumber: 1,
|
|
99
|
-
abortSignal: new AbortController().signal,
|
|
100
|
-
input: {
|
|
101
|
-
name: "test-event",
|
|
102
|
-
content: [{ type: "text", text: "hello" }],
|
|
103
|
-
properties: {},
|
|
104
|
-
source: {
|
|
105
|
-
connector: "test-connector",
|
|
106
|
-
connectionName: "test",
|
|
107
|
-
receivedAt: new Date().toISOString(),
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
llm: makeMockLlmClient(),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const stubStepResult: StepResult = {
|
|
115
|
-
toolCalls: [],
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// Tests
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
|
|
122
|
-
describe("CompactionSummarize", () => {
|
|
123
|
-
it("creates an Extension with name 'compaction-summarize'", () => {
|
|
124
|
-
const ext = CompactionSummarize({ threshold: 10 });
|
|
125
|
-
expect(ext.name).toBe("compaction-summarize");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("registers step middleware via api.pipeline.register", () => {
|
|
129
|
-
const conversation = makeMockConversationState([]);
|
|
130
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
131
|
-
|
|
132
|
-
const ext = CompactionSummarize({ threshold: 10 });
|
|
133
|
-
ext.register(api);
|
|
134
|
-
|
|
135
|
-
expect(api.pipeline.register).toHaveBeenCalledOnce();
|
|
136
|
-
expect(registeredMiddleware[0].level).toBe("step");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("does NOT compact when messages are below threshold", async () => {
|
|
140
|
-
const messages = makeMessages(5);
|
|
141
|
-
const conversation = makeMockConversationState(messages);
|
|
142
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
143
|
-
|
|
144
|
-
const ext = CompactionSummarize({ threshold: 10 });
|
|
145
|
-
ext.register(api);
|
|
146
|
-
|
|
147
|
-
const middleware = registeredMiddleware[0].handler;
|
|
148
|
-
const ctx = makeStepContext(conversation);
|
|
149
|
-
const next = vi.fn(async () => stubStepResult);
|
|
150
|
-
|
|
151
|
-
await middleware(ctx, next);
|
|
152
|
-
|
|
153
|
-
expect(conversation.emit).not.toHaveBeenCalled();
|
|
154
|
-
expect(next).toHaveBeenCalledOnce();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("compacts when messages exceed threshold", async () => {
|
|
158
|
-
const messages = makeMessages(12);
|
|
159
|
-
const conversation = makeMockConversationState(messages);
|
|
160
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
161
|
-
|
|
162
|
-
const ext = CompactionSummarize({ threshold: 10 });
|
|
163
|
-
ext.register(api);
|
|
164
|
-
|
|
165
|
-
const middleware = registeredMiddleware[0].handler;
|
|
166
|
-
const ctx = makeStepContext(conversation);
|
|
167
|
-
const next = vi.fn(async () => stubStepResult);
|
|
168
|
-
|
|
169
|
-
await middleware(ctx, next);
|
|
170
|
-
|
|
171
|
-
const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
|
|
172
|
-
|
|
173
|
-
// keepCount = floor(10/2) = 5, removeCount = 12 - 5 = 7
|
|
174
|
-
// Should have 7 remove events + 1 append event
|
|
175
|
-
const removeEvents = emitted.filter((e) => e.type === "remove");
|
|
176
|
-
const appendEvents = emitted.filter((e) => e.type === "append");
|
|
177
|
-
|
|
178
|
-
expect(removeEvents).toHaveLength(7);
|
|
179
|
-
expect(appendEvents).toHaveLength(1);
|
|
180
|
-
|
|
181
|
-
// The appended message should be a system summary
|
|
182
|
-
const appendEvent = appendEvents[0];
|
|
183
|
-
if (appendEvent.type === "append") {
|
|
184
|
-
expect(appendEvent.message.data.role).toBe("system");
|
|
185
|
-
expect(appendEvent.message.data.content).toContain("[Summary of earlier conversation]:");
|
|
186
|
-
expect(appendEvent.message.metadata?.__createdBy).toBe("compaction-summarize");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
expect(next).toHaveBeenCalledOnce();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("removes the oldest messages (not the newest)", async () => {
|
|
193
|
-
const messages = makeMessages(12);
|
|
194
|
-
const conversation = makeMockConversationState(messages);
|
|
195
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
196
|
-
|
|
197
|
-
const ext = CompactionSummarize({ threshold: 10 });
|
|
198
|
-
ext.register(api);
|
|
199
|
-
|
|
200
|
-
const middleware = registeredMiddleware[0].handler;
|
|
201
|
-
const ctx = makeStepContext(conversation);
|
|
202
|
-
const next = vi.fn(async () => stubStepResult);
|
|
203
|
-
|
|
204
|
-
await middleware(ctx, next);
|
|
205
|
-
|
|
206
|
-
const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
|
|
207
|
-
const removeEvents = emitted.filter(
|
|
208
|
-
(e): e is Extract<typeof e, { type: "remove" }> => e.type === "remove",
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Should remove msg-0 through msg-6 (the oldest 7)
|
|
212
|
-
const removedIds = removeEvents.map((e) => e.messageId);
|
|
213
|
-
expect(removedIds).toContain("msg-0");
|
|
214
|
-
expect(removedIds).toContain("msg-6");
|
|
215
|
-
expect(removedIds).not.toContain("msg-7");
|
|
216
|
-
expect(removedIds).not.toContain("msg-11");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("summary content includes text from removed messages", async () => {
|
|
220
|
-
const messages = makeMessages(12);
|
|
221
|
-
const conversation = makeMockConversationState(messages);
|
|
222
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
223
|
-
|
|
224
|
-
const ext = CompactionSummarize({ threshold: 10 });
|
|
225
|
-
ext.register(api);
|
|
226
|
-
|
|
227
|
-
const middleware = registeredMiddleware[0].handler;
|
|
228
|
-
const ctx = makeStepContext(conversation);
|
|
229
|
-
const next = vi.fn(async () => stubStepResult);
|
|
230
|
-
|
|
231
|
-
await middleware(ctx, next);
|
|
232
|
-
|
|
233
|
-
// Verify LLM was called for summarization
|
|
234
|
-
expect(ctx.llm.chat).toHaveBeenCalledOnce();
|
|
235
|
-
|
|
236
|
-
const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
|
|
237
|
-
const appendEvent = emitted.find((e) => e.type === "append");
|
|
238
|
-
if (appendEvent && appendEvent.type === "append") {
|
|
239
|
-
const content = appendEvent.message.data.content as string;
|
|
240
|
-
// Should include the LLM-generated summary
|
|
241
|
-
expect(content).toContain("LLM-generated summary of the conversation.");
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("uses custom summarizer when provided", async () => {
|
|
246
|
-
const messages = makeMessages(12);
|
|
247
|
-
const conversation = makeMockConversationState(messages);
|
|
248
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
249
|
-
|
|
250
|
-
const summarizer = vi.fn(async (msgs: Message[]) => {
|
|
251
|
-
return `Custom summary of ${msgs.length} messages`;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const ext = CompactionSummarize({ threshold: 10, summarizer });
|
|
255
|
-
ext.register(api);
|
|
256
|
-
|
|
257
|
-
const middleware = registeredMiddleware[0].handler;
|
|
258
|
-
const ctx = makeStepContext(conversation);
|
|
259
|
-
const next = vi.fn(async () => stubStepResult);
|
|
260
|
-
|
|
261
|
-
await middleware(ctx, next);
|
|
262
|
-
|
|
263
|
-
// summarizer should have been called with the 7 removed messages
|
|
264
|
-
expect(summarizer).toHaveBeenCalledOnce();
|
|
265
|
-
expect(summarizer).toHaveBeenCalledWith(
|
|
266
|
-
expect.arrayContaining([
|
|
267
|
-
expect.objectContaining({ id: "msg-0" }),
|
|
268
|
-
expect.objectContaining({ id: "msg-6" }),
|
|
269
|
-
]),
|
|
270
|
-
);
|
|
271
|
-
expect(summarizer.mock.calls[0][0]).toHaveLength(7);
|
|
272
|
-
|
|
273
|
-
const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
|
|
274
|
-
const appendEvent = emitted.find((e) => e.type === "append");
|
|
275
|
-
if (appendEvent && appendEvent.type === "append") {
|
|
276
|
-
const content = appendEvent.message.data.content as string;
|
|
277
|
-
expect(content).toContain("Custom summary of 7 messages");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
expect(next).toHaveBeenCalledOnce();
|
|
281
|
-
});
|
|
282
|
-
});
|
|
@@ -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
|
-
});
|