From 02a82a31e7735c992056b76042cdce5eee1007ec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 07:13:29 +0000 Subject: [PATCH] =?UTF-8?q?test:=20command=20hints=20+=20sql=5Fanalyze=20t?= =?UTF-8?q?ool=20=E2=80=94=20placeholder=20parsing=20and=20success=20seman?= =?UTF-8?q?tics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 14 tests covering two untested modules: - Command.hints() template placeholder extraction - SqlAnalyzeTool.execute success flag and formatAnalysis output Co-Authored-By: Claude Opus 4.6 (1M context) https://claude.ai/code/session_01VV8nsL7MbNaJtwMDKQBzdz --- .../altimate/tools/sql-analyze-tool.test.ts | 193 ++++++++++++++++++ packages/opencode/test/command/hints.test.ts | 39 ++++ 2 files changed, 232 insertions(+) create mode 100644 packages/opencode/test/altimate/tools/sql-analyze-tool.test.ts create mode 100644 packages/opencode/test/command/hints.test.ts diff --git a/packages/opencode/test/altimate/tools/sql-analyze-tool.test.ts b/packages/opencode/test/altimate/tools/sql-analyze-tool.test.ts new file mode 100644 index 000000000..02994ccee --- /dev/null +++ b/packages/opencode/test/altimate/tools/sql-analyze-tool.test.ts @@ -0,0 +1,193 @@ +/** + * Tests for SqlAnalyzeTool.execute — success flag semantics and output formatting. + * + * The bug fix AI-5975 changed sql_analyze to report success:true when analysis + * completes (even when issues are found). Regression would cause ~4000 false + * "unknown error" telemetry events per day. + */ +import { describe, test, expect, spyOn, afterAll, beforeEach } from "bun:test" +import * as Dispatcher from "../../../src/altimate/native/dispatcher" +import { SqlAnalyzeTool } from "../../../src/altimate/tools/sql-analyze" +import { SessionID, MessageID } from "../../../src/session/schema" + +beforeEach(() => { + process.env.ALTIMATE_TELEMETRY_DISABLED = "true" +}) + +const ctx = { + sessionID: SessionID.make("ses_test"), + messageID: MessageID.make("msg_test"), + callID: "call_test", + agent: "build", + abort: AbortSignal.any([]), + messages: [], + metadata: () => {}, + ask: async () => {}, +} + +let dispatcherSpy: ReturnType + +function mockDispatcher(response: any) { + dispatcherSpy?.mockRestore() + dispatcherSpy = spyOn(Dispatcher, "call").mockImplementation(async () => response) +} + +afterAll(() => { + dispatcherSpy?.mockRestore() + delete process.env.ALTIMATE_TELEMETRY_DISABLED +}) + +describe("SqlAnalyzeTool.execute: success semantics", () => { + test("issues found → success:true, no error in metadata", async () => { + mockDispatcher({ + success: true, + issues: [ + { + type: "lint", + severity: "warning", + message: "SELECT * detected", + recommendation: "List columns explicitly", + confidence: "high", + }, + ], + issue_count: 1, + confidence: "high", + confidence_factors: ["lint"], + }) + + const tool = await SqlAnalyzeTool.init() + const result = await tool.execute( + { sql: "SELECT * FROM t", dialect: "snowflake" }, + ctx as any, + ) + + expect(result.metadata.success).toBe(true) + expect(result.metadata.error).toBeUndefined() + expect(result.title).toContain("1 issue") + expect(result.title).not.toContain("PARSE ERROR") + }) + + test("zero issues → success:true, 'No anti-patterns' output", async () => { + mockDispatcher({ + success: true, + issues: [], + issue_count: 0, + confidence: "high", + confidence_factors: [], + }) + + const tool = await SqlAnalyzeTool.init() + const result = await tool.execute( + { sql: "SELECT id FROM t", dialect: "snowflake" }, + ctx as any, + ) + + expect(result.metadata.success).toBe(true) + expect(result.output).toContain("No anti-patterns") + expect(result.title).toContain("0 issues") + }) + + test("parse error → success:false, error in metadata and title", async () => { + mockDispatcher({ + success: false, + issues: [], + issue_count: 0, + confidence: "low", + confidence_factors: [], + error: "syntax error near SELECT", + }) + + const tool = await SqlAnalyzeTool.init() + const result = await tool.execute( + { sql: "SELEC FROM", dialect: "snowflake" }, + ctx as any, + ) + + expect(result.metadata.success).toBe(false) + expect(result.metadata.error).toBe("syntax error near SELECT") + expect(result.title).toContain("PARSE ERROR") + }) + + test("dispatcher throws → catch block returns ERROR title", async () => { + dispatcherSpy?.mockRestore() + dispatcherSpy = spyOn(Dispatcher, "call").mockRejectedValue(new Error("native crash")) + + const tool = await SqlAnalyzeTool.init() + const result = await tool.execute( + { sql: "SELECT 1", dialect: "snowflake" }, + ctx as any, + ) + + expect(result.title).toBe("Analyze: ERROR") + expect(result.metadata.success).toBe(false) + expect(result.metadata.error).toBe("native crash") + expect(result.output).toContain("Failed to analyze SQL: native crash") + }) +}) + +describe("SqlAnalyzeTool.execute: formatAnalysis output", () => { + test("singular issue → '1 issue' not '1 issues'", async () => { + mockDispatcher({ + success: true, + issues: [ + { + type: "lint", + severity: "warning", + message: "test issue", + recommendation: "fix it", + confidence: "high", + }, + ], + issue_count: 1, + confidence: "high", + confidence_factors: ["lint"], + }) + + const tool = await SqlAnalyzeTool.init() + const result = await tool.execute( + { sql: "x", dialect: "snowflake" }, + ctx as any, + ) + + expect(result.output).toContain("Found 1 issue ") + expect(result.output).not.toContain("1 issues") + }) + + test("multiple issues with location, confidence, and factors", async () => { + mockDispatcher({ + success: true, + issues: [ + { + type: "lint", + severity: "warning", + message: "SELECT * used", + recommendation: "List columns", + confidence: "high", + }, + { + type: "safety", + severity: "high", + message: "DROP TABLE detected", + recommendation: "Use caution", + location: "chars 0-5", + confidence: "medium", + }, + ], + issue_count: 2, + confidence: "high", + confidence_factors: ["lint", "safety"], + }) + + const tool = await SqlAnalyzeTool.init() + const result = await tool.execute( + { sql: "x", dialect: "snowflake" }, + ctx as any, + ) + + expect(result.output).toContain("2 issues") + expect(result.output).toContain("[WARNING] lint") + expect(result.output).toContain("[HIGH] safety [medium confidence]") + expect(result.output).toContain("chars 0-5") + expect(result.output).toContain("Note: lint; safety") + }) +}) diff --git a/packages/opencode/test/command/hints.test.ts b/packages/opencode/test/command/hints.test.ts new file mode 100644 index 000000000..85073cd6e --- /dev/null +++ b/packages/opencode/test/command/hints.test.ts @@ -0,0 +1,39 @@ +import { describe, test, expect } from "bun:test" +import { Command } from "../../src/command/index" + +describe("Command.hints: template placeholder parsing", () => { + test("extracts numbered placeholders in order", () => { + expect(Command.hints("run $1 with $2")).toEqual(["$1", "$2"]) + }) + + test("deduplicates repeated placeholders", () => { + expect(Command.hints("compare $1 to $1")).toEqual(["$1"]) + }) + + test("sorts numbered placeholders lexicographically", () => { + // String sort: "$1" < "$2" < "$3" + expect(Command.hints("$3 then $1 then $2")).toEqual(["$1", "$2", "$3"]) + }) + + test("$ARGUMENTS appears after numbered placeholders", () => { + expect(Command.hints("do $1 $ARGUMENTS")).toEqual(["$1", "$ARGUMENTS"]) + }) + + test("returns only $ARGUMENTS when no numbered placeholders", () => { + expect(Command.hints("run $ARGUMENTS")).toEqual(["$ARGUMENTS"]) + }) + + test("returns empty array when no placeholders", () => { + expect(Command.hints("static template with no args")).toEqual([]) + }) + + test("multi-digit placeholders sort lexicographically, not numerically", () => { + // String sort puts "$10" before "$2" — this is the actual behavior. + // If a template uses $10+, the TUI hint order will be $1, $10, $2. + expect(Command.hints("$10 $2 $1")).toEqual(["$1", "$10", "$2"]) + }) + + test("empty template returns empty array", () => { + expect(Command.hints("")).toEqual([]) + }) +})