Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions dist/index.js
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may need a rebase

Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,43 @@ var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
function __accessProp(key) {
return this[key];
}
var __toESMCache_node;
var __toESMCache_esm;
var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
var cached = cache.get(mod);
if (cached)
return cached;
}
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
get: __accessProp.bind(mod, key),
enumerable: true
});
if (canCache)
cache.set(mod, to);
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
set: __exportSetter.bind(all, name)
});
};

Expand Down Expand Up @@ -3447,7 +3465,7 @@ var require_constants2 = __commonJS((exports2, module2) => {
}
})();
var channel;
var structuredClone = globalThis.structuredClone ?? function structuredClone(value, options = undefined) {
var structuredClone = globalThis.structuredClone ?? function structuredClone2(value, options = undefined) {
if (arguments.length === 0) {
throw new TypeError("missing argument");
}
Expand Down Expand Up @@ -16372,7 +16390,7 @@ var require_undici = __commonJS((exports2, module2) => {
module2.exports.getGlobalDispatcher = getGlobalDispatcher;
if (util.nodeMajor > 16 || util.nodeMajor === 16 && util.nodeMinor >= 8) {
let fetchImpl = null;
module2.exports.fetch = async function fetch(resource) {
module2.exports.fetch = async function fetch2(resource) {
if (!fetchImpl) {
fetchImpl = require_fetch().fetch;
}
Expand Down Expand Up @@ -22708,11 +22726,11 @@ var require_github = __commonJS((exports2) => {
});

// src/index.ts
var core2 = __toESM(require_core());
var github = __toESM(require_github());
var core2 = __toESM(require_core(), 1);
var github = __toESM(require_github(), 1);

// src/action.ts
var core = __toESM(require_core());
var core = __toESM(require_core(), 1);

class CoderAgentChatAction {
coder;
Expand Down
64 changes: 55 additions & 9 deletions src/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ActionOutputsSchema } from "./schemas";
import {
MockCoderClient,
createMockOctokit,
mockOctokitCommentingHappyPath,
createMockInputs,
mockUser,
mockChat,
Expand Down Expand Up @@ -182,7 +183,7 @@ describe("CoderAgentChatAction", () => {
inputs,
);

expect(
await expect(
action.commentOnIssue("url", "owner", "repo", 123),
).resolves.toBeUndefined();
});
Expand All @@ -191,6 +192,7 @@ describe("CoderAgentChatAction", () => {
test("creates new chat successfully", async () => {
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
coderClient.mockCreateChat.mockResolvedValue(mockChat);
mockOctokitCommentingHappyPath(octokit);

const inputs = createMockInputs({
githubUserID: 12345,
Expand All @@ -216,10 +218,12 @@ describe("CoderAgentChatAction", () => {
expect(parsedResult.chatUrl).toMatch(
/^https:\/\/coder\.test\/chats\/[a-f0-9-]+$/,
);
expect(octokit.rest.issues.createComment).toHaveBeenCalled();
});

test("creates chat using direct coder-username", async () => {
coderClient.mockCreateChat.mockResolvedValue(mockChat);
mockOctokitCommentingHappyPath(octokit);

const inputs = createMockInputs({
githubUserID: undefined,
Expand All @@ -239,13 +243,15 @@ describe("CoderAgentChatAction", () => {
const parsedResult = ActionOutputsSchema.parse(result);
expect(parsedResult.coderUsername).toBe(mockUser.username);
expect(parsedResult.chatCreated).toBe(true);
expect(octokit.rest.issues.createComment).toHaveBeenCalled();
});

test("sends message to existing chat", async () => {
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
coderClient.mockCreateChatMessage.mockResolvedValue(
mockChatMessageResponse,
);
mockOctokitCommentingHappyPath(octokit);

const existingChatId = "990e8400-e29b-41d4-a716-446655440000";
const inputs = createMockInputs({
Expand All @@ -272,11 +278,13 @@ describe("CoderAgentChatAction", () => {
const parsedResult = ActionOutputsSchema.parse(result);
expect(parsedResult.chatCreated).toBe(false);
expect(parsedResult.chatId).toBe(existingChatId);
expect(octokit.rest.issues.createComment).toHaveBeenCalled();
});

test("creates chat with workspace-id", async () => {
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
coderClient.mockCreateChat.mockResolvedValue(mockChat);
mockOctokitCommentingHappyPath(octokit);

const workspaceId = "550e8400-e29b-41d4-a716-446655440000";
const inputs = createMockInputs({
Expand All @@ -296,6 +304,7 @@ describe("CoderAgentChatAction", () => {
workspace_id: workspaceId,
}),
);
expect(octokit.rest.issues.createComment).toHaveBeenCalled();
});

describe("commentOnIssue toggle", () => {
Expand All @@ -319,15 +328,33 @@ describe("CoderAgentChatAction", () => {
expect(octokit.rest.issues.createComment).not.toHaveBeenCalled();
});

test("does not comment when commentOnIssue is false (existing chat path)", async () => {
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
coderClient.mockCreateChatMessage.mockResolvedValue(
mockChatMessageResponse,
);

const inputs = createMockInputs({
githubUserID: 12345,
existingChatId: "990e8400-e29b-41d4-a716-446655440000",
commentOnIssue: false,
});
const action = new CoderAgentChatAction(
coderClient,
octokit as unknown as Octokit,
inputs,
);

await action.run();

expect(octokit.rest.issues.listComments).not.toHaveBeenCalled();
expect(octokit.rest.issues.createComment).not.toHaveBeenCalled();
});

test("comments when commentOnIssue is true", async () => {
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
coderClient.mockCreateChat.mockResolvedValue(mockChat);
octokit.rest.issues.listComments.mockResolvedValue({
data: [],
} as ReturnType<typeof octokit.rest.issues.listComments>);
octokit.rest.issues.createComment.mockResolvedValue(
{} as ReturnType<typeof octokit.rest.issues.createComment>,
);
mockOctokitCommentingHappyPath(octokit);

const inputs = createMockInputs({
githubUserID: 12345,
Expand Down Expand Up @@ -360,11 +387,30 @@ describe("CoderAgentChatAction", () => {
inputs,
);

expect(action.run()).rejects.toThrow(
await expect(action.run()).rejects.toThrow(
"No Coder user found with GitHub user ID 12345",
);
});

test("throws error when sending message to existing chat fails", async () => {
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
coderClient.mockCreateChatMessage.mockRejectedValue(
new Error("Chat not found"),
);

const inputs = createMockInputs({
githubUserID: 12345,
existingChatId: "990e8400-e29b-41d4-a716-446655440000",
});
const action = new CoderAgentChatAction(
coderClient,
octokit as unknown as Octokit,
inputs,
);

await expect(action.run()).rejects.toThrow("Chat not found");
});

test("throws error when chat creation fails", async () => {
coderClient.mockGetCoderUserByGithubID.mockResolvedValue(mockUser);
coderClient.mockCreateChat.mockRejectedValue(
Expand All @@ -378,7 +424,7 @@ describe("CoderAgentChatAction", () => {
inputs,
);

expect(action.run()).rejects.toThrow("Failed to create chat");
await expect(action.run()).rejects.toThrow("Failed to create chat");
});
});
});
41 changes: 35 additions & 6 deletions src/coder-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, test, beforeEach, mock } from "bun:test";
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
import { RealCoderClient, CoderAPIError } from "./coder-client";
import {
mockUser,
Expand All @@ -14,14 +14,20 @@ import {
describe("CoderClient", () => {
let client: RealCoderClient;
let mockFetch: ReturnType<typeof mock>;
let originalFetch: typeof fetch;

beforeEach(() => {
originalFetch = global.fetch;
const mockInputs = createMockInputs();
client = new RealCoderClient(mockInputs.coderURL, mockInputs.coderToken);
mockFetch = mock(() => Promise.resolve(createMockResponse([])));
global.fetch = mockFetch as unknown as typeof fetch;
});

afterEach(() => {
global.fetch = originalFetch;
});

describe("getCoderUserByGitHubId", () => {
test("returns the user when found", async () => {
mockFetch.mockResolvedValue(createMockResponse(mockUserList));
Expand All @@ -42,14 +48,14 @@ describe("CoderClient", () => {

test("throws when multiple users found", async () => {
mockFetch.mockResolvedValue(createMockResponse(mockUserListDuplicate));
expect(
await expect(
client.getCoderUserByGitHubId(mockUser.github_com_user_id ?? 0),
).rejects.toThrow(CoderAPIError);
});

test("throws when no user found", async () => {
mockFetch.mockResolvedValue(createMockResponse(mockUserListEmpty));
expect(
await expect(
client.getCoderUserByGitHubId(mockUser.github_com_user_id ?? 0),
).rejects.toThrow(CoderAPIError);
});
Expand All @@ -61,13 +67,13 @@ describe("CoderClient", () => {
{ ok: false, status: 401, statusText: "Unauthorized" },
),
);
expect(
await expect(
client.getCoderUserByGitHubId(mockUser.github_com_user_id ?? 0),
).rejects.toThrow(CoderAPIError);
});

test("throws when GitHub user ID is 0", async () => {
expect(client.getCoderUserByGitHubId(0)).rejects.toThrow(
await expect(client.getCoderUserByGitHubId(0)).rejects.toThrow(
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoderUserByGitHubId(0) currently rejects by throwing a bare string (see RealCoderClient implementation), but this test now uses await expect(...).rejects.toThrow(...), which typically only matches Error objects. To avoid flaky/failing tests and to make the API consistent, change the client implementation to throw an Error/CoderAPIError for the 0 case (or adjust the assertion to match a string rejection).

Suggested change
await expect(client.getCoderUserByGitHubId(0)).rejects.toThrow(
await expect(client.getCoderUserByGitHubId(0)).rejects.toBe(

Copilot uses AI. Check for mistakes.
"GitHub user ID cannot be 0",
);
});
Expand Down Expand Up @@ -132,14 +138,29 @@ describe("CoderClient", () => {
{ ok: false, status: 404, statusText: "Not Found" },
),
);
expect(
await expect(
client.createChatMessage(mockChat.id, {
content: [{ type: "text", text: "Test" }],
}),
).rejects.toThrow(CoderAPIError);
});
});

describe("listChats", () => {
test("returns chat list", async () => {
mockFetch.mockResolvedValue(createMockResponse([mockChat]));
const result = await client.listChats();
expect(result).toHaveLength(1);
expect(result[0].id).toBe(mockChat.id);
});

test("returns empty list", async () => {
mockFetch.mockResolvedValue(createMockResponse([]));
const result = await client.listChats();
expect(result).toHaveLength(0);
});
});

describe("getChat", () => {
test("returns chat when found", async () => {
mockFetch.mockResolvedValue(createMockResponse(mockChat));
Expand All @@ -156,4 +177,12 @@ describe("CoderClient", () => {
);
});
});

describe("getCoderUserByGitHubId edge cases", () => {
test("throws when GitHub user ID is undefined", async () => {
await expect(client.getCoderUserByGitHubId(undefined)).rejects.toThrow(
"GitHub user ID cannot be undefined",
);
});
});
});
50 changes: 49 additions & 1 deletion src/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, test } from "bun:test";
import { type ActionInputs, ActionInputsSchema } from "./schemas";
import {
type ActionInputs,
ActionInputsSchema,
ActionOutputsSchema,
} from "./schemas";

const actionInputValid: ActionInputs = {
coderURL: "https://coder.test",
Expand Down Expand Up @@ -163,3 +167,47 @@ describe("ActionInputsSchema", () => {
});
});
});

describe("ActionOutputsSchema", () => {
test("accepts valid outputs", () => {
const result = ActionOutputsSchema.parse({
coderUsername: "testuser",
chatId: "550e8400-e29b-41d4-a716-446655440000",
chatUrl: "https://coder.test/chats/550e8400-e29b-41d4-a716-446655440000",
chatCreated: true,
});
expect(result.coderUsername).toBe("testuser");
});

test("rejects missing chatId", () => {
expect(() =>
ActionOutputsSchema.parse({
coderUsername: "testuser",
chatUrl: "https://coder.test/chats/abc",
chatCreated: true,
}),
).toThrow();
});

test("rejects invalid chatUrl", () => {
expect(() =>
ActionOutputsSchema.parse({
coderUsername: "testuser",
chatId: "550e8400-e29b-41d4-a716-446655440000",
chatUrl: "not-a-url",
chatCreated: true,
}),
).toThrow();
});

test("rejects non-boolean chatCreated", () => {
expect(() =>
ActionOutputsSchema.parse({
coderUsername: "testuser",
chatId: "550e8400-e29b-41d4-a716-446655440000",
chatUrl: "https://coder.test/chats/abc",
chatCreated: "yes",
}),
).toThrow();
});
});
Loading
Loading