Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 4 additions & 20 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ members = [
"workers/approval",
"workers/approval-tiers",
"workers/bridge",
"workers/channel-bluesky",
"workers/channel-discord",
"workers/channel-mastodon",
"workers/channel-matrix",
"workers/channel-telegram",
"workers/channel-linkedin",
"workers/channel-reddit",
"workers/channel-slack",
"workers/channel-twitch",
"workers/coordination",
"workers/council",
"workers/cron",
"workers/directive",
Expand Down
178 changes: 178 additions & 0 deletions src/__tests__/channels-mastodon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// @ts-nocheck
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove @ts-nocheck from this suite.

Line 1 suppresses type checking for the entire file and also introduces a code comment under src/**, which this repo disallows.

As per coding guidelines: src/**/*.{ts,tsx,js,jsx}: Code should be self-documenting; do not use code comments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/channels-mastodon.test.ts` at line 1, Remove the top-line "//
`@ts-nocheck`" from the test file so TypeScript checking is enabled, then run the
type checker/tests to see reported errors and fix them by adding correct types
or safe casts in the test code (e.g., annotate mocked Mastodon client instances,
responses, and any helper functions used in channels-mastodon.test.ts); ensure
you import needed types from dependencies or use explicit "as" casts for test
fixtures instead of suppressing checks.

import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";

const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise<any> => {
if (fnId === "agent::chat") return { content: "Reply" };
return null;
});
const mockTriggerVoid = vi.fn();

const handlers: Record<string, Function> = {};
vi.mock("iii-sdk", () => ({
registerWorker: () => ({
registerFunction: (config: any, handler: Function) => {
handlers[config.id] = handler;
},
registerTrigger: vi.fn(),
trigger: (req: any) =>
req.action
? mockTriggerVoid(req.function_id, req.payload)
: mockTrigger(req.function_id, req.payload),
shutdown: vi.fn(),
}),
TriggerAction: { Void: () => ({}) },
}));

vi.mock("@agentos/shared/utils", () => ({
httpOk: (req: any, data: any) => data,
splitMessage: vi.fn((text: string, limit: number) => {
const chunks: string[] = [];
for (let i = 0; i < text.length; i += limit)
chunks.push(text.slice(i, i + limit));
return chunks.length ? chunks : [text];
}),
resolveAgent: vi.fn(async () => "default-agent"),
}));
Comment on lines +10 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the shared test harness/mocks instead of bespoke local scaffolding.

This file re-implements handler capture, call plumbing, and shared mocks locally. Please switch to src/__tests__/helpers.ts and include the standard shared module mocks (utils.js, logger.js, metrics.js, errors.js) to keep test behavior consistent across suites.

As per coding guidelines: src/__tests__/**/*.{ts,tsx}: Use shared test helpers from src/__tests__/helpers.ts including KV mock and request builders and Mock shared modules utils.js, logger.js, metrics.js, errors.js.

Also applies to: 65-69


const mockFetch = vi.fn(async () => ({
ok: true,
json: async () => ({ access_token: "test-token", id: "status-1" }),
}));
vi.stubGlobal("fetch", mockFetch);

beforeEach(() => {
mockTrigger.mockReset();
mockTrigger.mockImplementation(
async (fnId: string, data?: any): Promise<any> => {
if (fnId === "agent::chat") return { content: "Reply" };
return null;
},
);
mockTriggerVoid.mockClear();
mockFetch.mockClear();
mockFetch.mockImplementation(async () => ({
ok: true,
json: async () => ({ access_token: "test-token", id: "status-1" }),
}));
});

beforeAll(async () => {
process.env.MASTODON_INSTANCE = "https://mastodon.social";
process.env.MASTODON_TOKEN = "test-masto-token";
await import("../channels/mastodon.js");
});

async function call(id: string, input: any) {
const handler = handlers[id];
if (!handler) throw new Error(`Handler ${id} not registered`);
return handler(input);
}

describe("channel::mastodon::webhook", () => {
it("registers the handler", () => {
expect(handlers["channel::mastodon::webhook"]).toBeDefined();
});

it("ignores messages without status content", async () => {
const result = await call("channel::mastodon::webhook", {
body: { account: { acct: "user@masto.social" } },
});
expect(result.status_code).toBe(200);
});

it("processes valid mention", async () => {
const result = await call("channel::mastodon::webhook", {
body: {
account: { acct: "tester@masto.social" },
status: { content: "<p>Hello Mastodon</p>", id: "111222" },
},
});
expect(result.status_code).toBe(200);
});

it("strips HTML tags from content", async () => {
await call("channel::mastodon::webhook", {
body: {
account: { acct: "html@masto.social" },
status: { content: "<p>Plain <b>text</b> here</p>", id: "333" },
},
});
const chatCalls = mockTrigger.mock.calls.filter(
(c) => c[0] === "agent::chat",
);
expect(chatCalls[0][1].message).toBe("Plain text here");
});

it("routes to agent::chat with mastodon session", async () => {
await call("channel::mastodon::webhook", {
body: {
account: { acct: "session@masto.social" },
status: { content: "<p>Session test</p>", id: "444" },
},
});
const chatCalls = mockTrigger.mock.calls.filter(
(c) => c[0] === "agent::chat",
);
expect(chatCalls[0][1].sessionId).toBe("mastodon:session@masto.social");
});

it("uses account.id as fallback for session", async () => {
await call("channel::mastodon::webhook", {
body: {
account: { id: "12345" },
status: { content: "<p>ID fallback</p>", id: "555" },
},
});
const chatCalls = mockTrigger.mock.calls.filter(
(c) => c[0] === "agent::chat",
);
expect(chatCalls[0][1].sessionId).toBe("mastodon:12345");
});

it("sends reply as mastodon status", async () => {
await call("channel::mastodon::webhook", {
body: {
account: { acct: "reply@masto.social" },
status: { content: "<p>Reply</p>", id: "666" },
},
});
const statusCalls = mockFetch.mock.calls.filter(
(c) =>
(c[0] as string).includes("api/v1/statuses") &&
(c[1] as any)?.method === "POST",
);
expect(statusCalls.length).toBeGreaterThanOrEqual(1);
});

it("sends reply as reply to original status", async () => {
await call("channel::mastodon::webhook", {
body: {
account: { acct: "thread@masto.social" },
status: { content: "<p>Thread reply</p>", id: "777" },
},
});
const body = JSON.parse(
mockFetch.mock.calls.find(
(c) =>
(c[1] as any)?.method === "POST" &&
(c[0] as string).includes("statuses"),
)?.[1]?.body as string,
);
expect(body.in_reply_to_id).toBe("777");
});

it("emits audit event", async () => {
await call("channel::mastodon::webhook", {
body: {
account: { acct: "audit@masto.social" },
status: { content: "<p>Audit</p>", id: "888" },
},
});
expect(mockTriggerVoid).toHaveBeenCalledWith(
"security::audit",
expect.objectContaining({
detail: expect.objectContaining({ channel: "mastodon" }),
}),
);
});
});
Loading
Loading