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
78 changes: 78 additions & 0 deletions __tests__/app/jobs/jobs-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -538,4 +538,82 @@ describe("Running Jobs Page & JobsView Component", () => {
screen.getByText(/no jobs matched "nonexistent"/i),
).toBeInTheDocument();
});

it("suppresses error toast on background mount/polling but shows on manual refresh", async () => {
mockListJobs.mockResolvedValueOnce({
ok: true,
value: {
jobs: [],
offline: false,
error: "Failed to connect to background worker",
},
});

render(<JobsView />);

await waitFor(() => {
expect(mockListJobs).toHaveBeenCalledTimes(1);
});

// On mount / background polling, no toast should be emitted
expect(toast.error).not.toHaveBeenCalled();

// Now trigger manual refresh with same error condition
mockListJobs.mockResolvedValueOnce({
ok: true,
value: {
jobs: [],
offline: false,
error: "Failed to connect to background worker",
},
});

const refreshButton = screen.getByRole("button", { name: /refresh/i });
fireEvent.click(refreshButton);

await waitFor(() => {
expect(mockListJobs).toHaveBeenCalledTimes(2);
expect(toast.error).toHaveBeenCalledWith(
"Failed to connect to background worker",
);
});
});

it("suppresses toast on background polling action failure and network throws, shows on manual refresh", async () => {
// 1. Action failure on mount (background)
mockListJobs.mockResolvedValueOnce({
ok: false,
error: { message: "Server action failed" },
});

const { unmount } = render(<JobsView />);

await waitFor(() => {
expect(mockListJobs).toHaveBeenCalledTimes(1);
});

expect(toast.error).not.toHaveBeenCalled();
unmount();

// 2. Exception throw on manual refresh
mockListJobs.mockResolvedValueOnce({
ok: true,
value: { jobs: [], offline: false },
});

render(<JobsView />);

await waitFor(() => {
expect(mockListJobs).toHaveBeenCalledTimes(2);
});

mockListJobs.mockRejectedValueOnce(new Error("Network connection lost"));
const refreshButton = screen.getByRole("button", { name: /refresh/i });
fireEvent.click(refreshButton);

await waitFor(() => {
expect(mockListJobs).toHaveBeenCalledTimes(3);
expect(toast.error).toHaveBeenCalledWith("Network connection lost");
});
});
});
21 changes: 19 additions & 2 deletions __tests__/lib/inngest/run-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe("Inngest Run Service (Unified REST v2)", () => {
expect(capturedUrl).toContain("http://127.0.0.1:8288/v2/runs");
expect(capturedUrl).toContain("query=event.data.userId+%3D%3D+%22user-123%22");
expect(capturedUrl).toContain("limit=20");
expect(capturedUrl).toContain("include=output");
expect(capturedUrl).not.toContain("include=output");
expect(capturedHeaders?.Authorization).toBeUndefined();

expect(result.jobs).toHaveLength(1);
Expand Down Expand Up @@ -420,7 +420,7 @@ describe("Inngest Run Service (Unified REST v2)", () => {
expect(result.error).toBe("network aborted");
});

it("returns error string when Inngest responds with non-ok HTTP status", async () => {
it("returns error string when Inngest responds with non-ok HTTP status and flags offline: true", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
Expand All @@ -430,6 +430,7 @@ describe("Inngest Run Service (Unified REST v2)", () => {
const result = await fetchUserInngestRuns("user-123");

expect(result.jobs).toEqual([]);
expect(result.offline).toBe(true);
expect(result.error).toBe("Inngest API request failed with HTTP 404");
});

Expand Down Expand Up @@ -461,6 +462,22 @@ describe("Inngest Run Service (Unified REST v2)", () => {
expect(capturedUrl).toContain("http://my-inngest:8288/v2/runs");
expect(capturedUrl).not.toContain("///v2");
});

it("strips trailing /api/inngest on INNGEST_BASE_URL cleanly", async () => {
mockEnv.INNGEST_BASE_URL = "http://127.0.0.1:3000/api/inngest/";
let capturedUrl = "";

globalThis.fetch = vi.fn().mockImplementation(async (url: string) => {
capturedUrl = url.toString();
return { ok: true, status: 200, json: async () => ({ data: [] }) };
});

const { fetchUserInngestRuns } = await import("@/lib/inngest/run-service");
await fetchUserInngestRuns("user-123");

expect(capturedUrl).toContain("http://127.0.0.1:3000/v2/runs");
expect(capturedUrl).not.toContain("/api/inngest");
});
});

describe("cancelInngestRun", () => {
Expand Down
12 changes: 7 additions & 5 deletions components/jobs/jobs-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,18 @@ export function JobsView() {
if (res.ok) {
setJobs(res.value.jobs || []);
setOffline(Boolean(res.value.offline));
if (res.value.error && !res.value.offline) {
if (isManualRefresh && res.value.error && !res.value.offline) {
toast.error(res.value.error);
}
} else {
} else if (isManualRefresh) {
toast.error(res.error.message || "Failed to load jobs");
}
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to load jobs";
toast.error(message);
if (isManualRefresh) {
const message =
err instanceof Error ? err.message : "Failed to load jobs";
toast.error(message);
}
} finally {
setLoading(false);
}
Expand Down
2 changes: 1 addition & 1 deletion components/jobs/use-jobs-polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function useJobsPolling(

// Initial fetch on mount
useEffect(() => {
loadJobs(true);
loadJobs(false);
}, [loadJobs]);

// Tab visibility listener: pause polling when tab is hidden, refresh immediately when visible
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ services:
command: server /data --console-address ":9001"

inngest:
image: inngest/inngest:latest
image: inngest/inngest:v1.45.1
network_mode: host
environment:
- INNGEST_DEV=1
Expand Down
2 changes: 1 addition & 1 deletion lib/inngest/run-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ function buildRunsUrl(
const url = new URL(`${baseUrl}/v2/runs`);
url.searchParams.set("query", `event.data.userId == "${userId}"`);
url.searchParams.set("limit", String(options?.limit ?? 20));
url.searchParams.set("include", "output");

if (options?.status && options.status.length > 0) {
for (const s of options.status) {
Expand Down Expand Up @@ -240,6 +239,7 @@ export async function fetchUserInngestRuns(
if (!res.ok) {
return {
jobs: [],
offline: true,
error: `Inngest API request failed with HTTP ${res.status}`,
};
}
Expand Down
10 changes: 7 additions & 3 deletions lib/inngest/run-service/run-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,20 @@ export function resolveInngestEventKey(): string | undefined {

/**
* Resolves the Inngest API base URL for REST v2 queries.
* Trims trailing slashes to guarantee clean endpoint concatenation.
* Trims trailing slashes and removes accidental serve endpoint paths (/api/inngest).
*
* @returns Base URL without trailing slash
* @returns Base URL without trailing slash or serve endpoint suffix
* @author Maruf Bepary
*/
export function resolveInngestApiBaseUrl(): string {
const base =
env.INNGEST_BASE_URL ||
(isDevMode() ? "http://127.0.0.1:8288" : "https://api.inngest.com");
return base.replace(/\/+$/, "");
return base
.trim()
.replace(/\/+$/, "")
.replace(/\/api\/inngest$/, "")
.replace(/\/+$/, "");
}

/**
Expand Down
Loading