Merge pull request #3634 from auyua9/fix/ast-grep-pattern-hints
fix(ast-grep): restore pattern hints in tools
This commit is contained in:
+23
-23
@@ -60,14 +60,14 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null)
|
const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null)
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
||||||
const overrides = {
|
const overrides = {
|
||||||
sisyphus: { model: "github-copilot/gpt-5.4" },
|
sisyphus: { model: "github-copilot/gpt-5.5" },
|
||||||
}
|
}
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4")
|
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.5")
|
||||||
expect(agents.sisyphus.reasoningEffort).toBe("medium")
|
expect(agents.sisyphus.reasoningEffort).toBe("medium")
|
||||||
expect(agents.sisyphus.thinking).toBeUndefined()
|
expect(agents.sisyphus.thinking).toBeUndefined()
|
||||||
providerModelsSpy.mockRestore()
|
providerModelsSpy.mockRestore()
|
||||||
@@ -77,9 +77,9 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("Atlas uses uiSelectedModel", async () => {
|
test("Atlas uses uiSelectedModel", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
|
new Set(["openai/gpt-5.5", "anthropic/claude-sonnet-4-6"])
|
||||||
)
|
)
|
||||||
const uiSelectedModel = "openai/gpt-5.4"
|
const uiSelectedModel = "openai/gpt-5.5"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// #when
|
// #when
|
||||||
@@ -98,7 +98,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.atlas).toBeDefined()
|
expect(agents.atlas).toBeDefined()
|
||||||
expect(agents.atlas.model).toBe("openai/gpt-5.4")
|
expect(agents.atlas.model).toBe("openai/gpt-5.5")
|
||||||
} finally {
|
} finally {
|
||||||
fetchSpy.mockRestore()
|
fetchSpy.mockRestore()
|
||||||
}
|
}
|
||||||
@@ -107,9 +107,9 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("user config model takes priority over uiSelectedModel for sisyphus", async () => {
|
test("user config model takes priority over uiSelectedModel for sisyphus", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
|
new Set(["openai/gpt-5.5", "anthropic/claude-sonnet-4-6"])
|
||||||
)
|
)
|
||||||
const uiSelectedModel = "openai/gpt-5.4"
|
const uiSelectedModel = "openai/gpt-5.5"
|
||||||
const overrides = {
|
const overrides = {
|
||||||
sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||||
}
|
}
|
||||||
@@ -140,9 +140,9 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("user config model takes priority over uiSelectedModel for atlas", async () => {
|
test("user config model takes priority over uiSelectedModel for atlas", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
|
new Set(["openai/gpt-5.5", "anthropic/claude-sonnet-4-6"])
|
||||||
)
|
)
|
||||||
const uiSelectedModel = "openai/gpt-5.4"
|
const uiSelectedModel = "openai/gpt-5.5"
|
||||||
const overrides = {
|
const overrides = {
|
||||||
atlas: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
atlas: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||||
}
|
}
|
||||||
@@ -265,14 +265,14 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null)
|
const providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null)
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
||||||
const overrides = {
|
const overrides = {
|
||||||
sisyphus: { model: "github-copilot/gpt-5.4", temperature: 0.5 },
|
sisyphus: { model: "github-copilot/gpt-5.5", temperature: 0.5 },
|
||||||
}
|
}
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4")
|
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.5")
|
||||||
expect(agents.sisyphus.temperature).toBe(0.5)
|
expect(agents.sisyphus.temperature).toBe(0.5)
|
||||||
providerModelsSpy.mockRestore()
|
providerModelsSpy.mockRestore()
|
||||||
fetchSpy.mockRestore()
|
fetchSpy.mockRestore()
|
||||||
@@ -306,7 +306,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
"opencode/kimi-k2.5-free",
|
"opencode/kimi-k2.5-free",
|
||||||
"zai-coding-plan/glm-5",
|
"zai-coding-plan/glm-5",
|
||||||
"opencode/big-pickle",
|
"opencode/big-pickle",
|
||||||
"openai/gpt-5.4",
|
"openai/gpt-5.5",
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("excludes hidden custom agents from orchestrator prompts", async () => {
|
test("excludes hidden custom agents from orchestrator prompts", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"])
|
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.5"])
|
||||||
)
|
)
|
||||||
|
|
||||||
const customAgentSummaries = [
|
const customAgentSummaries = [
|
||||||
@@ -379,7 +379,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("excludes disabled custom agents from orchestrator prompts", async () => {
|
test("excludes disabled custom agents from orchestrator prompts", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"])
|
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.5"])
|
||||||
)
|
)
|
||||||
|
|
||||||
const customAgentSummaries = [
|
const customAgentSummaries = [
|
||||||
@@ -415,7 +415,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("excludes custom agents when disabledAgents contains their name (case-insensitive)", async () => {
|
test("excludes custom agents when disabledAgents contains their name (case-insensitive)", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"])
|
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.5"])
|
||||||
)
|
)
|
||||||
|
|
||||||
const disabledAgents = ["ReSeArChEr"]
|
const disabledAgents = ["ReSeArChEr"]
|
||||||
@@ -451,7 +451,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("does not advertise duplicate custom agents case-insensitively", async () => {
|
test("does not advertise duplicate custom agents case-insensitively", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"])
|
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.5"])
|
||||||
)
|
)
|
||||||
|
|
||||||
const customAgentSummaries = [
|
const customAgentSummaries = [
|
||||||
@@ -483,7 +483,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
test("does not surface custom agent strings in orchestrator prompts", async () => {
|
test("does not surface custom agent strings in orchestrator prompts", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"])
|
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.5"])
|
||||||
)
|
)
|
||||||
|
|
||||||
const customAgentSummaries = [
|
const customAgentSummaries = [
|
||||||
@@ -842,7 +842,7 @@ describe("Atlas is unaffected by environment context toggle", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.4"])
|
new Set(["anthropic/claude-opus-4-7", "openai/gpt-5.5"])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -968,7 +968,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
|||||||
// #given - user configures a model from a plugin provider (like antigravity)
|
// #given - user configures a model from a plugin provider (like antigravity)
|
||||||
// that is NOT in the availableModels cache and NOT in the fallback chain
|
// that is NOT in the availableModels cache and NOT in the fallback chain
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["openai/gpt-5.4"])
|
new Set(["openai/gpt-5.5"])
|
||||||
)
|
)
|
||||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(
|
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(
|
||||||
["openai"]
|
["openai"]
|
||||||
@@ -1098,7 +1098,7 @@ describe("buildAgent with category and skills", () => {
|
|||||||
|
|
||||||
const categories = {
|
const categories = {
|
||||||
"custom-category": {
|
"custom-category": {
|
||||||
model: "openai/gpt-5.4",
|
model: "openai/gpt-5.5",
|
||||||
variant: "xhigh",
|
variant: "xhigh",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1107,7 +1107,7 @@ describe("buildAgent with category and skills", () => {
|
|||||||
const agent = buildAgent(source["test-agent"], TEST_MODEL, categories)
|
const agent = buildAgent(source["test-agent"], TEST_MODEL, categories)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agent.model).toBe("openai/gpt-5.4")
|
expect(agent.model).toBe("openai/gpt-5.5")
|
||||||
expect(agent.variant).toBe("xhigh")
|
expect(agent.variant).toBe("xhigh")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1357,7 +1357,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
|||||||
// #given - custom category has reasoningEffort=xhigh, direct override says "low"
|
// #given - custom category has reasoningEffort=xhigh, direct override says "low"
|
||||||
const categories = {
|
const categories = {
|
||||||
"test-cat": {
|
"test-cat": {
|
||||||
model: "openai/gpt-5.4",
|
model: "openai/gpt-5.5",
|
||||||
reasoningEffort: "xhigh" as const,
|
reasoningEffort: "xhigh" as const,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1377,7 +1377,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
|||||||
// #given - custom category has reasoningEffort, no direct reasoningEffort in override
|
// #given - custom category has reasoningEffort, no direct reasoningEffort in override
|
||||||
const categories = {
|
const categories = {
|
||||||
"reasoning-cat": {
|
"reasoning-cat": {
|
||||||
model: "openai/gpt-5.4",
|
model: "openai/gpt-5.5",
|
||||||
reasoningEffort: "high" as const,
|
reasoningEffort: "high" as const,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe("resolveAgentVariant", () => {
|
|||||||
sisyphus: { category: "ultrabrain" },
|
sisyphus: { category: "ultrabrain" },
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
ultrabrain: { model: "openai/gpt-5.4", variant: "xhigh" },
|
ultrabrain: { model: "openai/gpt-5.5", variant: "xhigh" },
|
||||||
},
|
},
|
||||||
} as OhMyOpenCodeConfig
|
} as OhMyOpenCodeConfig
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, it, mock } from "bun:test"
|
||||||
|
import { AST_GREP_REPLACE_DESCRIPTION, AST_GREP_SEARCH_DESCRIPTION } from "./tool-descriptions"
|
||||||
|
|
||||||
|
const runSgMock = mock(async () => ({
|
||||||
|
matches: [],
|
||||||
|
totalMatches: 0,
|
||||||
|
truncated: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("./cli", () => ({
|
||||||
|
runSg: runSgMock,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { createAstGrepTools } from "./tools"
|
||||||
|
|
||||||
|
describe("createAstGrepTools", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runSgMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("#given the production tool factory #when creating tools #then exposes shared ast-grep descriptions", () => {
|
||||||
|
// given / when
|
||||||
|
const tools = createAstGrepTools({ directory: "/repo" } as never)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(tools.ast_grep_search.description).toBe(AST_GREP_SEARCH_DESCRIPTION)
|
||||||
|
expect(tools.ast_grep_replace.description).toBe(AST_GREP_REPLACE_DESCRIPTION)
|
||||||
|
expect(tools.ast_grep_search.description).toContain("NOT regex")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("#given empty search results from a regex-shaped pattern #when executing #then appends the pattern hint", async () => {
|
||||||
|
// given
|
||||||
|
const tools = createAstGrepTools({ directory: "/repo" } as never)
|
||||||
|
|
||||||
|
// when
|
||||||
|
const output = await tools.ast_grep_search.execute(
|
||||||
|
{ pattern: "foo|bar", lang: "typescript" },
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(output).toContain("No matches found")
|
||||||
|
expect(output).toContain("alternation")
|
||||||
|
expect(output).toContain("grep")
|
||||||
|
expect(runSgMock).toHaveBeenCalledWith({
|
||||||
|
pattern: "foo|bar",
|
||||||
|
lang: "typescript",
|
||||||
|
paths: ["/repo"],
|
||||||
|
globs: undefined,
|
||||||
|
context: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
+10
-35
@@ -3,6 +3,12 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
|||||||
import { CLI_LANGUAGES } from "./constants"
|
import { CLI_LANGUAGES } from "./constants"
|
||||||
import { runSg } from "./cli"
|
import { runSg } from "./cli"
|
||||||
import { formatSearchResult, formatReplaceResult } from "./result-formatter"
|
import { formatSearchResult, formatReplaceResult } from "./result-formatter"
|
||||||
|
import { getPatternHint } from "./pattern-hints"
|
||||||
|
import {
|
||||||
|
AST_GREP_REPLACE_DESCRIPTION,
|
||||||
|
AST_GREP_SEARCH_DESCRIPTION,
|
||||||
|
AST_GREP_SEARCH_PATTERN_PARAM,
|
||||||
|
} from "./tool-descriptions"
|
||||||
import type { CliLanguage } from "./types"
|
import type { CliLanguage } from "./types"
|
||||||
|
|
||||||
async function showOutputToUser(context: unknown, output: string): Promise<void> {
|
async function showOutputToUser(context: unknown, output: string): Promise<void> {
|
||||||
@@ -12,39 +18,11 @@ async function showOutputToUser(context: unknown, output: string): Promise<void>
|
|||||||
await ctx.metadata?.({ metadata: { output } })
|
await ctx.metadata?.({ metadata: { output } })
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null {
|
|
||||||
const src = pattern.trim()
|
|
||||||
|
|
||||||
if (lang === "python") {
|
|
||||||
if (src.startsWith("class ") && src.endsWith(":")) {
|
|
||||||
const withoutColon = src.slice(0, -1)
|
|
||||||
return `Hint: Remove trailing colon. Try: "${withoutColon}"`
|
|
||||||
}
|
|
||||||
if ((src.startsWith("def ") || src.startsWith("async def ")) && src.endsWith(":")) {
|
|
||||||
const withoutColon = src.slice(0, -1)
|
|
||||||
return `Hint: Remove trailing colon. Try: "${withoutColon}"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["javascript", "typescript", "tsx"].includes(lang)) {
|
|
||||||
if (/^(export\s+)?(async\s+)?function\s+\$[A-Z_]+\s*$/i.test(src)) {
|
|
||||||
return `Hint: Function patterns need params and body. Try "function $NAME($$$) { $$$ }"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAstGrepTools(ctx: PluginInput): Record<string, ToolDefinition> {
|
export function createAstGrepTools(ctx: PluginInput): Record<string, ToolDefinition> {
|
||||||
const ast_grep_search: ToolDefinition = tool({
|
const ast_grep_search: ToolDefinition = tool({
|
||||||
description:
|
description: AST_GREP_SEARCH_DESCRIPTION,
|
||||||
"Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " +
|
|
||||||
"Use meta-variables: $VAR (single node), $$$ (multiple nodes). " +
|
|
||||||
"IMPORTANT: Patterns must be complete AST nodes (valid code). " +
|
|
||||||
"For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " +
|
|
||||||
"Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
|
|
||||||
args: {
|
args: {
|
||||||
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
|
pattern: tool.schema.string().describe(AST_GREP_SEARCH_PATTERN_PARAM),
|
||||||
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
||||||
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
|
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
|
||||||
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
|
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
|
||||||
@@ -63,7 +41,7 @@ export function createAstGrepTools(ctx: PluginInput): Record<string, ToolDefinit
|
|||||||
let output = formatSearchResult(result)
|
let output = formatSearchResult(result)
|
||||||
|
|
||||||
if (result.matches.length === 0 && !result.error) {
|
if (result.matches.length === 0 && !result.error) {
|
||||||
const hint = getEmptyResultHint(args.pattern, args.lang as CliLanguage)
|
const hint = getPatternHint(args.pattern, args.lang as CliLanguage)
|
||||||
if (hint) {
|
if (hint) {
|
||||||
output += `\n\n${hint}`
|
output += `\n\n${hint}`
|
||||||
}
|
}
|
||||||
@@ -80,10 +58,7 @@ export function createAstGrepTools(ctx: PluginInput): Record<string, ToolDefinit
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ast_grep_replace: ToolDefinition = tool({
|
const ast_grep_replace: ToolDefinition = tool({
|
||||||
description:
|
description: AST_GREP_REPLACE_DESCRIPTION,
|
||||||
"Replace code patterns across filesystem with AST-aware rewriting. " +
|
|
||||||
"Dry-run by default. Use meta-variables in rewrite to preserve matched content. " +
|
|
||||||
"Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
|
|
||||||
args: {
|
args: {
|
||||||
pattern: tool.schema.string().describe("AST pattern to match"),
|
pattern: tool.schema.string().describe("AST pattern to match"),
|
||||||
rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
||||||
|
|||||||
@@ -5,29 +5,29 @@ describe("buildMultimodalLookerFallbackChain", () => {
|
|||||||
// given
|
// given
|
||||||
const { buildMultimodalLookerFallbackChain } = await import("./multimodal-fallback-chain")
|
const { buildMultimodalLookerFallbackChain } = await import("./multimodal-fallback-chain")
|
||||||
const visionCapableModels = [
|
const visionCapableModels = [
|
||||||
{ providerID: "openai", modelID: "gpt-5.4" },
|
{ providerID: "openai", modelID: "gpt-5.5" },
|
||||||
{ providerID: "opencode", modelID: "gpt-5.4" },
|
{ providerID: "opencode", modelID: "gpt-5.5" },
|
||||||
]
|
]
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = buildMultimodalLookerFallbackChain(visionCapableModels)
|
const result = buildMultimodalLookerFallbackChain(visionCapableModels)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const gpt54Entries = result.filter((entry) => entry.model === "gpt-5.4")
|
const gpt55Entries = result.filter((entry) => entry.model === "gpt-5.5")
|
||||||
expect(gpt54Entries.length).toBeGreaterThan(0)
|
expect(gpt55Entries.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("avoids duplicates when adding hardcoded entries", async () => {
|
it("avoids duplicates when adding hardcoded entries", async () => {
|
||||||
// given
|
// given
|
||||||
const { buildMultimodalLookerFallbackChain } = await import("./multimodal-fallback-chain")
|
const { buildMultimodalLookerFallbackChain } = await import("./multimodal-fallback-chain")
|
||||||
const visionCapableModels = [{ providerID: "openai", modelID: "gpt-5.4" }]
|
const visionCapableModels = [{ providerID: "openai", modelID: "gpt-5.5" }]
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = buildMultimodalLookerFallbackChain(visionCapableModels)
|
const result = buildMultimodalLookerFallbackChain(visionCapableModels)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result.length).toBeGreaterThan(0)
|
expect(result.length).toBeGreaterThan(0)
|
||||||
expect(result[0].model).toBe("gpt-5.4")
|
expect(result[0].model).toBe("gpt-5.5")
|
||||||
expect(result[0].providers).toContain("openai")
|
expect(result[0].providers).toContain("openai")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user