From 808c12c74f4d0dcd76d6cd4833937e78cc13dedc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 23:12:38 +0000 Subject: [PATCH] test: CLI error formatting + YAML frontmatter sanitization (31 new tests) Cover two zero-test-coverage areas that produce user-facing output: FormatError/FormatUnknownError (error messages) and ConfigMarkdown.fallbackSanitization (messy YAML from other AI tools). Co-Authored-By: Claude Opus 4.6 (1M context) https://claude.ai/code/session_01KYDdfMxVRzntwFi66wMnEr --- .../opencode/test/cli/error-format.test.ts | 157 ++++++++++++++++++ .../opencode/test/config/markdown.test.ts | 75 +++++++++ 2 files changed, 232 insertions(+) create mode 100644 packages/opencode/test/cli/error-format.test.ts diff --git a/packages/opencode/test/cli/error-format.test.ts b/packages/opencode/test/cli/error-format.test.ts new file mode 100644 index 0000000000..d7c1b250e1 --- /dev/null +++ b/packages/opencode/test/cli/error-format.test.ts @@ -0,0 +1,157 @@ +import { describe, test, expect } from "bun:test" +import { FormatError, FormatUnknownError } from "../../src/cli/error" + +describe("FormatError: known error types", () => { + test("MCP.Failed returns helpful message", () => { + const err = { name: "MCPFailed", data: { name: "my-server" } } + const result = FormatError(err) + expect(result).toContain("my-server") + expect(result).toContain("failed") + }) + + test("Provider.ModelNotFoundError with suggestions", () => { + const err = { + name: "ProviderModelNotFoundError", + data: { providerID: "openai", modelID: "gpt-5", suggestions: ["gpt-4", "gpt-4o"] }, + } + const result = FormatError(err) + expect(result).toContain("gpt-5") + expect(result).toContain("Did you mean") + expect(result).toContain("gpt-4") + }) + + test("Provider.ModelNotFoundError without suggestions", () => { + const err = { + name: "ProviderModelNotFoundError", + data: { providerID: "openai", modelID: "gpt-5", suggestions: [] }, + } + const result = FormatError(err) + expect(result).toContain("gpt-5") + expect(result).not.toContain("Did you mean") + }) + + test("Provider.InitError returns provider name", () => { + const err = { name: "ProviderInitError", data: { providerID: "anthropic" } } + const result = FormatError(err) + expect(result).toContain("anthropic") + }) + + test("Config.JsonError with message", () => { + const err = { name: "ConfigJsonError", data: { path: "/home/user/.config/altimate.json", message: "Unexpected token" } } + const result = FormatError(err) + expect(result).toContain("altimate.json") + expect(result).toContain("Unexpected token") + }) + + test("Config.JsonError without message", () => { + const err = { name: "ConfigJsonError", data: { path: "/path/to/config.json" } } + const result = FormatError(err) + expect(result).toContain("config.json") + expect(result).toContain("not valid JSON") + }) + + test("Config.ConfigDirectoryTypoError", () => { + const err = { + name: "ConfigDirectoryTypoError", + data: { dir: ".openCode", path: "/project/.openCode", suggestion: ".opencode" }, + } + const result = FormatError(err) + expect(result).toContain(".openCode") + expect(result).toContain(".opencode") + expect(result).toContain("typo") + }) + + test("ConfigMarkdown.FrontmatterError", () => { + const err = { + name: "ConfigFrontmatterError", + data: { path: "CLAUDE.md", message: "CLAUDE.md: Failed to parse YAML frontmatter: invalid key" }, + } + const result = FormatError(err) + expect(result).toContain("Failed to parse") + }) + + test("Config.InvalidError with issues", () => { + const err = { + name: "ConfigInvalidError", + data: { + path: "provider.model", + message: "Invalid model", + issues: [{ message: "must be string", path: ["provider", "model"] }], + }, + } + const result = FormatError(err) + expect(result).toContain("provider.model") + expect(result).toContain("must be string") + }) + + test("Config.InvalidError without path shows generic header", () => { + const err = { + name: "ConfigInvalidError", + data: { path: "config", issues: [] }, + } + const result = FormatError(err) + expect(result).toContain("Configuration is invalid") + // "config" path should not appear as a location qualifier + expect(result).not.toContain("at config") + }) + + test("UI.CancelledError returns empty string", () => { + const err = { name: "UICancelledError" } + const result = FormatError(err) + expect(result).toBe("") + }) + + test("unknown error returns undefined", () => { + const err = new Error("random error") + expect(FormatError(err)).toBeUndefined() + }) + + test("null input returns undefined", () => { + expect(FormatError(null)).toBeUndefined() + }) +}) + +describe("FormatUnknownError", () => { + test("Error with stack returns stack", () => { + const err = new Error("boom") + const result = FormatUnknownError(err) + expect(result).toContain("boom") + expect(result).toContain("Error") + }) + + test("Error without stack returns name + message", () => { + const err = new Error("boom") + err.stack = undefined + const result = FormatUnknownError(err) + expect(result).toBe("Error: boom") + }) + + test("plain object is JSON stringified", () => { + const result = FormatUnknownError({ code: 42, msg: "fail" }) + expect(result).toContain('"code": 42') + expect(result).toContain('"msg": "fail"') + }) + + test("circular object returns fallback message", () => { + const obj: any = { a: 1 } + obj.self = obj + const result = FormatUnknownError(obj) + expect(result).toBe("Unexpected error (unserializable)") + }) + + test("string input returns itself", () => { + expect(FormatUnknownError("something went wrong")).toBe("something went wrong") + }) + + test("number input returns string representation", () => { + expect(FormatUnknownError(404)).toBe("404") + }) + + test("undefined returns string 'undefined'", () => { + expect(FormatUnknownError(undefined)).toBe("undefined") + }) + + test("null returns string 'null'", () => { + expect(FormatUnknownError(null)).toBe("null") + }) +}) diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts index 865af21077..2bcb52160f 100644 --- a/packages/opencode/test/config/markdown.test.ts +++ b/packages/opencode/test/config/markdown.test.ts @@ -226,3 +226,78 @@ describe("ConfigMarkdown: frontmatter has weird model id", async () => { expect(result.content.trim()).toBe("Strictly follow da rules") }) }) + +describe("ConfigMarkdown.fallbackSanitization", () => { + test("converts unquoted value with colon to block scalar that gray-matter can parse", async () => { + const input = `---\nurl: https://example.com:8080\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain("url: |-") + expect(result).toContain(" https://example.com:8080") + // Verify the sanitized output is actually parseable + const matter = await import("gray-matter") + const parsed = matter.default(result) + expect(parsed.data.url).toBe("https://example.com:8080") + }) + + test("preserves already double-quoted values", () => { + const input = `---\nurl: "https://example.com:8080"\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain('url: "https://example.com:8080"') + }) + + test("preserves already single-quoted values", () => { + const input = `---\nurl: 'https://example.com:8080'\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain("url: 'https://example.com:8080'") + }) + + test("preserves empty values", () => { + const input = `---\nempty:\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain("empty:") + }) + + test("preserves YAML comments", () => { + const input = `---\n# This is a comment\nkey: value\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain("# This is a comment") + }) + + test("preserves indented continuation lines", () => { + const input = `---\nlist:\n - item1\n - item2\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain(" - item1") + expect(result).toContain(" - item2") + }) + + test("returns input unchanged when no frontmatter present", () => { + const input = "Just plain content" + expect(ConfigMarkdown.fallbackSanitization(input)).toBe(input) + }) + + test("handles multiple values with colons", async () => { + const input = `---\nurl: http://a:1\nmodel: org/repo:tag\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain("url: |-") + expect(result).toContain("model: |-") + // Verify both values parse correctly + const matter = await import("gray-matter") + const parsed = matter.default(result) + expect(parsed.data.url).toBe("http://a:1") + expect(parsed.data.model).toBe("org/repo:tag") + }) + + test("values without colons are left unchanged", () => { + const input = `---\nname: John\nage: 30\n---\nContent` + const result = ConfigMarkdown.fallbackSanitization(input) + expect(result).toContain("name: John") + expect(result).toContain("age: 30") + }) + + test("content after frontmatter is not modified", () => { + const input = `---\nkey: val:ue\n---\nurl: https://example.com:8080/path` + const result = ConfigMarkdown.fallbackSanitization(input) + // Body content should be preserved verbatim + expect(result).toContain("url: https://example.com:8080/path") + }) +})