@oh-my-pi/pi-coding-agent 5.7.67 → 5.7.69

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 (53) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -6
  3. package/package.json +8 -7
  4. package/src/migrations.ts +1 -34
  5. package/src/vendor/photon/index.js +4 -2
  6. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  7. package/src/core/python-executor-display.test.ts +0 -42
  8. package/src/core/python-executor-lifecycle.test.ts +0 -99
  9. package/src/core/python-executor-mapping.test.ts +0 -41
  10. package/src/core/python-executor-per-call.test.ts +0 -49
  11. package/src/core/python-executor-session.test.ts +0 -103
  12. package/src/core/python-executor-streaming.test.ts +0 -77
  13. package/src/core/python-executor-timeout.test.ts +0 -35
  14. package/src/core/python-executor.lifecycle.test.ts +0 -139
  15. package/src/core/python-executor.result.test.ts +0 -49
  16. package/src/core/python-executor.test.ts +0 -180
  17. package/src/core/python-kernel-display.test.ts +0 -54
  18. package/src/core/python-kernel-env.test.ts +0 -138
  19. package/src/core/python-kernel-session.test.ts +0 -87
  20. package/src/core/python-kernel-ws.test.ts +0 -104
  21. package/src/core/python-kernel.lifecycle.test.ts +0 -249
  22. package/src/core/python-kernel.test.ts +0 -461
  23. package/src/core/python-modules.test.ts +0 -102
  24. package/src/core/python-prelude.test.ts +0 -140
  25. package/src/core/settings-manager-python.test.ts +0 -23
  26. package/src/core/streaming-output.test.ts +0 -26
  27. package/src/core/system-prompt.python.test.ts +0 -17
  28. package/src/core/tools/index.test.ts +0 -212
  29. package/src/core/tools/python-execution.test.ts +0 -68
  30. package/src/core/tools/python-fallback.test.ts +0 -72
  31. package/src/core/tools/python-renderer.test.ts +0 -36
  32. package/src/core/tools/python-tool-mode.test.ts +0 -43
  33. package/src/core/tools/python.test.ts +0 -121
  34. package/src/core/tools/schema-validation.test.ts +0 -530
  35. package/src/core/tools/web-scrapers/academic.test.ts +0 -239
  36. package/src/core/tools/web-scrapers/business.test.ts +0 -82
  37. package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
  38. package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
  39. package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
  40. package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
  41. package/src/core/tools/web-scrapers/media.test.ts +0 -138
  42. package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
  43. package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
  44. package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
  45. package/src/core/tools/web-scrapers/research.test.ts +0 -107
  46. package/src/core/tools/web-scrapers/security.test.ts +0 -103
  47. package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
  48. package/src/core/tools/web-scrapers/social.test.ts +0 -259
  49. package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
  50. package/src/core/tools/web-scrapers/standards.test.ts +0 -122
  51. package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
  52. package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
  53. package/src/discovery/helpers.test.ts +0 -131
@@ -1,530 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { sanitizeSchemaForGoogle } from "@oh-my-pi/pi-ai";
3
- import { BUILTIN_TOOLS, createTools, HIDDEN_TOOLS, type ToolSession } from "./index";
4
-
5
- /**
6
- * Problematic JSON Schema features that cause issues with various providers.
7
- *
8
- * These are checked AFTER sanitization (sanitizeSchemaForGoogle) is applied,
9
- * so features like `const` that are transformed by sanitization are not flagged.
10
- *
11
- * Prohibited (error):
12
- * - $schema: Explicit schema declarations
13
- * - $ref / $defs: Schema references (must inline everything)
14
- * - prefixItems: Draft 2020-12 feature (use items array)
15
- * - $dynamicRef / $dynamicAnchor: Draft 2020-12 features
16
- * - unevaluatedProperties / unevaluatedItems: Draft 2020-12 features
17
- * - const: Should be converted to enum by sanitization
18
- * - examples: Should be stripped
19
- *
20
- * Warnings (non-blocking):
21
- * - additionalProperties: false - Sometimes causes validation issues
22
- * - format: Some validators don't recognize format keywords
23
- */
24
-
25
- const PROHIBITED_KEYS = new Set([
26
- "$schema",
27
- "$ref",
28
- "$defs",
29
- "$dynamicRef",
30
- "$dynamicAnchor",
31
- "prefixItems",
32
- "unevaluatedProperties",
33
- "unevaluatedItems",
34
- "const", // Should be converted to enum by sanitizeSchemaForGoogle
35
- "examples",
36
- ]);
37
-
38
- const WARNING_KEYS = new Set(["additionalProperties", "format"]);
39
-
40
- interface SchemaViolation {
41
- path: string;
42
- key: string;
43
- value: unknown;
44
- severity: "error" | "warning";
45
- }
46
-
47
- function validateSchema(schema: unknown, path = "root"): SchemaViolation[] {
48
- const violations: SchemaViolation[] = [];
49
-
50
- if (schema === null || typeof schema !== "object") {
51
- return violations;
52
- }
53
-
54
- if (Array.isArray(schema)) {
55
- for (let i = 0; i < schema.length; i++) {
56
- violations.push(...validateSchema(schema[i], `${path}[${i}]`));
57
- }
58
- return violations;
59
- }
60
-
61
- const obj = schema as Record<string, unknown>;
62
-
63
- for (const [key, value] of Object.entries(obj)) {
64
- const currentPath = `${path}.${key}`;
65
-
66
- if (PROHIBITED_KEYS.has(key)) {
67
- violations.push({
68
- path: currentPath,
69
- key,
70
- value,
71
- severity: "error",
72
- });
73
- }
74
-
75
- if (WARNING_KEYS.has(key)) {
76
- // additionalProperties: false is the problematic case
77
- if (key === "additionalProperties" && value === false) {
78
- violations.push({
79
- path: currentPath,
80
- key,
81
- value,
82
- severity: "warning",
83
- });
84
- }
85
- // format is always potentially problematic
86
- if (key === "format" && typeof value === "string") {
87
- violations.push({
88
- path: currentPath,
89
- key,
90
- value,
91
- severity: "warning",
92
- });
93
- }
94
- }
95
-
96
- // Recurse into nested objects
97
- if (value !== null && typeof value === "object") {
98
- violations.push(...validateSchema(value, currentPath));
99
- }
100
- }
101
-
102
- return violations;
103
- }
104
-
105
- function createTestSession(): ToolSession {
106
- return {
107
- cwd: "/tmp/test",
108
- hasUI: true,
109
- getSessionFile: () => null,
110
- getSessionSpawns: () => "*",
111
- };
112
- }
113
-
114
- describe("sanitizeSchemaForGoogle", () => {
115
- it("converts const to enum", () => {
116
- const schema = { type: "string", const: "active" };
117
- const sanitized = sanitizeSchemaForGoogle(schema);
118
- expect(sanitized).toEqual({ type: "string", enum: ["active"] });
119
- });
120
-
121
- it("merges const into existing enum", () => {
122
- const schema = { type: "string", const: "active", enum: ["inactive"] };
123
- const sanitized = sanitizeSchemaForGoogle(schema);
124
- expect(sanitized).toEqual({ type: "string", enum: ["inactive", "active"] });
125
- });
126
-
127
- it("does not duplicate const in enum", () => {
128
- const schema = { type: "string", const: "active", enum: ["active", "inactive"] };
129
- const sanitized = sanitizeSchemaForGoogle(schema);
130
- expect(sanitized).toEqual({ type: "string", enum: ["active", "inactive"] });
131
- });
132
-
133
- it("collapses anyOf with const values into enum", () => {
134
- const schema = {
135
- anyOf: [
136
- { type: "string", const: "file" },
137
- { type: "string", const: "dir" },
138
- ],
139
- };
140
- const sanitized = sanitizeSchemaForGoogle(schema);
141
- // anyOf with all const values should collapse into a single enum
142
- expect(sanitized).toEqual({
143
- type: "string",
144
- enum: ["file", "dir"],
145
- });
146
- });
147
-
148
- it("handles deeply nested schemas", () => {
149
- const schema = {
150
- type: "object",
151
- properties: {
152
- nested: {
153
- type: "object",
154
- properties: {
155
- status: { type: "string", const: "active" },
156
- },
157
- },
158
- },
159
- };
160
- const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
161
- const props = sanitized.properties as Record<string, unknown>;
162
- const nested = props.nested as Record<string, unknown>;
163
- const nestedProps = nested.properties as Record<string, unknown>;
164
- const status = nestedProps.status as Record<string, unknown>;
165
- expect(status.const).toBeUndefined();
166
- expect(status.enum).toEqual(["active"]);
167
- });
168
-
169
- it("preserves other schema properties", () => {
170
- const schema = {
171
- type: "string",
172
- const: "value",
173
- description: "A description",
174
- minLength: 1,
175
- };
176
- const sanitized = sanitizeSchemaForGoogle(schema);
177
- expect(sanitized).toEqual({
178
- type: "string",
179
- enum: ["value"],
180
- description: "A description",
181
- });
182
- });
183
-
184
- it("handles arrays correctly", () => {
185
- const schema = {
186
- type: "array",
187
- items: { type: "string", const: "only" },
188
- };
189
- const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
190
- const items = sanitized.items as Record<string, unknown>;
191
- expect(items.const).toBeUndefined();
192
- expect(items.enum).toEqual(["only"]);
193
- });
194
-
195
- it("passes through primitives unchanged", () => {
196
- expect(sanitizeSchemaForGoogle("string")).toBe("string");
197
- expect(sanitizeSchemaForGoogle(123)).toBe(123);
198
- expect(sanitizeSchemaForGoogle(true)).toBe(true);
199
- expect(sanitizeSchemaForGoogle(null)).toBe(null);
200
- });
201
-
202
- it("preserves property names that match schema keywords (e.g., 'pattern')", () => {
203
- const schema = {
204
- type: "object",
205
- properties: {
206
- pattern: { type: "string", description: "The search pattern" },
207
- format: { type: "string", description: "Output format" },
208
- },
209
- required: ["pattern"],
210
- };
211
- const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
212
- const props = sanitized.properties as Record<string, unknown>;
213
- expect(props.pattern).toEqual({ type: "string", description: "The search pattern" });
214
- expect(props.format).toEqual({ type: "string", description: "Output format" });
215
- expect(sanitized.required).toEqual(["pattern"]);
216
- });
217
-
218
- it("still strips schema keywords from non-properties contexts", () => {
219
- const schema = {
220
- type: "string",
221
- pattern: "^[a-z]+$",
222
- format: "email",
223
- minLength: 1,
224
- };
225
- const sanitized = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
226
- expect(sanitized.pattern).toBeUndefined();
227
- expect(sanitized.format).toBeUndefined();
228
- expect(sanitized.minLength).toBeUndefined();
229
- expect(sanitized.type).toBe("string");
230
- });
231
- });
232
-
233
- describe("tool schema validation (post-sanitization)", () => {
234
- it("all builtin tool schemas are valid after sanitization", async () => {
235
- const session = createTestSession();
236
- const tools = await createTools(session);
237
-
238
- const allViolations: { tool: string; violations: SchemaViolation[] }[] = [];
239
-
240
- for (const tool of tools) {
241
- const schema = tool.parameters;
242
- if (!schema) continue;
243
-
244
- // Apply the same sanitization that happens before sending to providers
245
- const sanitized = sanitizeSchemaForGoogle(schema);
246
- const violations = validateSchema(sanitized, tool.name);
247
- const errors = violations.filter((v) => v.severity === "error");
248
-
249
- if (errors.length > 0) {
250
- allViolations.push({ tool: tool.name, violations: errors });
251
- }
252
- }
253
-
254
- if (allViolations.length > 0) {
255
- const message = allViolations
256
- .map(({ tool, violations }) => {
257
- const details = violations.map((v) => ` - ${v.path}: ${v.key} = ${JSON.stringify(v.value)}`).join("\n");
258
- return `${tool}:\n${details}`;
259
- })
260
- .join("\n\n");
261
-
262
- throw new Error(`Prohibited JSON Schema features found after sanitization:\n\n${message}`);
263
- }
264
-
265
- expect(allViolations).toEqual([]);
266
- });
267
-
268
- it("no sanitized schema contains $schema declaration", async () => {
269
- const session = createTestSession();
270
- const tools = await createTools(session);
271
-
272
- for (const tool of tools) {
273
- const schema = tool.parameters;
274
- if (!schema) continue;
275
-
276
- const sanitized = sanitizeSchemaForGoogle(schema);
277
- const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "$schema");
278
- expect(violations).toEqual([]);
279
- }
280
- });
281
-
282
- it("no sanitized schema contains $ref or $defs", async () => {
283
- const session = createTestSession();
284
- const tools = await createTools(session);
285
-
286
- for (const tool of tools) {
287
- const schema = tool.parameters;
288
- if (!schema) continue;
289
-
290
- const sanitized = sanitizeSchemaForGoogle(schema);
291
- const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "$ref" || v.key === "$defs");
292
- expect(violations).toEqual([]);
293
- }
294
- });
295
-
296
- it("no sanitized schema contains Draft 2020-12 specific features", async () => {
297
- const session = createTestSession();
298
- const tools = await createTools(session);
299
-
300
- const draft2020Features = [
301
- "prefixItems",
302
- "$dynamicRef",
303
- "$dynamicAnchor",
304
- "unevaluatedProperties",
305
- "unevaluatedItems",
306
- ];
307
-
308
- for (const tool of tools) {
309
- const schema = tool.parameters;
310
- if (!schema) continue;
311
-
312
- const sanitized = sanitizeSchemaForGoogle(schema);
313
- const violations = validateSchema(sanitized, tool.name).filter((v) => draft2020Features.includes(v.key));
314
- expect(violations).toEqual([]);
315
- }
316
- });
317
-
318
- it("sanitization removes const (converts to enum)", async () => {
319
- const session = createTestSession();
320
- const tools = await createTools(session);
321
-
322
- for (const tool of tools) {
323
- const schema = tool.parameters;
324
- if (!schema) continue;
325
-
326
- const sanitized = sanitizeSchemaForGoogle(schema);
327
- const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "const");
328
- expect(violations).toEqual([]);
329
- }
330
- });
331
-
332
- it("no sanitized schema contains examples field", async () => {
333
- const session = createTestSession();
334
- const tools = await createTools(session);
335
-
336
- for (const tool of tools) {
337
- const schema = tool.parameters;
338
- if (!schema) continue;
339
-
340
- const sanitized = sanitizeSchemaForGoogle(schema);
341
- const violations = validateSchema(sanitized, tool.name).filter((v) => v.key === "examples");
342
- expect(violations).toEqual([]);
343
- }
344
- });
345
-
346
- it("hidden tools also have valid sanitized schemas", async () => {
347
- const session = createTestSession();
348
-
349
- for (const [name, factory] of Object.entries(HIDDEN_TOOLS)) {
350
- const tool = await factory(session);
351
- if (!tool) continue;
352
-
353
- const schema = tool.parameters;
354
- if (!schema) continue;
355
-
356
- const sanitized = sanitizeSchemaForGoogle(schema);
357
- const violations = validateSchema(sanitized, name);
358
- const errors = violations.filter((v) => v.severity === "error");
359
-
360
- if (errors.length > 0) {
361
- const details = errors.map((v) => ` - ${v.path}: ${v.key} = ${JSON.stringify(v.value)}`).join("\n");
362
- throw new Error(`Hidden tool ${name} has prohibited schema features after sanitization:\n${details}`);
363
- }
364
- }
365
- });
366
-
367
- it("BUILTIN_TOOLS registry matches expected tools", () => {
368
- const expectedTools = [
369
- "ask",
370
- "bash",
371
- "calc",
372
- "ssh",
373
- "edit",
374
- "find",
375
- "git",
376
- "grep",
377
- "ls",
378
- "lsp",
379
- "notebook",
380
- "output",
381
- "python",
382
- "read",
383
- "task",
384
- "todo_write",
385
- "web_fetch",
386
- "web_search",
387
- "write",
388
- ];
389
-
390
- expect(Object.keys(BUILTIN_TOOLS).sort()).toEqual(expectedTools.sort());
391
- });
392
-
393
- it("logs warnings for potentially problematic features (non-blocking)", async () => {
394
- const session = createTestSession();
395
- const tools = await createTools(session);
396
-
397
- const warnings: { tool: string; violations: SchemaViolation[] }[] = [];
398
-
399
- for (const tool of tools) {
400
- const schema = tool.parameters;
401
- if (!schema) continue;
402
-
403
- const sanitized = sanitizeSchemaForGoogle(schema);
404
- const violations = validateSchema(sanitized, tool.name);
405
- const toolWarnings = violations.filter((v) => v.severity === "warning");
406
-
407
- if (toolWarnings.length > 0) {
408
- warnings.push({ tool: tool.name, violations: toolWarnings });
409
- }
410
- }
411
-
412
- // Log warnings but don't fail - these are advisory
413
- if (warnings.length > 0) {
414
- const message = warnings
415
- .map(({ tool, violations }) => {
416
- const details = violations.map((v) => ` - ${v.path}: ${v.key} = ${JSON.stringify(v.value)}`).join("\n");
417
- return `${tool}:\n${details}`;
418
- })
419
- .join("\n\n");
420
-
421
- console.log(`Schema warnings (non-blocking):\n\n${message}`);
422
- }
423
-
424
- // This test passes regardless - warnings are informational
425
- expect(true).toBe(true);
426
- });
427
- });
428
-
429
- describe("validateSchema helper", () => {
430
- it("detects $schema declarations", () => {
431
- const schema = { $schema: "http://json-schema.org/draft-07/schema#", type: "object" };
432
- const violations = validateSchema(schema);
433
- expect(violations.some((v) => v.key === "$schema")).toBe(true);
434
- });
435
-
436
- it("detects $ref usage", () => {
437
- const schema = { type: "object", properties: { foo: { $ref: "#/$defs/Foo" } } };
438
- const violations = validateSchema(schema);
439
- expect(violations.some((v) => v.key === "$ref")).toBe(true);
440
- });
441
-
442
- it("detects $defs usage", () => {
443
- const schema = { $defs: { Foo: { type: "string" } }, type: "object" };
444
- const violations = validateSchema(schema);
445
- expect(violations.some((v) => v.key === "$defs")).toBe(true);
446
- });
447
-
448
- it("detects const usage (should be sanitized away)", () => {
449
- const schema = { type: "object", properties: { status: { const: "active" } } };
450
- const violations = validateSchema(schema);
451
- expect(violations.some((v) => v.key === "const")).toBe(true);
452
- });
453
-
454
- it("detects examples field", () => {
455
- const schema = { type: "string", examples: ["foo", "bar"] };
456
- const violations = validateSchema(schema);
457
- expect(violations.some((v) => v.key === "examples")).toBe(true);
458
- });
459
-
460
- it("detects prefixItems (Draft 2020-12)", () => {
461
- const schema = { type: "array", prefixItems: [{ type: "string" }] };
462
- const violations = validateSchema(schema);
463
- expect(violations.some((v) => v.key === "prefixItems")).toBe(true);
464
- });
465
-
466
- it("detects unevaluatedProperties (Draft 2020-12)", () => {
467
- const schema = { type: "object", unevaluatedProperties: false };
468
- const violations = validateSchema(schema);
469
- expect(violations.some((v) => v.key === "unevaluatedProperties")).toBe(true);
470
- });
471
-
472
- it("warns on additionalProperties: false", () => {
473
- const schema = { type: "object", additionalProperties: false };
474
- const violations = validateSchema(schema);
475
- const warning = violations.find((v) => v.key === "additionalProperties");
476
- expect(warning?.severity).toBe("warning");
477
- });
478
-
479
- it("does not warn on additionalProperties: true", () => {
480
- const schema = { type: "object", additionalProperties: true };
481
- const violations = validateSchema(schema);
482
- expect(violations.some((v) => v.key === "additionalProperties")).toBe(false);
483
- });
484
-
485
- it("warns on format keyword", () => {
486
- const schema = { type: "string", format: "uri" };
487
- const violations = validateSchema(schema);
488
- const warning = violations.find((v) => v.key === "format");
489
- expect(warning?.severity).toBe("warning");
490
- });
491
-
492
- it("recursively validates nested schemas", () => {
493
- const schema = {
494
- type: "object",
495
- properties: {
496
- nested: {
497
- type: "object",
498
- properties: {
499
- deep: { $ref: "#/$defs/Deep" },
500
- },
501
- },
502
- },
503
- };
504
- const violations = validateSchema(schema);
505
- expect(violations.some((v) => v.key === "$ref")).toBe(true);
506
- expect(violations.find((v) => v.key === "$ref")?.path).toContain("nested");
507
- });
508
-
509
- it("validates array items", () => {
510
- const schema = {
511
- type: "array",
512
- items: [{ const: "first" }, { type: "string" }],
513
- };
514
- const violations = validateSchema(schema);
515
- expect(violations.some((v) => v.key === "const")).toBe(true);
516
- });
517
-
518
- it("returns empty array for valid schema", () => {
519
- const schema = {
520
- type: "object",
521
- properties: {
522
- name: { type: "string", description: "User name" },
523
- age: { type: "number", minimum: 0 },
524
- },
525
- required: ["name"],
526
- };
527
- const violations = validateSchema(schema);
528
- expect(violations.filter((v) => v.severity === "error")).toEqual([]);
529
- });
530
- });