@aaroncql/pim-agent 0.0.1 → 0.1.0
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/README.md +19 -8
- package/bin/pim.ts +55 -3
- package/package.json +20 -5
- package/src/extensions/_init/index.ts +3 -2
- package/src/extensions/bash/capture.test.ts +0 -126
- package/src/extensions/bash/format.test.ts +0 -240
- package/src/extensions/bash/run.test.ts +0 -262
- package/src/extensions/command-picker/ranker.test.ts +0 -46
- package/src/extensions/edit/edit.test.ts +0 -285
- package/src/extensions/file-picker/catalog.test.ts +0 -263
- package/src/extensions/file-picker/index.test.ts +0 -168
- package/src/extensions/file-picker/ranker.test.ts +0 -94
- package/src/extensions/footer/git.test.ts +0 -76
- package/src/extensions/footer/index.test.ts +0 -161
- package/src/extensions/footer/segments.test.ts +0 -164
- package/src/extensions/glob/glob.test.ts +0 -171
- package/src/extensions/glob/index.test.ts +0 -68
- package/src/extensions/glob/render.test.ts +0 -126
- package/src/extensions/grep/grep.test.ts +0 -387
- package/src/extensions/grep/index.test.ts +0 -68
- package/src/extensions/grep/render.test.ts +0 -269
- package/src/extensions/read/read.test.ts +0 -177
- package/src/extensions/read/render.test.ts +0 -61
- package/src/extensions/subagent/index.test.ts +0 -44
- package/src/extensions/subagent/render.test.ts +0 -292
- package/src/extensions/subagent/subagent.test.ts +0 -315
- package/src/extensions/system-prompt/prompt.test.ts +0 -64
- package/src/extensions/todo/index.test.ts +0 -244
- package/src/extensions/todo/render.test.ts +0 -180
- package/src/extensions/todo/todo.test.ts +0 -222
- package/src/extensions/tps/index.test.ts +0 -254
- package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
- package/src/extensions/web-fetch/fetch.test.ts +0 -244
- package/src/extensions/web-fetch/render.test.ts +0 -56
- package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
- package/src/extensions/web-search/render.test.ts +0 -21
- package/src/extensions/web-search/search.test.ts +0 -53
- package/src/extensions/working-indicator/index.test.ts +0 -21
- package/src/extensions/write/render.test.ts +0 -64
- package/src/extensions/write/write.test.ts +0 -108
- package/src/shared/DiffLines.test.ts +0 -193
- package/src/shared/DiffRenderer.test.ts +0 -206
- package/src/shared/EditMatcher.test.ts +0 -123
- package/src/shared/FileScanner.test.ts +0 -158
- package/src/shared/FuzzyMatcher.test.ts +0 -114
- package/src/shared/GitignoreFilter.test.ts +0 -64
- package/src/shared/Lines.test.ts +0 -25
- package/src/shared/McpClient.test.ts +0 -235
- package/src/shared/OutputBudget.test.ts +0 -99
- package/src/shared/Paths.test.ts +0 -51
- package/src/shared/PimSettings.test.ts +0 -90
- package/src/shared/Renderer.test.ts +0 -190
- package/src/shared/SpillCache.test.ts +0 -94
- package/src/shared/Tools.test.ts +0 -392
- package/src/telegram/Config.test.ts +0 -275
- package/src/telegram/Markdown.test.ts +0 -143
- package/src/telegram/Renderer.test.ts +0 -216
- package/src/telegram/SessionRegistry.test.ts +0 -89
- package/src/telegram/TaskScheduler.test.ts +0 -278
- package/src/telegram/TaskTool.test.ts +0 -179
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { SpillCache } from "./SpillCache";
|
|
6
|
-
|
|
7
|
-
let previousPimHomeDir: string | undefined;
|
|
8
|
-
let testPimHomeDir: string | undefined;
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
previousPimHomeDir = process.env.PIM_HOME_DIR;
|
|
12
|
-
testPimHomeDir = await mkdtemp(join(tmpdir(), "pim-spill-home-"));
|
|
13
|
-
process.env.PIM_HOME_DIR = testPimHomeDir;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterAll(async () => {
|
|
17
|
-
if (previousPimHomeDir === undefined) {
|
|
18
|
-
delete process.env.PIM_HOME_DIR;
|
|
19
|
-
} else {
|
|
20
|
-
process.env.PIM_HOME_DIR = previousPimHomeDir;
|
|
21
|
-
}
|
|
22
|
-
if (testPimHomeDir) {
|
|
23
|
-
await rm(testPimHomeDir, { recursive: true, force: true });
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("SpillCache.write", () => {
|
|
28
|
-
test("writes a prefixed UUIDv7 file with locked-down modes", async () => {
|
|
29
|
-
const path = await SpillCache.write("fetch", "md", "# hello\nworld");
|
|
30
|
-
expect(path).toBeTruthy();
|
|
31
|
-
expect(path!.startsWith(join(SpillCache.dir(), "fetch-"))).toBe(true);
|
|
32
|
-
expect(path!.endsWith(".md")).toBe(true);
|
|
33
|
-
|
|
34
|
-
const dirMode = (await stat(SpillCache.dir())).mode & 0o777;
|
|
35
|
-
const fileMode = (await stat(path!)).mode & 0o777;
|
|
36
|
-
expect(dirMode).toBe(0o700);
|
|
37
|
-
expect(fileMode).toBe(0o600);
|
|
38
|
-
|
|
39
|
-
expect(await Bun.file(path!).text()).toBe("# hello\nworld");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("accepts binary payloads", async () => {
|
|
43
|
-
const path = await SpillCache.write(
|
|
44
|
-
"bash",
|
|
45
|
-
"out",
|
|
46
|
-
new Uint8Array([65, 66])
|
|
47
|
-
);
|
|
48
|
-
expect(path).toBeTruthy();
|
|
49
|
-
expect(await Bun.file(path!).text()).toBe("AB");
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("SpillCache.cleanup", () => {
|
|
54
|
-
test("deletes only expired spill files across prefixes", async () => {
|
|
55
|
-
const root = await mkdtemp(join(tmpdir(), "pim-spill-cleanup-"));
|
|
56
|
-
const now = Date.now();
|
|
57
|
-
const oldBash = join(root, "bash-0192ce11-26d5-7dc3-9305-1426de888c5a.out");
|
|
58
|
-
const oldFetch = join(
|
|
59
|
-
root,
|
|
60
|
-
"fetch-0192ce11-26d5-7dc3-9305-1426de888c5b.md"
|
|
61
|
-
);
|
|
62
|
-
const recent = join(root, "bash-0192ce11-26d5-7dc4-8894-bc88d506d6ee.err");
|
|
63
|
-
const invalidName = join(root, "bash-not-a-uuid.out");
|
|
64
|
-
const unrelated = join(root, "other-old.out");
|
|
65
|
-
try {
|
|
66
|
-
await writeFile(oldBash, "old");
|
|
67
|
-
await writeFile(oldFetch, "old");
|
|
68
|
-
await writeFile(recent, "recent");
|
|
69
|
-
await writeFile(invalidName, "invalid");
|
|
70
|
-
await writeFile(unrelated, "unrelated");
|
|
71
|
-
const oldDate = new Date(now - SpillCache.TTL_MS - 1000);
|
|
72
|
-
await utimes(oldBash, oldDate, oldDate);
|
|
73
|
-
await utimes(oldFetch, oldDate, oldDate);
|
|
74
|
-
await utimes(invalidName, oldDate, oldDate);
|
|
75
|
-
await utimes(unrelated, oldDate, oldDate);
|
|
76
|
-
|
|
77
|
-
SpillCache.cleanup(root, now);
|
|
78
|
-
|
|
79
|
-
expect(await Bun.file(oldBash).exists()).toBe(false);
|
|
80
|
-
expect(await Bun.file(oldFetch).exists()).toBe(false);
|
|
81
|
-
expect(await Bun.file(recent).exists()).toBe(true);
|
|
82
|
-
expect(await Bun.file(invalidName).exists()).toBe(true);
|
|
83
|
-
expect(await Bun.file(unrelated).exists()).toBe(true);
|
|
84
|
-
} finally {
|
|
85
|
-
await rm(root, { recursive: true, force: true });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("is a no-op when the cache dir is absent", () => {
|
|
90
|
-
expect(() =>
|
|
91
|
-
SpillCache.cleanup(join(tmpdir(), "pim-spill-missing-dir"), Date.now())
|
|
92
|
-
).not.toThrow();
|
|
93
|
-
});
|
|
94
|
-
});
|
package/src/shared/Tools.test.ts
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { Type, type TSchema } from "typebox";
|
|
3
|
-
import { StringEnum, validateToolArguments } from "@earendil-works/pi-ai";
|
|
4
|
-
import { Tools } from "./Tools";
|
|
5
|
-
|
|
6
|
-
function runValidator(parameters: TSchema, args: unknown): Error {
|
|
7
|
-
try {
|
|
8
|
-
validateToolArguments({ name: "t", parameters } as never, {
|
|
9
|
-
type: "toolCall",
|
|
10
|
-
id: "1",
|
|
11
|
-
name: "t",
|
|
12
|
-
arguments: args as Record<string, unknown>,
|
|
13
|
-
});
|
|
14
|
-
} catch (e) {
|
|
15
|
-
return e as Error;
|
|
16
|
-
}
|
|
17
|
-
throw new Error("expected validation to fail");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function rewrite(toolName: string, parameters: TSchema, args: unknown): string {
|
|
21
|
-
return Tools.rewriteValidationError(
|
|
22
|
-
toolName,
|
|
23
|
-
parameters as never,
|
|
24
|
-
runValidator(parameters, args),
|
|
25
|
-
args
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("Tools.rewriteValidationError", () => {
|
|
30
|
-
test("missing single required property at root", () => {
|
|
31
|
-
const params = Type.Object({ path: Type.String() });
|
|
32
|
-
expect(rewrite("read", params, {})).toBe(
|
|
33
|
-
'Validation failed for tool "read":\n - missing required property: path'
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("missing multiple required properties at root", () => {
|
|
38
|
-
const params = Type.Object({
|
|
39
|
-
path: Type.String(),
|
|
40
|
-
edits: Type.Array(Type.String()),
|
|
41
|
-
});
|
|
42
|
-
expect(rewrite("edit", params, {})).toBe(
|
|
43
|
-
'Validation failed for tool "edit":\n - missing required properties: path, edits'
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("missing nested required property", () => {
|
|
48
|
-
const params = Type.Object({
|
|
49
|
-
path: Type.String(),
|
|
50
|
-
edits: Type.Array(
|
|
51
|
-
Type.Object({
|
|
52
|
-
old_string: Type.String(),
|
|
53
|
-
new_string: Type.String(),
|
|
54
|
-
})
|
|
55
|
-
),
|
|
56
|
-
});
|
|
57
|
-
expect(
|
|
58
|
-
rewrite("edit", params, { path: "foo", edits: [{ old_string: "x" }] })
|
|
59
|
-
).toBe(
|
|
60
|
-
'Validation failed for tool "edit":\n - missing required property at edits.0: new_string'
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("constraint messages pass through with original path", () => {
|
|
65
|
-
const params = Type.Object({
|
|
66
|
-
limit: Type.Integer({ minimum: 1, maximum: 2000 }),
|
|
67
|
-
});
|
|
68
|
-
expect(rewrite("read", params, { limit: 99999 })).toBe(
|
|
69
|
-
'Validation failed for tool "read":\n - limit: must be <= 2000'
|
|
70
|
-
);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("strips Received arguments dump", () => {
|
|
74
|
-
const params = Type.Object({ path: Type.String() });
|
|
75
|
-
expect(rewrite("read", params, {})).not.toContain("Received arguments");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("non-validation errors pass through unchanged", () => {
|
|
79
|
-
expect(
|
|
80
|
-
Tools.rewriteValidationError(
|
|
81
|
-
"read",
|
|
82
|
-
Type.Object({}) as never,
|
|
83
|
-
new Error("boom")
|
|
84
|
-
)
|
|
85
|
-
).toBe("boom");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("union of literals collapses to enumerated values (bare strings, no quotes)", () => {
|
|
89
|
-
const params = Type.Object({
|
|
90
|
-
action: Type.Union([
|
|
91
|
-
Type.Literal("create"),
|
|
92
|
-
Type.Literal("list"),
|
|
93
|
-
Type.Literal("delete"),
|
|
94
|
-
Type.Literal("pause"),
|
|
95
|
-
Type.Literal("resume"),
|
|
96
|
-
Type.Literal("update_prompt"),
|
|
97
|
-
]),
|
|
98
|
-
});
|
|
99
|
-
expect(rewrite("task", params, { action: "foo" })).toBe(
|
|
100
|
-
'Validation failed for tool "task":\n - action: must be one of: create, list, delete, pause, resume, update_prompt'
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("tagged union collapses to discriminator values (bare strings)", () => {
|
|
105
|
-
const params = Type.Object({
|
|
106
|
-
schedule: Type.Union([
|
|
107
|
-
Type.Object({ type: Type.Literal("once"), at: Type.String() }),
|
|
108
|
-
Type.Object({ type: Type.Literal("interval"), every: Type.String() }),
|
|
109
|
-
Type.Object({ type: Type.Literal("cron"), expr: Type.String() }),
|
|
110
|
-
]),
|
|
111
|
-
});
|
|
112
|
-
expect(
|
|
113
|
-
rewrite("task", params, { schedule: { type: "foo", at: "x" } })
|
|
114
|
-
).toBe(
|
|
115
|
-
'Validation failed for tool "task":\n - schedule: must match one of the allowed variants (type: once, interval, cron)'
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("tagged union with discriminator match shows only matched branch errors", () => {
|
|
120
|
-
const params = Type.Object({
|
|
121
|
-
schedule: Type.Union([
|
|
122
|
-
Type.Object({ type: Type.Literal("once"), at: Type.String() }),
|
|
123
|
-
Type.Object({ type: Type.Literal("interval"), every: Type.String() }),
|
|
124
|
-
Type.Object({ type: Type.Literal("cron"), expr: Type.String() }),
|
|
125
|
-
]),
|
|
126
|
-
});
|
|
127
|
-
expect(rewrite("task", params, { schedule: { type: "once" } })).toBe(
|
|
128
|
-
'Validation failed for tool "task":\n - missing required property at schedule: at'
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("StringEnum lists valid values (bare strings)", () => {
|
|
133
|
-
const params = Type.Object({
|
|
134
|
-
outputMode: StringEnum(["files_with_matches", "content", "count"]),
|
|
135
|
-
});
|
|
136
|
-
expect(rewrite("grep", params, { outputMode: "invalid_enum_value" })).toBe(
|
|
137
|
-
'Validation failed for tool "grep":\n - outputMode: must be one of: files_with_matches, content, count'
|
|
138
|
-
);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("Tools.wrap quoted-enum coercion", () => {
|
|
143
|
-
function wrapTool(params: TSchema) {
|
|
144
|
-
return Tools.wrap({
|
|
145
|
-
name: "task",
|
|
146
|
-
label: "task",
|
|
147
|
-
description: "test",
|
|
148
|
-
parameters: params,
|
|
149
|
-
async execute() {
|
|
150
|
-
return { content: [{ type: "text", text: "" }], details: {} };
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
test("unwraps double-quoted enum value", () => {
|
|
156
|
-
const wrapped = wrapTool(
|
|
157
|
-
Type.Object({
|
|
158
|
-
action: Type.Union([Type.Literal("create"), Type.Literal("list")]),
|
|
159
|
-
})
|
|
160
|
-
);
|
|
161
|
-
expect(wrapped.prepareArguments!({ action: '"create"' })).toEqual({
|
|
162
|
-
action: "create",
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("unwraps single-quoted enum value", () => {
|
|
167
|
-
const wrapped = wrapTool(Type.Object({ mode: StringEnum(["foo", "bar"]) }));
|
|
168
|
-
expect(wrapped.prepareArguments!({ mode: "'bar'" })).toEqual({
|
|
169
|
-
mode: "bar",
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("unwraps backtick-quoted enum value", () => {
|
|
174
|
-
const wrapped = wrapTool(Type.Object({ mode: StringEnum(["foo", "bar"]) }));
|
|
175
|
-
expect(wrapped.prepareArguments!({ mode: "`foo`" })).toEqual({
|
|
176
|
-
mode: "foo",
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("does NOT unwrap when inner value is invalid (still errors)", () => {
|
|
181
|
-
const wrapped = wrapTool(
|
|
182
|
-
Type.Object({
|
|
183
|
-
action: Type.Union([Type.Literal("create"), Type.Literal("list")]),
|
|
184
|
-
})
|
|
185
|
-
);
|
|
186
|
-
expect(() => wrapped.prepareArguments!({ action: '"nope"' })).toThrow(
|
|
187
|
-
'Validation failed for tool "task":\n - action: must be one of: create, list'
|
|
188
|
-
);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("unwraps nested enum inside tagged union branch", () => {
|
|
192
|
-
const wrapped = wrapTool(
|
|
193
|
-
Type.Object({
|
|
194
|
-
schedule: Type.Union([
|
|
195
|
-
Type.Object({ type: Type.Literal("once"), at: Type.String() }),
|
|
196
|
-
Type.Object({
|
|
197
|
-
type: Type.Literal("interval"),
|
|
198
|
-
every: Type.String(),
|
|
199
|
-
}),
|
|
200
|
-
]),
|
|
201
|
-
})
|
|
202
|
-
);
|
|
203
|
-
expect(
|
|
204
|
-
wrapped.prepareArguments!({
|
|
205
|
-
schedule: { type: '"once"', at: "2026-01-01T00:00:00Z" },
|
|
206
|
-
})
|
|
207
|
-
).toEqual({
|
|
208
|
-
schedule: { type: "once", at: "2026-01-01T00:00:00Z" },
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("leaves non-enum string fields alone", () => {
|
|
213
|
-
const wrapped = wrapTool(
|
|
214
|
-
Type.Object({ prompt: Type.String(), action: StringEnum(["a"]) })
|
|
215
|
-
);
|
|
216
|
-
expect(
|
|
217
|
-
wrapped.prepareArguments!({ prompt: '"hello"', action: "a" })
|
|
218
|
-
).toEqual({ prompt: '"hello"', action: "a" });
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe("Tools.wrap strict type checks", () => {
|
|
223
|
-
function wrapTool(params: TSchema) {
|
|
224
|
-
return Tools.wrap({
|
|
225
|
-
name: "t",
|
|
226
|
-
label: "t",
|
|
227
|
-
description: "test",
|
|
228
|
-
parameters: params,
|
|
229
|
-
async execute() {
|
|
230
|
-
return { content: [{ type: "text", text: "" }], details: {} };
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
test('rejects null for string field instead of coercing to "null"', () => {
|
|
236
|
-
const wrapped = wrapTool(Type.Object({ path: Type.String() }));
|
|
237
|
-
expect(() => wrapped.prepareArguments!({ path: null })).toThrow(
|
|
238
|
-
'Validation failed for tool "t":\n - path: must not be null (expected string)'
|
|
239
|
-
);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("rejects null for integer field instead of coercing to 0", () => {
|
|
243
|
-
const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
|
|
244
|
-
expect(() => wrapped.prepareArguments!({ n: null })).toThrow(
|
|
245
|
-
'Validation failed for tool "t":\n - n: must not be null (expected integer)'
|
|
246
|
-
);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("rejects null for boolean field instead of coercing to false", () => {
|
|
250
|
-
const wrapped = wrapTool(Type.Object({ b: Type.Boolean() }));
|
|
251
|
-
expect(() => wrapped.prepareArguments!({ b: null })).toThrow(
|
|
252
|
-
'Validation failed for tool "t":\n - b: must not be null (expected boolean)'
|
|
253
|
-
);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test("rejects float string for integer field instead of truncating", () => {
|
|
257
|
-
const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
|
|
258
|
-
expect(() => wrapped.prepareArguments!({ n: "42.5" })).toThrow(
|
|
259
|
-
'Validation failed for tool "t":\n - n: must be an integer (received "42.5" — fractional part would be truncated)'
|
|
260
|
-
);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("integer string with no fractional part still coerces", () => {
|
|
264
|
-
const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
|
|
265
|
-
expect(wrapped.prepareArguments!({ n: "42" })).toEqual({ n: 42 });
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("integer string like '42.0' is allowed (no precision loss)", () => {
|
|
269
|
-
const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
|
|
270
|
-
expect(wrapped.prepareArguments!({ n: "42.0" })).toEqual({ n: 42 });
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("string-to-bool coercion still works (defensible LLM quirk)", () => {
|
|
274
|
-
const wrapped = wrapTool(Type.Object({ b: Type.Boolean() }));
|
|
275
|
-
expect(wrapped.prepareArguments!({ b: "true" })).toEqual({ b: true });
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("null in a nested field is also rejected", () => {
|
|
279
|
-
const wrapped = wrapTool(
|
|
280
|
-
Type.Object({
|
|
281
|
-
edits: Type.Array(Type.Object({ value: Type.String() })),
|
|
282
|
-
})
|
|
283
|
-
);
|
|
284
|
-
expect(() =>
|
|
285
|
-
wrapped.prepareArguments!({ edits: [{ value: null }] })
|
|
286
|
-
).toThrow(
|
|
287
|
-
'Validation failed for tool "t":\n - edits.0.value: must not be null (expected string)'
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("null is not rejected when schema explicitly accepts it", () => {
|
|
292
|
-
const wrapped = wrapTool(
|
|
293
|
-
Type.Object({ x: Type.Union([Type.String(), Type.Null()]) })
|
|
294
|
-
);
|
|
295
|
-
expect(() => wrapped.prepareArguments!({ x: null })).not.toThrow();
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
describe("Tools.wrap unknown property detection", () => {
|
|
300
|
-
test("rejects unknown top-level key", () => {
|
|
301
|
-
const params = Type.Object({ command: Type.String() });
|
|
302
|
-
const wrapped = Tools.wrap({
|
|
303
|
-
name: "bash",
|
|
304
|
-
label: "bash",
|
|
305
|
-
description: "test",
|
|
306
|
-
parameters: params,
|
|
307
|
-
async execute() {
|
|
308
|
-
return { content: [{ type: "text", text: "" }], details: {} };
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
expect(() =>
|
|
312
|
-
wrapped.prepareArguments!({ command: "ls", fakeParam: "x" })
|
|
313
|
-
).toThrow(
|
|
314
|
-
'Validation failed for tool "bash":\n - unknown property: fakeParam'
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("suggests close matches by edit distance", () => {
|
|
319
|
-
const params = Type.Object({ headLimit: Type.Integer() });
|
|
320
|
-
const wrapped = Tools.wrap({
|
|
321
|
-
name: "grep",
|
|
322
|
-
label: "grep",
|
|
323
|
-
description: "test",
|
|
324
|
-
parameters: params,
|
|
325
|
-
async execute() {
|
|
326
|
-
return { content: [{ type: "text", text: "" }], details: {} };
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
expect(() =>
|
|
330
|
-
wrapped.prepareArguments!({ headLimit: 1, headlimit: 1 })
|
|
331
|
-
).toThrow(
|
|
332
|
-
'Validation failed for tool "grep":\n - unknown property: headlimit (did you mean "headLimit"?)'
|
|
333
|
-
);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe("Tools.wrap", () => {
|
|
338
|
-
test("prepareArguments rewrites the thrown message", () => {
|
|
339
|
-
const params = Type.Object({ path: Type.String() });
|
|
340
|
-
const wrapped = Tools.wrap({
|
|
341
|
-
name: "read",
|
|
342
|
-
label: "read",
|
|
343
|
-
description: "test",
|
|
344
|
-
parameters: params,
|
|
345
|
-
async execute() {
|
|
346
|
-
return { content: [{ type: "text", text: "" }], details: {} };
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
expect(() => wrapped.prepareArguments!({})).toThrow(
|
|
350
|
-
'Validation failed for tool "read":\n - missing required property: path'
|
|
351
|
-
);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test("prepareArguments returns coerced args on success", () => {
|
|
355
|
-
const params = Type.Object({ count: Type.Integer() });
|
|
356
|
-
const wrapped = Tools.wrap({
|
|
357
|
-
name: "t",
|
|
358
|
-
label: "t",
|
|
359
|
-
description: "test",
|
|
360
|
-
parameters: params,
|
|
361
|
-
async execute() {
|
|
362
|
-
return { content: [{ type: "text", text: "" }], details: {} };
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
expect(wrapped.prepareArguments!({ count: "42" })).toEqual({ count: 42 });
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe("Tools.register", () => {
|
|
370
|
-
test("forwards the wrapped def to pi.registerTool", () => {
|
|
371
|
-
const params = Type.Object({ path: Type.String() });
|
|
372
|
-
let captured: ReturnType<typeof Tools.wrap> | undefined;
|
|
373
|
-
const fakePi = {
|
|
374
|
-
registerTool(def: ReturnType<typeof Tools.wrap>) {
|
|
375
|
-
captured = def;
|
|
376
|
-
},
|
|
377
|
-
};
|
|
378
|
-
Tools.register(fakePi as never, {
|
|
379
|
-
name: "read",
|
|
380
|
-
label: "read",
|
|
381
|
-
description: "test",
|
|
382
|
-
parameters: params,
|
|
383
|
-
async execute() {
|
|
384
|
-
return { content: [{ type: "text", text: "" }], details: {} };
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
expect(captured?.prepareArguments).toBeDefined();
|
|
388
|
-
expect(() => captured!.prepareArguments!({})).toThrow(
|
|
389
|
-
'Validation failed for tool "read":\n - missing required property: path'
|
|
390
|
-
);
|
|
391
|
-
});
|
|
392
|
-
});
|