|
1 | 1 | import { describe, it, expect } from "vitest" |
2 | 2 | import type OpenAI from "openai" |
3 | | -import type { ModeConfig } from "@roo-code/types" |
4 | | -import { filterNativeToolsForMode, filterMcpToolsForMode } from "../filter-tools-for-mode" |
| 3 | +import type { ModeConfig, ModelInfo } from "@roo-code/types" |
| 4 | +import { filterNativeToolsForMode, filterMcpToolsForMode, applyModelToolCustomization } from "../filter-tools-for-mode" |
5 | 5 |
|
6 | 6 | describe("filterNativeToolsForMode", () => { |
7 | 7 | const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [ |
@@ -467,4 +467,265 @@ describe("filterMcpToolsForMode", () => { |
467 | 467 | // Should include MCP tools since default mode has mcp group |
468 | 468 | expect(filtered.length).toBeGreaterThan(0) |
469 | 469 | }) |
| 470 | + |
| 471 | + describe("applyModelToolCustomization", () => { |
| 472 | + const codeMode: ModeConfig = { |
| 473 | + slug: "code", |
| 474 | + name: "Code", |
| 475 | + roleDefinition: "Test", |
| 476 | + groups: ["read", "edit", "browser", "command", "mcp"] as const, |
| 477 | + } |
| 478 | + |
| 479 | + const architectMode: ModeConfig = { |
| 480 | + slug: "architect", |
| 481 | + name: "Architect", |
| 482 | + roleDefinition: "Test", |
| 483 | + groups: ["read", "browser", "mcp"] as const, |
| 484 | + } |
| 485 | + |
| 486 | + it("should return original tools when modelInfo is undefined", () => { |
| 487 | + const tools = new Set(["read_file", "write_to_file", "apply_diff"]) |
| 488 | + const result = applyModelToolCustomization(tools, codeMode, undefined) |
| 489 | + expect(result).toEqual(tools) |
| 490 | + }) |
| 491 | + |
| 492 | + it("should exclude tools specified in excludedTools", () => { |
| 493 | + const tools = new Set(["read_file", "write_to_file", "apply_diff"]) |
| 494 | + const modelInfo: ModelInfo = { |
| 495 | + contextWindow: 100000, |
| 496 | + supportsPromptCache: false, |
| 497 | + excludedTools: ["apply_diff"], |
| 498 | + } |
| 499 | + const result = applyModelToolCustomization(tools, codeMode, modelInfo) |
| 500 | + expect(result.has("read_file")).toBe(true) |
| 501 | + expect(result.has("write_to_file")).toBe(true) |
| 502 | + expect(result.has("apply_diff")).toBe(false) |
| 503 | + }) |
| 504 | + |
| 505 | + it("should exclude multiple tools", () => { |
| 506 | + const tools = new Set(["read_file", "write_to_file", "apply_diff", "execute_command"]) |
| 507 | + const modelInfo: ModelInfo = { |
| 508 | + contextWindow: 100000, |
| 509 | + supportsPromptCache: false, |
| 510 | + excludedTools: ["apply_diff", "write_to_file"], |
| 511 | + } |
| 512 | + const result = applyModelToolCustomization(tools, codeMode, modelInfo) |
| 513 | + expect(result.has("read_file")).toBe(true) |
| 514 | + expect(result.has("execute_command")).toBe(true) |
| 515 | + expect(result.has("write_to_file")).toBe(false) |
| 516 | + expect(result.has("apply_diff")).toBe(false) |
| 517 | + }) |
| 518 | + |
| 519 | + it("should include tools only if they belong to allowed groups", () => { |
| 520 | + const tools = new Set(["read_file"]) |
| 521 | + const modelInfo: ModelInfo = { |
| 522 | + contextWindow: 100000, |
| 523 | + supportsPromptCache: false, |
| 524 | + includedTools: ["write_to_file", "apply_diff"], // Both in edit group |
| 525 | + } |
| 526 | + const result = applyModelToolCustomization(tools, codeMode, modelInfo) |
| 527 | + expect(result.has("read_file")).toBe(true) |
| 528 | + expect(result.has("write_to_file")).toBe(true) |
| 529 | + expect(result.has("apply_diff")).toBe(true) |
| 530 | + }) |
| 531 | + |
| 532 | + it("should NOT include tools from groups not allowed by mode", () => { |
| 533 | + const tools = new Set(["read_file"]) |
| 534 | + const modelInfo: ModelInfo = { |
| 535 | + contextWindow: 100000, |
| 536 | + supportsPromptCache: false, |
| 537 | + includedTools: ["write_to_file", "apply_diff"], // Edit group tools |
| 538 | + } |
| 539 | + // Architect mode doesn't have edit group |
| 540 | + const result = applyModelToolCustomization(tools, architectMode, modelInfo) |
| 541 | + expect(result.has("read_file")).toBe(true) |
| 542 | + expect(result.has("write_to_file")).toBe(false) // Not in allowed groups |
| 543 | + expect(result.has("apply_diff")).toBe(false) // Not in allowed groups |
| 544 | + }) |
| 545 | + |
| 546 | + it("should apply both exclude and include operations", () => { |
| 547 | + const tools = new Set(["read_file", "write_to_file", "apply_diff"]) |
| 548 | + const modelInfo: ModelInfo = { |
| 549 | + contextWindow: 100000, |
| 550 | + supportsPromptCache: false, |
| 551 | + excludedTools: ["apply_diff"], |
| 552 | + includedTools: ["insert_content"], // Another edit tool |
| 553 | + } |
| 554 | + const result = applyModelToolCustomization(tools, codeMode, modelInfo) |
| 555 | + expect(result.has("read_file")).toBe(true) |
| 556 | + expect(result.has("write_to_file")).toBe(true) |
| 557 | + expect(result.has("apply_diff")).toBe(false) // Excluded |
| 558 | + expect(result.has("insert_content")).toBe(true) // Included |
| 559 | + }) |
| 560 | + |
| 561 | + it("should handle empty excludedTools and includedTools arrays", () => { |
| 562 | + const tools = new Set(["read_file", "write_to_file"]) |
| 563 | + const modelInfo: ModelInfo = { |
| 564 | + contextWindow: 100000, |
| 565 | + supportsPromptCache: false, |
| 566 | + excludedTools: [], |
| 567 | + includedTools: [], |
| 568 | + } |
| 569 | + const result = applyModelToolCustomization(tools, codeMode, modelInfo) |
| 570 | + expect(result).toEqual(tools) |
| 571 | + }) |
| 572 | + |
| 573 | + it("should ignore excluded tools that are not in the original set", () => { |
| 574 | + const tools = new Set(["read_file", "write_to_file"]) |
| 575 | + const modelInfo: ModelInfo = { |
| 576 | + contextWindow: 100000, |
| 577 | + supportsPromptCache: false, |
| 578 | + excludedTools: ["apply_diff", "nonexistent_tool"], |
| 579 | + } |
| 580 | + const result = applyModelToolCustomization(tools, codeMode, modelInfo) |
| 581 | + expect(result.has("read_file")).toBe(true) |
| 582 | + expect(result.has("write_to_file")).toBe(true) |
| 583 | + expect(result.size).toBe(2) |
| 584 | + }) |
| 585 | + }) |
| 586 | + |
| 587 | + describe("filterNativeToolsForMode with model customization", () => { |
| 588 | + const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [ |
| 589 | + { |
| 590 | + type: "function", |
| 591 | + function: { |
| 592 | + name: "read_file", |
| 593 | + description: "Read files", |
| 594 | + parameters: {}, |
| 595 | + }, |
| 596 | + }, |
| 597 | + { |
| 598 | + type: "function", |
| 599 | + function: { |
| 600 | + name: "write_to_file", |
| 601 | + description: "Write files", |
| 602 | + parameters: {}, |
| 603 | + }, |
| 604 | + }, |
| 605 | + { |
| 606 | + type: "function", |
| 607 | + function: { |
| 608 | + name: "apply_diff", |
| 609 | + description: "Apply diff", |
| 610 | + parameters: {}, |
| 611 | + }, |
| 612 | + }, |
| 613 | + { |
| 614 | + type: "function", |
| 615 | + function: { |
| 616 | + name: "insert_content", |
| 617 | + description: "Insert content", |
| 618 | + parameters: {}, |
| 619 | + }, |
| 620 | + }, |
| 621 | + { |
| 622 | + type: "function", |
| 623 | + function: { |
| 624 | + name: "execute_command", |
| 625 | + description: "Execute command", |
| 626 | + parameters: {}, |
| 627 | + }, |
| 628 | + }, |
| 629 | + ] |
| 630 | + |
| 631 | + it("should exclude tools when model specifies excludedTools", () => { |
| 632 | + const codeMode: ModeConfig = { |
| 633 | + slug: "code", |
| 634 | + name: "Code", |
| 635 | + roleDefinition: "Test", |
| 636 | + groups: ["read", "edit", "browser", "command", "mcp"] as const, |
| 637 | + } |
| 638 | + |
| 639 | + const modelInfo: ModelInfo = { |
| 640 | + contextWindow: 100000, |
| 641 | + supportsPromptCache: false, |
| 642 | + excludedTools: ["apply_diff"], |
| 643 | + } |
| 644 | + |
| 645 | + const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, { |
| 646 | + modelInfo, |
| 647 | + }) |
| 648 | + |
| 649 | + const toolNames = filtered.map((t) => ("function" in t ? t.function.name : "")) |
| 650 | + |
| 651 | + expect(toolNames).toContain("read_file") |
| 652 | + expect(toolNames).toContain("write_to_file") |
| 653 | + expect(toolNames).toContain("insert_content") |
| 654 | + expect(toolNames).not.toContain("apply_diff") // Excluded by model |
| 655 | + }) |
| 656 | + |
| 657 | + it("should include tools when model specifies includedTools from allowed groups", () => { |
| 658 | + const modeWithOnlyRead: ModeConfig = { |
| 659 | + slug: "limited", |
| 660 | + name: "Limited", |
| 661 | + roleDefinition: "Test", |
| 662 | + groups: ["read", "edit"] as const, |
| 663 | + } |
| 664 | + |
| 665 | + const modelInfo: ModelInfo = { |
| 666 | + contextWindow: 100000, |
| 667 | + supportsPromptCache: false, |
| 668 | + includedTools: ["insert_content"], // Edit group tool |
| 669 | + } |
| 670 | + |
| 671 | + const filtered = filterNativeToolsForMode(mockNativeTools, "limited", [modeWithOnlyRead], {}, undefined, { |
| 672 | + modelInfo, |
| 673 | + }) |
| 674 | + |
| 675 | + const toolNames = filtered.map((t) => ("function" in t ? t.function.name : "")) |
| 676 | + |
| 677 | + expect(toolNames).toContain("insert_content") // Included by model |
| 678 | + }) |
| 679 | + |
| 680 | + it("should NOT include tools from groups not allowed by mode", () => { |
| 681 | + const architectMode: ModeConfig = { |
| 682 | + slug: "architect", |
| 683 | + name: "Architect", |
| 684 | + roleDefinition: "Test", |
| 685 | + groups: ["read", "browser"] as const, // No edit group |
| 686 | + } |
| 687 | + |
| 688 | + const modelInfo: ModelInfo = { |
| 689 | + contextWindow: 100000, |
| 690 | + supportsPromptCache: false, |
| 691 | + includedTools: ["write_to_file", "apply_diff"], // Edit group tools |
| 692 | + } |
| 693 | + |
| 694 | + const filtered = filterNativeToolsForMode(mockNativeTools, "architect", [architectMode], {}, undefined, { |
| 695 | + modelInfo, |
| 696 | + }) |
| 697 | + |
| 698 | + const toolNames = filtered.map((t) => ("function" in t ? t.function.name : "")) |
| 699 | + |
| 700 | + expect(toolNames).toContain("read_file") |
| 701 | + expect(toolNames).not.toContain("write_to_file") // Not in mode's allowed groups |
| 702 | + expect(toolNames).not.toContain("apply_diff") // Not in mode's allowed groups |
| 703 | + }) |
| 704 | + |
| 705 | + it("should combine excludedTools and includedTools", () => { |
| 706 | + const codeMode: ModeConfig = { |
| 707 | + slug: "code", |
| 708 | + name: "Code", |
| 709 | + roleDefinition: "Test", |
| 710 | + groups: ["read", "edit", "browser", "command", "mcp"] as const, |
| 711 | + } |
| 712 | + |
| 713 | + const modelInfo: ModelInfo = { |
| 714 | + contextWindow: 100000, |
| 715 | + supportsPromptCache: false, |
| 716 | + excludedTools: ["apply_diff"], |
| 717 | + includedTools: ["insert_content"], |
| 718 | + } |
| 719 | + |
| 720 | + const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, { |
| 721 | + modelInfo, |
| 722 | + }) |
| 723 | + |
| 724 | + const toolNames = filtered.map((t) => ("function" in t ? t.function.name : "")) |
| 725 | + |
| 726 | + expect(toolNames).toContain("write_to_file") |
| 727 | + expect(toolNames).toContain("insert_content") // Included |
| 728 | + expect(toolNames).not.toContain("apply_diff") // Excluded |
| 729 | + }) |
| 730 | + }) |
470 | 731 | }) |
0 commit comments