Skip to content

Commit 9932264

Browse files
refactor(mobile): reuse task API client
Move task runs and automations onto the existing API client and derive automation contracts from its generated schema. Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
1 parent 74c2c57 commit 9932264

12 files changed

Lines changed: 174 additions & 175 deletions

File tree

apps/mobile/src/features/tasks/api.test.ts

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

3-
const { mockFetch } = vi.hoisted(() => ({
3+
const { mockFetch, mockRunTaskInCloud } = vi.hoisted(() => ({
44
mockFetch: vi.fn(),
5+
mockRunTaskInCloud: vi.fn(),
56
}));
67

78
vi.mock("expo/fetch", () => ({
@@ -34,6 +35,10 @@ vi.mock("@/lib/api", () => ({
3435
}),
3536
}));
3637

38+
vi.mock("@/lib/posthogApiClient", () => ({
39+
getPostHogApiClient: () => ({ runTaskInCloud: mockRunTaskInCloud }),
40+
}));
41+
3742
import { cancelRun, HttpError, runTaskInCloud } from "./api";
3843

3944
function bodyOf(call: unknown): Record<string, unknown> {
@@ -43,34 +48,37 @@ function bodyOf(call: unknown): Record<string, unknown> {
4348

4449
describe("runTaskInCloud", () => {
4550
beforeEach(() => {
46-
mockFetch.mockReset();
47-
mockFetch.mockResolvedValue(
48-
new Response(JSON.stringify({ id: "task-1" }), { status: 200 }),
49-
);
51+
mockRunTaskInCloud.mockReset();
52+
mockRunTaskInCloud.mockResolvedValue({ id: "task-1" });
5053
});
5154

5255
it.each([true, false])(
5356
"forwards auto_publish=%s to the payload",
5457
async (flag) => {
5558
await runTaskInCloud("task-1", { autoPublish: flag });
5659

57-
expect(bodyOf(mockFetch.mock.calls[0])).toMatchObject({
58-
auto_publish: flag,
59-
});
60+
expect(mockRunTaskInCloud).toHaveBeenCalledWith(
61+
"task-1",
62+
undefined,
63+
expect.objectContaining({ autoPublish: flag }),
64+
);
6065
},
6166
);
6267

6368
it("omits auto_publish when not provided", async () => {
6469
await runTaskInCloud("task-1", { model: "claude-opus-4-8" });
6570

66-
expect(bodyOf(mockFetch.mock.calls[0])).not.toHaveProperty("auto_publish");
71+
expect(mockRunTaskInCloud).toHaveBeenCalledWith(
72+
"task-1",
73+
undefined,
74+
expect.objectContaining({ autoPublish: undefined }),
75+
);
6776
});
6877

6978
it("sends no body for the plain initial run", async () => {
7079
await runTaskInCloud("task-1");
7180

72-
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
73-
expect(init.body).toBeUndefined();
81+
expect(mockRunTaskInCloud).toHaveBeenCalledWith("task-1");
7482
});
7583

7684
it("forwards the selected sandbox environment and custom image", async () => {
@@ -79,10 +87,14 @@ describe("runTaskInCloud", () => {
7987
customImageId: "image-123",
8088
});
8189

82-
expect(bodyOf(mockFetch.mock.calls[0])).toMatchObject({
83-
sandbox_environment_id: "environment-123",
84-
custom_image_id: "image-123",
85-
});
90+
expect(mockRunTaskInCloud).toHaveBeenCalledWith(
91+
"task-1",
92+
undefined,
93+
expect.objectContaining({
94+
sandboxEnvironmentId: "environment-123",
95+
customImageId: "image-123",
96+
}),
97+
);
8698
});
8799

88100
it("omits the sandbox environment and custom image when unset", async () => {
@@ -92,24 +104,34 @@ describe("runTaskInCloud", () => {
92104
customImageId: null,
93105
});
94106

95-
const body = bodyOf(mockFetch.mock.calls[0]);
96-
expect(body).not.toHaveProperty("sandbox_environment_id");
97-
expect(body).not.toHaveProperty("custom_image_id");
107+
expect(mockRunTaskInCloud).toHaveBeenCalledWith(
108+
"task-1",
109+
undefined,
110+
expect.objectContaining({
111+
sandboxEnvironmentId: undefined,
112+
customImageId: undefined,
113+
}),
114+
);
98115
});
99116

100117
it("sends rtk_enabled=false when the run opts out", async () => {
101118
await runTaskInCloud("task-1", { rtkEnabled: false });
102119

103-
expect(bodyOf(mockFetch.mock.calls[0])).toMatchObject({
104-
rtk_enabled: false,
105-
});
120+
expect(mockRunTaskInCloud).toHaveBeenCalledWith(
121+
"task-1",
122+
undefined,
123+
expect.objectContaining({ rtkEnabled: false }),
124+
);
106125
});
107126

108127
it("omits rtk_enabled when the run keeps compression on", async () => {
109128
await runTaskInCloud("task-1", { rtkEnabled: true });
110129

111-
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
112-
expect(init.body).toBeUndefined();
130+
expect(mockRunTaskInCloud).toHaveBeenCalledWith(
131+
"task-1",
132+
undefined,
133+
expect.objectContaining({ rtkEnabled: true }),
134+
);
113135
});
114136
});
115137

apps/mobile/src/features/tasks/api.ts

Lines changed: 28 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { Adapter, StoredLogEntry, Task, TaskRun } from "@posthog/shared";
1+
import type {
2+
Adapter,
3+
ExecutionMode,
4+
StoredLogEntry,
5+
Task,
6+
TaskRun,
7+
} from "@posthog/shared";
28
import { fetch } from "expo/fetch";
39
import {
410
authedFetch,
@@ -8,6 +14,7 @@ import {
814
getProjectId,
915
HttpError,
1016
} from "@/lib/api";
17+
import { getPostHogApiClient } from "@/lib/posthogApiClient";
1118

1219
export { HttpError } from "@/lib/api";
1320

@@ -78,84 +85,34 @@ export interface RunTaskInCloudOptions {
7885
autoPublish?: boolean;
7986
/** Only false is sent: opts the run out of rtk command-output compression. */
8087
rtkEnabled?: boolean;
88+
sandboxEnvironmentId?: string | null;
89+
customImageId?: string | null;
8190
}
8291

8392
export async function runTaskInCloud(
8493
taskId: string,
8594
options?: RunTaskInCloudOptions,
8695
): Promise<Task> {
87-
const baseUrl = getBaseUrl();
88-
const projectId = getProjectId();
89-
90-
// Only serialize a body when we have options to send. Sending an empty
91-
// or minimal body on the initial run historically changed backend
92-
// behavior, so we preserve the "no body" path for the common case.
93-
const hasOptions =
94-
!!options &&
95-
(options.branch !== undefined ||
96-
options.resumeFromRunId !== undefined ||
97-
options.pendingUserMessage !== undefined ||
98-
options.mode !== undefined ||
99-
options.runtimeAdapter !== undefined ||
100-
options.model !== undefined ||
101-
options.reasoningEffort !== undefined ||
102-
options.initialPermissionMode !== undefined ||
103-
options.runSource !== undefined ||
104-
options.signalReportId !== undefined ||
105-
options.autoPublish !== undefined ||
106-
options.rtkEnabled === false);
107-
108-
let body: string | undefined;
109-
if (hasOptions) {
110-
const payload: Record<string, unknown> = {
111-
mode: options?.mode ?? "interactive",
112-
};
113-
if (options?.branch) payload.branch = options.branch;
114-
if (options?.resumeFromRunId) {
115-
payload.resume_from_run_id = options.resumeFromRunId;
116-
}
117-
if (options?.pendingUserMessage) {
118-
payload.pending_user_message = options.pendingUserMessage;
119-
}
120-
if (options?.runtimeAdapter) {
121-
payload.runtime_adapter = options.runtimeAdapter;
122-
if (options?.model) payload.model = options.model;
123-
if (options?.reasoningEffort) {
124-
payload.reasoning_effort = options.reasoningEffort;
125-
}
126-
}
127-
if (options?.initialPermissionMode) {
128-
payload.initial_permission_mode = options.initialPermissionMode;
129-
}
130-
if (options?.runSource) payload.run_source = options.runSource;
131-
if (options?.signalReportId)
132-
payload.signal_report_id = options.signalReportId;
133-
if (options?.autoPublish !== undefined) {
134-
payload.auto_publish = options.autoPublish;
135-
}
136-
if (options?.rtkEnabled === false) {
137-
payload.rtk_enabled = false;
138-
}
139-
body = JSON.stringify(payload);
96+
if (!options) {
97+
return getPostHogApiClient().runTaskInCloud(taskId);
14098
}
14199

142-
const response = await authedFetch(
143-
`${baseUrl}/api/projects/${projectId}/tasks/${taskId}/run/`,
144-
{
145-
method: "POST",
146-
body,
147-
},
148-
);
149-
150-
if (!response.ok) {
151-
throw new HttpError(
152-
response.status,
153-
response.statusText,
154-
"Failed to run task",
155-
);
156-
}
157-
158-
return await response.json();
100+
return getPostHogApiClient().runTaskInCloud(taskId, options.branch, {
101+
adapter: options.runtimeAdapter,
102+
model: options.model,
103+
reasoningLevel: options.reasoningEffort,
104+
initialPermissionMode: options.initialPermissionMode as
105+
| ExecutionMode
106+
| undefined,
107+
runSource: options.runSource,
108+
signalReportId: options.signalReportId,
109+
autoPublish: options.autoPublish,
110+
rtkEnabled: options.rtkEnabled,
111+
sandboxEnvironmentId: options.sandboxEnvironmentId ?? undefined,
112+
customImageId: options.customImageId ?? undefined,
113+
resumeFromRunId: options.resumeFromRunId,
114+
pendingUserMessage: options.pendingUserMessage,
115+
});
159116
}
160117

161118
export async function getTaskRun(

apps/mobile/src/features/tasks/components/AutomationDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Text } from "@components/text";
2+
import type { TaskAutomation } from "@posthog/api-client/posthog-client";
23
import type { TaskRun } from "@posthog/shared";
34
import { ActivityIndicator, Pressable, View } from "react-native";
4-
import type { TaskAutomation } from "../types";
55
import { formatAutomationScheduleSummary } from "../utils/automationSchedule";
66
import { getAutomationTemplatePresentation } from "../utils/automationTemplatePresentation";
77
import { AutomationStatusBadge } from "./AutomationStatusBadge";

apps/mobile/src/features/tasks/components/AutomationForm.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Text } from "@components/text";
2+
import type { CreateTaskAutomationOptions } from "@posthog/api-client/posthog-client";
23
import { CaretDown, GithubLogo } from "phosphor-react-native";
34
import { type MutableRefObject, useEffect, useMemo, useState } from "react";
45
import {
@@ -12,10 +13,7 @@ import { MarkdownText } from "@/features/chat/components/MarkdownText";
1213
import { useThemeColors } from "@/lib/theme";
1314
import { RepositoryPickerInline } from "../composer/RepositoryPickerInline";
1415
import { useIntegrations } from "../hooks/useIntegrations";
15-
import type {
16-
CreateTaskAutomationOptions,
17-
RepositorySelection,
18-
} from "../types";
16+
import type { RepositorySelection } from "../types";
1917
import {
2018
type AutomationScheduleDraft,
2119
buildCronExpression,

apps/mobile/src/features/tasks/components/AutomationItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Text } from "@components/text";
2+
import type { TaskAutomation } from "@posthog/api-client/posthog-client";
23
import type { TaskRun } from "@posthog/shared";
34
import { format, formatDistanceToNow } from "date-fns";
45
import { memo } from "react";
56
import { Pressable, View } from "react-native";
6-
import type { TaskAutomation } from "../types";
77
import { formatAutomationScheduleSummary } from "../utils/automationSchedule";
88
import { getAutomationTemplatePresentation } from "../utils/automationTemplatePresentation";
99
import { AutomationStatusBadge } from "./AutomationStatusBadge";

apps/mobile/src/features/tasks/components/AutomationList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Text } from "@components/text";
2+
import type { TaskAutomation } from "@posthog/api-client/posthog-client";
23
import { Plus } from "phosphor-react-native";
34
import {
45
ActivityIndicator,
@@ -10,7 +11,6 @@ import {
1011
import { useThemeColors } from "@/lib/theme";
1112
import { useAutomations } from "../hooks/useAutomations";
1213
import { useTasks } from "../hooks/useTasks";
13-
import type { TaskAutomation } from "../types";
1414
import { AutomationItem } from "./AutomationItem";
1515

1616
interface AutomationListProps {

apps/mobile/src/features/tasks/hooks/useAutomations.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2-
import { useAuthStore } from "@/features/auth";
3-
import { logger } from "@/lib/logger";
4-
import { getPostHogApiClient } from "@/lib/posthogApiClient";
51
import type {
62
CreateTaskAutomationOptions,
73
TaskAutomation,
84
UpdateTaskAutomationOptions,
9-
} from "../types";
5+
} from "@posthog/api-client/posthog-client";
6+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
7+
import { useAuthStore } from "@/features/auth";
8+
import { logger } from "@/lib/logger";
9+
import { getPostHogApiClient } from "@/lib/posthogApiClient";
1010
import { taskKeys } from "./useTasks";
1111

1212
const log = logger.scope("automations-mutations");

apps/mobile/src/features/tasks/types.ts

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,6 @@ import type {
55
TaskRunStatus,
66
} from "@posthog/shared";
77

8-
export interface TaskAutomation {
9-
id: string;
10-
name: string;
11-
prompt: string;
12-
repository: string;
13-
github_integration?: number | null;
14-
cron_expression: string;
15-
timezone?: string | null;
16-
template_id?: string | null;
17-
enabled: boolean;
18-
last_run_at: string | null;
19-
last_run_status: string | null;
20-
last_task_id: string | null;
21-
last_task_run_id: string | null;
22-
last_error: string | null;
23-
created_at: string;
24-
updated_at: string;
25-
}
26-
27-
export interface CreateTaskAutomationOptions {
28-
name: string;
29-
prompt: string;
30-
repository: string;
31-
github_integration?: number | null;
32-
cron_expression: string;
33-
timezone: string;
34-
enabled?: boolean;
35-
template_id?: string | null;
36-
}
37-
38-
export interface UpdateTaskAutomationOptions {
39-
name?: string;
40-
prompt?: string;
41-
repository?: string;
42-
github_integration?: number | null;
43-
cron_expression?: string;
44-
timezone?: string;
45-
enabled?: boolean;
46-
template_id?: string | null;
47-
}
48-
498
export interface MobileStoredLogEntry extends SharedStoredLogEntry {
509
direction?: "client" | "agent";
5110
}

apps/mobile/src/features/tasks/utils/automationSchedule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TaskAutomation } from "../types";
1+
import type { TaskAutomation } from "@posthog/api-client/posthog-client";
22

33
export type AutomationScheduleMode =
44
| "hourly"

apps/mobile/src/features/tasks/utils/automationTemplatePresentation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import type { TaskAutomation } from "@posthog/api-client/posthog-client";
12
import { parseSkillTemplateId } from "../skills/skillTemplateIds";
2-
import type { TaskAutomation } from "../types";
33

44
export interface AutomationTemplatePresentation {
55
templateName: string | null;

0 commit comments

Comments
 (0)