@arii/boomtick-mcp 0.3.7 → 0.4.1

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.
@@ -22,11 +22,47 @@ describe("github.create_issue", () => {
22
22
  durationMs: 10,
23
23
  command: "td-cli gh create-issue --title 'Test Issue' --body 'Test Body'"
24
24
  });
25
- const result = await createIssueHandler({ title: "Test Issue", body: "Test Body", file: null });
25
+ const result = await createIssueHandler({ title: "Test Issue", body: "Test Body", file: null, repo: null });
26
26
  expect(result.status).toBe("success");
27
27
  expect(result.issue?.number).toBe(123);
28
28
  expect(result.issue?.title).toBe("Test Issue");
29
29
  });
30
+ it("should append --repo flag when repo parameter is provided", async () => {
31
+ const mockResponse = {
32
+ status: "success",
33
+ issue: {
34
+ number: 456,
35
+ title: "Cross Repo Issue",
36
+ html_url: "https://github.com/org/other-repo/issues/456",
37
+ state: "open"
38
+ }
39
+ };
40
+ vi.mocked(shell.runCommand).mockResolvedValue({
41
+ stdout: JSON.stringify(mockResponse),
42
+ stderr: "",
43
+ exitCode: 0,
44
+ durationMs: 10,
45
+ command: "td-cli gh create-issue --title 'Cross Repo Issue' --body 'Body' --repo 'org/other-repo'"
46
+ });
47
+ const result = await createIssueHandler({
48
+ title: "Cross Repo Issue",
49
+ body: "Body",
50
+ file: null,
51
+ repo: "org/other-repo"
52
+ });
53
+ expect(shell.runCommand).toHaveBeenCalledWith("td-cli", [
54
+ "gh",
55
+ "create-issue",
56
+ "--title",
57
+ "Cross Repo Issue",
58
+ "--body",
59
+ "Body",
60
+ "--repo",
61
+ "org/other-repo"
62
+ ]);
63
+ expect(result.status).toBe("success");
64
+ expect(result.issue?.number).toBe(456);
65
+ });
30
66
  it("should throw error on command failure", async () => {
31
67
  vi.mocked(shell.runCommand).mockResolvedValue({
32
68
  stdout: "",
@@ -35,7 +71,7 @@ describe("github.create_issue", () => {
35
71
  durationMs: 10,
36
72
  command: "td-cli gh create-issue"
37
73
  });
38
- await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null })).rejects.toThrow("Failed to create issue: Auth failed");
74
+ await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null, repo: null })).rejects.toThrow("Failed to create issue: Auth failed");
39
75
  });
40
76
  it("should handle error status from CLI output", async () => {
41
77
  const mockResponse = {
@@ -49,6 +85,6 @@ describe("github.create_issue", () => {
49
85
  durationMs: 10,
50
86
  command: "td-cli gh create-issue"
51
87
  });
52
- await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null })).rejects.toThrow("Failed to create issue: Repo not found");
88
+ await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null, repo: null })).rejects.toThrow("Failed to create issue: Repo not found");
53
89
  });
54
90
  });
@@ -1,24 +1,22 @@
1
- import { z } from "zod";
2
1
  import { runCommand } from "../lib/shell.js";
3
- export const CreatePullRequestInputSchema = z.object({
4
- title: z.string(),
5
- body: z.string(),
6
- head: z.string(),
7
- base: z.string().optional().default("main"),
8
- draft: z.boolean().optional().default(false),
9
- });
2
+ import { CreatePullRequestInputSchema } from "./contract.js";
3
+ export { CreatePullRequestInputSchema };
10
4
  export async function createPullRequestHandler(args) {
5
+ const params = CreatePullRequestInputSchema.parse(args);
11
6
  const tdArgs = [
12
7
  "gh",
13
8
  "create-pr",
14
- "--title", args.title,
15
- "--body", args.body,
16
- "--head", args.head,
17
- "--base", args.base
9
+ "--title", params.title,
10
+ "--body", params.body,
11
+ "--head", params.head,
12
+ "--base", params.base
18
13
  ];
19
- if (args.draft) {
14
+ if (params.draft) {
20
15
  tdArgs.push("--draft");
21
16
  }
17
+ if (params.repo) {
18
+ tdArgs.push("--repo", params.repo);
19
+ }
22
20
  const result = await runCommand("td-cli", tdArgs);
23
21
  if (result.exitCode !== 0) {
24
22
  // Attempt to parse stdout as JSON to see if there's a structured error message
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { createPullRequestHandler } from "./github.create_pull_request.js";
3
+ import * as shell from "../lib/shell.js";
4
+ vi.mock("../lib/shell.js", () => ({
5
+ runCommand: vi.fn()
6
+ }));
7
+ describe("github.create_pull_request", () => {
8
+ it("should create a pull request successfully", async () => {
9
+ const mockResponse = {
10
+ status: "success",
11
+ pr: {
12
+ number: 789,
13
+ html_url: "https://github.com/owner/repo/pull/789"
14
+ }
15
+ };
16
+ vi.mocked(shell.runCommand).mockResolvedValue({
17
+ stdout: JSON.stringify(mockResponse),
18
+ stderr: "",
19
+ exitCode: 0,
20
+ durationMs: 10,
21
+ command: "td-cli gh create-pr --title 'PR' --body 'Body' --head 'feature' --base 'main'"
22
+ });
23
+ const result = await createPullRequestHandler({
24
+ title: "PR",
25
+ body: "Body",
26
+ head: "feature",
27
+ base: "main",
28
+ draft: false
29
+ });
30
+ expect(result.success).toBe(true);
31
+ expect(result.url).toBe("https://github.com/owner/repo/pull/789");
32
+ });
33
+ it("should append --repo flag when repo parameter is provided", async () => {
34
+ const mockResponse = {
35
+ status: "success",
36
+ pr: {
37
+ number: 999,
38
+ html_url: "https://github.com/org/other-repo/pull/999"
39
+ }
40
+ };
41
+ vi.mocked(shell.runCommand).mockResolvedValue({
42
+ stdout: JSON.stringify(mockResponse),
43
+ stderr: "",
44
+ exitCode: 0,
45
+ durationMs: 10,
46
+ command: "td-cli gh create-pr --title 'PR' --body 'Body' --head 'feature' --base 'main' --repo 'org/other-repo'"
47
+ });
48
+ const result = await createPullRequestHandler({
49
+ title: "PR",
50
+ body: "Body",
51
+ head: "feature",
52
+ base: "main",
53
+ draft: false,
54
+ repo: "org/other-repo"
55
+ });
56
+ expect(shell.runCommand).toHaveBeenCalledWith("td-cli", [
57
+ "gh",
58
+ "create-pr",
59
+ "--title",
60
+ "PR",
61
+ "--body",
62
+ "Body",
63
+ "--head",
64
+ "feature",
65
+ "--base",
66
+ "main",
67
+ "--repo",
68
+ "org/other-repo"
69
+ ]);
70
+ expect(result.success).toBe(true);
71
+ expect(result.url).toBe("https://github.com/org/other-repo/pull/999");
72
+ });
73
+ it("should throw error on CLI error response", async () => {
74
+ const mockResponse = {
75
+ status: "error",
76
+ message: "Branch not found"
77
+ };
78
+ vi.mocked(shell.runCommand).mockResolvedValue({
79
+ stdout: JSON.stringify(mockResponse),
80
+ stderr: "",
81
+ exitCode: 0,
82
+ durationMs: 10,
83
+ command: "td-cli gh create-pr"
84
+ });
85
+ await expect(createPullRequestHandler({
86
+ title: "PR",
87
+ body: "Body",
88
+ head: "feature",
89
+ base: "main",
90
+ draft: false
91
+ })).rejects.toThrow("Failed to create pull request: Branch not found");
92
+ });
93
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseJulesSession } from "./shared.js";
3
+ describe("jules/shared", () => {
4
+ describe("parseJulesSession", () => {
5
+ it("should strip 'sessions/' prefix from session name to form ID", () => {
6
+ const input = {
7
+ name: "sessions/12345",
8
+ state: "IN_PROGRESS",
9
+ };
10
+ const result = parseJulesSession(input);
11
+ expect(result.id).toBe("12345");
12
+ expect(result.status).toBe("IN_PROGRESS");
13
+ });
14
+ it("should use raw name as ID if 'sessions/' prefix is absent", () => {
15
+ const input = {
16
+ name: "custom-session-id",
17
+ state: "SUCCEEDED",
18
+ };
19
+ expect(parseJulesSession(input).id).toBe("custom-session-id");
20
+ });
21
+ it("should default name/id to empty string if missing", () => {
22
+ expect(parseJulesSession({}).id).toBe("");
23
+ });
24
+ it("should map states to COMPLETED, FAILED, IN_PROGRESS, or fallback PENDING", () => {
25
+ expect(parseJulesSession({ state: "SUCCEEDED" }).status).toBe("COMPLETED");
26
+ expect(parseJulesSession({ state: "COMPLETED" }).status).toBe("COMPLETED");
27
+ expect(parseJulesSession({ state: "FAILED" }).status).toBe("FAILED");
28
+ expect(parseJulesSession({ state: "CANCELLED" }).status).toBe("FAILED");
29
+ expect(parseJulesSession({ state: "TERMINATED" }).status).toBe("FAILED");
30
+ expect(parseJulesSession({ state: "IN_PROGRESS" }).status).toBe("IN_PROGRESS");
31
+ expect(parseJulesSession({ state: "UNKNOWN_STATE" }).status).toBe("PENDING");
32
+ expect(parseJulesSession({}).status).toBe("PENDING");
33
+ });
34
+ it("should extract pullRequestUrl from outputs array when present", () => {
35
+ const input = {
36
+ name: "sessions/abc",
37
+ outputs: [
38
+ { type: "text", text: "logs" },
39
+ { pullRequest: { url: "https://github.com/arii/boomtick/pull/100" } },
40
+ ],
41
+ };
42
+ const session = parseJulesSession(input);
43
+ expect(session.pullRequestUrl).toBe("https://github.com/arii/boomtick/pull/100");
44
+ });
45
+ it("should return undefined pullRequestUrl if missing or invalid outputs", () => {
46
+ expect(parseJulesSession({ outputs: [] }).pullRequestUrl).toBeUndefined();
47
+ expect(parseJulesSession({ outputs: [{ other: "data" }] }).pullRequestUrl).toBeUndefined();
48
+ expect(parseJulesSession({ outputs: "not-an-array" }).pullRequestUrl).toBeUndefined();
49
+ });
50
+ it("should parse createTime or fallback to current date", () => {
51
+ const timeStr = "2025-01-01T12:00:00Z";
52
+ const sessionWithTime = parseJulesSession({ createTime: timeStr });
53
+ expect(sessionWithTime.createdAt).toEqual(new Date(timeStr));
54
+ const sessionWithoutTime = parseJulesSession({});
55
+ expect(sessionWithoutTime.createdAt).toBeInstanceOf(Date);
56
+ });
57
+ });
58
+ });
@@ -20,6 +20,10 @@ describe("repo.get_package_scripts", () => {
20
20
  vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"));
21
21
  await expect(getPackageScriptsHandler({})).rejects.toThrow("Failed to read package.json: File not found");
22
22
  });
23
+ it("should throw error when package.json is malformed", async () => {
24
+ vi.mocked(fs.readFile).mockResolvedValue("{ invalid json ");
25
+ await expect(getPackageScriptsHandler({})).rejects.toThrow(/Failed to read package.json: Malformed package.json:/);
26
+ });
23
27
  it("should filter scripts by pattern", async () => {
24
28
  const mockPkg = {
25
29
  scripts: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arii/boomtick-mcp",
3
- "version": "0.3.7",
3
+ "version": "0.4.1",
4
4
  "description": "Boomtick MCP Guide: PR Rescue + Merge Conflict Agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,165 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { execSync } from "child_process";
3
+
4
+ vi.mock("child_process", () => ({
5
+ execSync: vi.fn(),
6
+ }));
7
+
8
+ describe("config", () => {
9
+ const originalEnv = process.env;
10
+
11
+ beforeEach(() => {
12
+ vi.resetModules();
13
+ process.env = { ...originalEnv };
14
+ vi.mocked(execSync).mockReset();
15
+ vi.mocked(execSync).mockReturnValue("{}");
16
+ });
17
+
18
+ afterEach(() => {
19
+ process.env = originalEnv;
20
+ });
21
+
22
+ describe("initializeConfig", () => {
23
+ it("should load dynamic config from td-cli config view using active cwd", async () => {
24
+ const mockConfig = { github_repo: "owner/repo", base_branch: "origin/main", vite_base_path: "/app/" };
25
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
26
+
27
+ const { initializeConfig } = await import("./config.js");
28
+ const result = initializeConfig();
29
+
30
+ expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.objectContaining({
31
+ cwd: process.cwd()
32
+ }));
33
+ expect(result).toEqual(mockConfig);
34
+ });
35
+
36
+ it("should return cached dynamic config on subsequent calls", async () => {
37
+ const mockConfig = { github_repo: "owner/repo" };
38
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
39
+
40
+ const { initializeConfig } = await import("./config.js");
41
+ const result1 = initializeConfig();
42
+ const result2 = initializeConfig();
43
+
44
+ expect(result1).toBe(result2);
45
+ expect(execSync).toHaveBeenCalledTimes(1);
46
+ });
47
+
48
+ it("should throw critical error when execSync fails in development or CI", async () => {
49
+ process.env.NODE_ENV = "development";
50
+ vi.mocked(execSync).mockImplementation(() => {
51
+ throw new Error("Command failed");
52
+ });
53
+
54
+ const { initializeConfig } = await import("./config.js");
55
+ expect(() => initializeConfig()).toThrow(/CRITICAL: Failed to load dynamic config/);
56
+ });
57
+
58
+ it("should log warning and set empty config when execSync fails in production", async () => {
59
+ process.env.NODE_ENV = "production";
60
+ process.env.CI = "false";
61
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
62
+
63
+ vi.mocked(execSync).mockImplementation(() => {
64
+ throw new Error("Command failed");
65
+ });
66
+
67
+ const { initializeConfig } = await import("./config.js");
68
+ const result = initializeConfig();
69
+
70
+ expect(result).toEqual({});
71
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("CRITICAL: Failed to load dynamic config"));
72
+ consoleWarnSpy.mockRestore();
73
+ });
74
+ });
75
+
76
+ describe("config object getters", () => {
77
+ it("should resolve githubToken from process.env or gh CLI fallback", async () => {
78
+ process.env.GITHUB_TOKEN = "env-token-123";
79
+ const { config } = await import("./config.js");
80
+ expect(config.githubToken).toBe("env-token-123");
81
+
82
+ delete process.env.GITHUB_TOKEN;
83
+ vi.mocked(execSync).mockReturnValue("gh-cli-token\n");
84
+ expect(config.githubToken).toBe("gh-cli-token");
85
+
86
+ vi.mocked(execSync).mockImplementation(() => {
87
+ throw new Error("gh not found");
88
+ });
89
+ expect(config.githubToken).toBeUndefined();
90
+ });
91
+
92
+ it("should prioritize cachedDynamicConfig over environment variables for githubOwner and githubRepo", async () => {
93
+ process.env.GITHUB_OWNER = "env-owner";
94
+ process.env.GITHUB_REPO = "env-repo";
95
+ process.env.GITHUB_REPOSITORY = "env-owner/env-repo";
96
+
97
+ const mockConfig = { github_repo: "workspace-owner/workspace-repo" };
98
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
99
+
100
+ const { config, initializeConfig } = await import("./config.js");
101
+ initializeConfig();
102
+
103
+ expect(config.githubOwner).toBe("workspace-owner");
104
+ expect(config.githubRepo).toBe("workspace-repo");
105
+ });
106
+
107
+ it("should fallback to environment variables when cachedDynamicConfig is empty", async () => {
108
+ process.env.GITHUB_OWNER = "custom-owner";
109
+ process.env.GITHUB_REPO = "custom-repo";
110
+ const { config } = await import("./config.js");
111
+
112
+ expect(config.githubOwner).toBe("custom-owner");
113
+ expect(config.githubRepo).toBe("custom-repo");
114
+ });
115
+
116
+ it("should throw when githubOwner / githubRepo cannot be determined", async () => {
117
+ delete process.env.GITHUB_OWNER;
118
+ delete process.env.GITHUB_REPO;
119
+ delete process.env.GITHUB_REPOSITORY;
120
+ delete process.env.GH_REPO;
121
+ vi.mocked(execSync).mockImplementation(() => {
122
+ throw new Error("td-cli failed");
123
+ });
124
+
125
+ process.env.NODE_ENV = "production";
126
+ process.env.CI = "false";
127
+ vi.spyOn(console, "warn").mockImplementation(() => {});
128
+
129
+ const { config, initializeConfig } = await import("./config.js");
130
+ initializeConfig();
131
+
132
+ expect(() => config.githubOwner).toThrow("GITHUB_OWNER must be set via environment variable or project_config.json");
133
+ expect(() => config.githubRepo).toThrow("GITHUB_REPO must be set via environment variable or project_config.json");
134
+ });
135
+
136
+ it("should resolve repoPath from BOOMTICK_REPO_PATH or findRepoRoot fallback", async () => {
137
+ process.env.BOOMTICK_REPO_PATH = "/custom/repo/path";
138
+ const { config } = await import("./config.js");
139
+ expect(config.repoPath).toBe("/custom/repo/path");
140
+ });
141
+
142
+ it("should resolve defaultBaseBranch, viteBasePath, and ghPath", async () => {
143
+ process.env.DEFAULT_BASE_BRANCH = "develop";
144
+ process.env.VITE_BASE_PATH = "/base/";
145
+ process.env.GH_PATH = "/usr/bin/gh";
146
+
147
+ const { config } = await import("./config.js");
148
+ expect(config.defaultBaseBranch).toBe("develop");
149
+ expect(config.viteBasePath).toBe("/base/");
150
+ expect(config.ghPath).toBe("/usr/bin/gh");
151
+ });
152
+
153
+ it("should throw when viteBasePath is missing", async () => {
154
+ delete process.env.VITE_BASE_PATH;
155
+ process.env.NODE_ENV = "production";
156
+ process.env.CI = "false";
157
+ vi.spyOn(console, "warn").mockImplementation(() => {});
158
+
159
+ const { config, initializeConfig } = await import("./config.js");
160
+ initializeConfig();
161
+
162
+ expect(() => config.viteBasePath).toThrow("VITE_BASE_PATH must be set via environment variable or project_config.json");
163
+ });
164
+ });
165
+ });
package/src/config.ts CHANGED
@@ -1,4 +1,3 @@
1
- import dotenv from "dotenv";
2
1
  import path from "path";
3
2
  import fs from "fs";
4
3
  import { fileURLToPath } from "url";
@@ -6,10 +5,14 @@ import { execSync } from "child_process";
6
5
 
7
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
7
 
9
- dotenv.config({
10
- path: path.resolve(__dirname, "../.env"),
11
- quiet: true
12
- });
8
+ const envPath = path.resolve(__dirname, "../.env");
9
+ if (fs.existsSync(envPath) && typeof process.loadEnvFile === "function") {
10
+ try {
11
+ process.loadEnvFile(envPath);
12
+ } catch {
13
+ // Ignore if already loaded or invalid
14
+ }
15
+ }
13
16
 
14
17
  function getGithubToken(): string | undefined {
15
18
  if (process.env.GITHUB_TOKEN) {
@@ -36,9 +39,11 @@ export function initializeConfig() {
36
39
  }
37
40
 
38
41
  try {
39
- // Attempt to load core properties from the Python CLI to avoid duplication
42
+ // Attempt to load core properties from the Python CLI in the active workspace
43
+ const cwd = process.env.BOOMTICK_REPO_PATH || process.cwd();
40
44
  const cmd = `td-cli config view`;
41
45
  const output = execSync(cmd, {
46
+ cwd,
42
47
  encoding: "utf-8",
43
48
  stdio: ["ignore", "pipe", "ignore"]
44
49
  });
@@ -64,10 +69,27 @@ function findRepoRoot() {
64
69
  return process.env.BOOMTICK_REPO_PATH;
65
70
  }
66
71
 
67
- // Traverse up to find workspace.json or .git
68
- let current = __dirname;
72
+ // Traverse up from process.cwd() to find project_config.json, workspace.json, or .git
73
+ let current = process.cwd();
69
74
  while (current !== path.parse(current).root) {
70
- if (fs.existsSync(path.join(current, "workspace.json")) || fs.existsSync(path.join(current, ".git"))) {
75
+ if (
76
+ fs.existsSync(path.join(current, "project_config.json")) ||
77
+ fs.existsSync(path.join(current, "workspace.json")) ||
78
+ fs.existsSync(path.join(current, ".git"))
79
+ ) {
80
+ return current;
81
+ }
82
+ current = path.dirname(current);
83
+ }
84
+
85
+ // Fallback to __dirname search if discovery from cwd fails
86
+ current = __dirname;
87
+ while (current !== path.parse(current).root) {
88
+ if (
89
+ fs.existsSync(path.join(current, "project_config.json")) ||
90
+ fs.existsSync(path.join(current, "workspace.json")) ||
91
+ fs.existsSync(path.join(current, ".git"))
92
+ ) {
71
93
  return current;
72
94
  }
73
95
  current = path.dirname(current);
@@ -79,20 +101,28 @@ function findRepoRoot() {
79
101
  export const config = {
80
102
  get githubToken() { return getGithubToken(); },
81
103
  get githubOwner() {
82
- if (process.env.GITHUB_OWNER) return process.env.GITHUB_OWNER;
83
104
  const repoString = cachedDynamicConfig?.github_repo;
84
- if (typeof repoString !== "string" || !repoString.includes("/")) {
85
- throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
105
+ if (typeof repoString === "string" && repoString.includes("/")) {
106
+ return repoString.split("/")[0];
107
+ }
108
+ if (process.env.GITHUB_OWNER) return process.env.GITHUB_OWNER;
109
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
110
+ if (envRepo && envRepo.includes("/")) {
111
+ return envRepo.split("/")[0];
86
112
  }
87
- return repoString.split("/")[0];
113
+ throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
88
114
  },
89
115
  get githubRepo() {
90
- if (process.env.GITHUB_REPO) return process.env.GITHUB_REPO;
91
116
  const repoString = cachedDynamicConfig?.github_repo;
92
- if (typeof repoString !== "string" || !repoString.includes("/")) {
93
- throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
117
+ if (typeof repoString === "string" && repoString.includes("/")) {
118
+ return repoString.split("/")[1];
119
+ }
120
+ if (process.env.GITHUB_REPO) return process.env.GITHUB_REPO;
121
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
122
+ if (envRepo && envRepo.includes("/")) {
123
+ return envRepo.split("/")[1];
94
124
  }
95
- return repoString.split("/")[1];
125
+ throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
96
126
  },
97
127
  get repoPath() {
98
128
  return findRepoRoot();
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { sanitizeError } from "./error_utils.js";
3
+
4
+ describe("error_utils", () => {
5
+ describe("sanitizeError", () => {
6
+ it("should return 'Unknown error' when stderr is empty", () => {
7
+ expect(sanitizeError("")).toBe("Unknown error");
8
+ });
9
+
10
+ it("should return only the first line of stderr", () => {
11
+ const stderr = "Error: Something failed\n at StackTrace.line1\n at StackTrace.line2";
12
+ expect(sanitizeError(stderr)).toBe("Error: Something failed");
13
+ });
14
+
15
+ it("should truncate lines exceeding 200 characters", () => {
16
+ const longLine = "A".repeat(250);
17
+ const result = sanitizeError(longLine);
18
+ expect(result.length).toBe(200);
19
+ expect(result).toBe("A".repeat(200));
20
+ });
21
+
22
+ it("should preserve first line if under 200 characters", () => {
23
+ const line = "Fatal error: unexpected token near line 5";
24
+ expect(sanitizeError(line)).toBe(line);
25
+ });
26
+ });
27
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createSuccessResult, createErrorResult } from "./result.js";
3
+
4
+ describe("result utils", () => {
5
+ describe("createSuccessResult", () => {
6
+ it("should format string data directly without JSON stringifying", () => {
7
+ const result = createSuccessResult("simple string message");
8
+ expect(result).toEqual({
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: "simple string message",
13
+ },
14
+ ],
15
+ });
16
+ expect(result.isError).toBeUndefined();
17
+ });
18
+
19
+ it("should format object data as pretty-printed JSON string", () => {
20
+ const objData = { foo: "bar", count: 42 };
21
+ const result = createSuccessResult(objData);
22
+ expect(result).toEqual({
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: JSON.stringify(objData, null, 2),
27
+ },
28
+ ],
29
+ });
30
+ expect(result.isError).toBeUndefined();
31
+ });
32
+ });
33
+
34
+ describe("createErrorResult", () => {
35
+ it("should format error result with message and isError set to true", () => {
36
+ const result = createErrorResult("Something went wrong");
37
+ expect(result).toEqual({
38
+ content: [
39
+ {
40
+ type: "text",
41
+ text: "Something went wrong",
42
+ },
43
+ ],
44
+ isError: true,
45
+ });
46
+ });
47
+ });
48
+ });