@startsimpli/api 0.5.20 → 0.5.22
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.
- package/package.json +10 -10
- package/src/constants/endpoints.ts +33 -0
- package/src/index.ts +50 -1
- package/src/lib/__tests__/companies-api.test.ts +53 -0
- package/src/lib/__tests__/domain-claims-api.test.ts +68 -0
- package/src/lib/__tests__/team-invitations-api.test.ts +50 -0
- package/src/lib/__tests__/teams-api.test.ts +74 -0
- package/src/lib/companies-api.ts +48 -0
- package/src/lib/domain-claims-api.ts +63 -0
- package/src/lib/presentations-api.test.ts +104 -0
- package/src/lib/presentations-api.ts +166 -0
- package/src/lib/team-invitations-api.ts +49 -0
- package/src/lib/teams-api.ts +73 -0
- package/src/lib/users-api.ts +13 -0
- package/src/lib/workflows-api.test.ts +129 -0
- package/src/lib/workflows-api.ts +161 -115
- package/src/types/index.ts +22 -0
- package/src/types/team.ts +175 -0
- package/src/types/user.ts +20 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** Presentations API client — AI-generated slide decks (epic startsim-3ks).
|
|
2
|
+
|
|
3
|
+
Mirrors the other @startsimpli/api wrappers and the proven n8n "Slide Deck
|
|
4
|
+
Agent v9" pipeline: a deck has ONE derived style spec applied verbatim to every
|
|
5
|
+
slide; each slide is a full-bleed 16:9 image (image render mode), compiled into
|
|
6
|
+
PPTX + PDF. The client auto-transforms snake_case <-> camelCase, so types here
|
|
7
|
+
are camelCase.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PaginatedResponse } from '../types';
|
|
11
|
+
import type { ApiClient } from './api-client';
|
|
12
|
+
|
|
13
|
+
const E = {
|
|
14
|
+
decks: 'api/v1/presentations/decks',
|
|
15
|
+
deck: (id: string) => `api/v1/presentations/decks/${id}`,
|
|
16
|
+
generate: (id: string) => `api/v1/presentations/decks/${id}/generate`,
|
|
17
|
+
compile: (id: string) => `api/v1/presentations/decks/${id}/compile`,
|
|
18
|
+
status: (id: string) => `api/v1/presentations/decks/${id}/status`,
|
|
19
|
+
slides: (id: string) => `api/v1/presentations/decks/${id}/slides`,
|
|
20
|
+
slide: (id: string, n: number) => `api/v1/presentations/decks/${id}/slides/${n}`,
|
|
21
|
+
regenerate: (id: string, n: number) => `api/v1/presentations/decks/${id}/slides/${n}/regenerate`,
|
|
22
|
+
reorder: (id: string) => `api/v1/presentations/decks/${id}/slides/reorder`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type DeckStatus = 'draft' | 'generating' | 'ready' | 'error';
|
|
26
|
+
export type SlideStatus = 'pending' | 'generating' | 'ready' | 'error';
|
|
27
|
+
export type RenderMode = 'image' | 'structured';
|
|
28
|
+
|
|
29
|
+
export interface Slide {
|
|
30
|
+
id: string;
|
|
31
|
+
deckId: string;
|
|
32
|
+
slideNumber: number;
|
|
33
|
+
title?: string;
|
|
34
|
+
/** Raw text content / outline for this slide. */
|
|
35
|
+
content: string;
|
|
36
|
+
/** The full image-generation prompt (style spec + slide content). */
|
|
37
|
+
imagePrompt?: string;
|
|
38
|
+
/** Full-bleed slide image (image render mode). */
|
|
39
|
+
imageUrl?: string;
|
|
40
|
+
status: SlideStatus;
|
|
41
|
+
renderMode: RenderMode;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Deck {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
/** Pasted outline the deck was generated from. */
|
|
50
|
+
outline: string;
|
|
51
|
+
/** Free-text audience / context that informs the style spec. */
|
|
52
|
+
audience: string;
|
|
53
|
+
/** The single derived STYLE SPEC, reused verbatim on every slide. */
|
|
54
|
+
styleSpec: string;
|
|
55
|
+
status: DeckStatus;
|
|
56
|
+
renderMode: RenderMode;
|
|
57
|
+
slideCount: number;
|
|
58
|
+
/** Compiled artifacts (present once compiled). */
|
|
59
|
+
pptxUrl?: string | null;
|
|
60
|
+
pdfUrl?: string | null;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
/** Present on detail responses. */
|
|
64
|
+
slides?: Slide[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DeckInput {
|
|
68
|
+
title: string;
|
|
69
|
+
outline?: string;
|
|
70
|
+
audience?: string;
|
|
71
|
+
renderMode?: RenderMode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface GenerateDeckInput {
|
|
75
|
+
/** Outline to (re)generate from; defaults to the deck's stored outline. */
|
|
76
|
+
outline?: string;
|
|
77
|
+
/** Optional explicit style spec; otherwise the backend derives one. */
|
|
78
|
+
styleSpec?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SlideInput {
|
|
82
|
+
title?: string;
|
|
83
|
+
content?: string;
|
|
84
|
+
imagePrompt?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RegenerateSlideInput {
|
|
88
|
+
/** Revised content for the slide. */
|
|
89
|
+
content?: string;
|
|
90
|
+
/** Natural-language change request ("make it punchier"). */
|
|
91
|
+
instruction?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Lightweight per-slide progress for polling during generation. */
|
|
95
|
+
export interface DeckGenerationStatus {
|
|
96
|
+
status: DeckStatus;
|
|
97
|
+
slides: Array<{
|
|
98
|
+
slideNumber: number;
|
|
99
|
+
status: SlideStatus;
|
|
100
|
+
imageUrl?: string;
|
|
101
|
+
}>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface CompileResult {
|
|
105
|
+
status: DeckStatus;
|
|
106
|
+
pptxUrl: string;
|
|
107
|
+
pdfUrl: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface DeckListParams {
|
|
111
|
+
page?: number;
|
|
112
|
+
pageSize?: number;
|
|
113
|
+
search?: string;
|
|
114
|
+
ordering?: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class PresentationsApi {
|
|
119
|
+
constructor(private client: ApiClient) {}
|
|
120
|
+
|
|
121
|
+
// --- Decks ---
|
|
122
|
+
listDecks(params?: DeckListParams): Promise<PaginatedResponse<Deck>> {
|
|
123
|
+
return this.client.fetch.get<PaginatedResponse<Deck>>(E.decks, { params });
|
|
124
|
+
}
|
|
125
|
+
getDeck(id: string): Promise<Deck> {
|
|
126
|
+
return this.client.fetch.get<Deck>(E.deck(id));
|
|
127
|
+
}
|
|
128
|
+
createDeck(data: DeckInput): Promise<Deck> {
|
|
129
|
+
return this.client.fetch.post<Deck>(E.decks, data);
|
|
130
|
+
}
|
|
131
|
+
updateDeck(id: string, data: Partial<DeckInput>): Promise<Deck> {
|
|
132
|
+
return this.client.fetch.patch<Deck>(E.deck(id), data);
|
|
133
|
+
}
|
|
134
|
+
deleteDeck(id: string): Promise<void> {
|
|
135
|
+
return this.client.fetch.delete<void>(E.deck(id));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Generation pipeline (mirrors the n8n agent tools) ---
|
|
139
|
+
/** Derive the style spec (if absent) and generate every slide image async. */
|
|
140
|
+
generateDeck(id: string, data?: GenerateDeckInput): Promise<Deck> {
|
|
141
|
+
return this.client.fetch.post<Deck>(E.generate(id), data);
|
|
142
|
+
}
|
|
143
|
+
/** Per-slide progress, for polling while generation/compile runs. */
|
|
144
|
+
getDeckStatus(id: string): Promise<DeckGenerationStatus> {
|
|
145
|
+
return this.client.fetch.get<DeckGenerationStatus>(E.status(id));
|
|
146
|
+
}
|
|
147
|
+
/** Build the Google-Slides-equivalent deck and export PPTX + PDF. */
|
|
148
|
+
compileDeck(id: string): Promise<CompileResult> {
|
|
149
|
+
return this.client.fetch.post<CompileResult>(E.compile(id), undefined);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Slides ---
|
|
153
|
+
listSlides(id: string): Promise<Slide[]> {
|
|
154
|
+
return this.client.fetch.get<Slide[]>(E.slides(id));
|
|
155
|
+
}
|
|
156
|
+
updateSlide(id: string, slideNumber: number, data: SlideInput): Promise<Slide> {
|
|
157
|
+
return this.client.fetch.patch<Slide>(E.slide(id, slideNumber), data);
|
|
158
|
+
}
|
|
159
|
+
/** Regenerate a single slide image (keeps the deck style spec). */
|
|
160
|
+
regenerateSlide(id: string, slideNumber: number, data?: RegenerateSlideInput): Promise<Slide> {
|
|
161
|
+
return this.client.fetch.post<Slide>(E.regenerate(id, slideNumber), data);
|
|
162
|
+
}
|
|
163
|
+
reorderSlides(id: string, order: number[]): Promise<void> {
|
|
164
|
+
return this.client.fetch.post<void>(E.reorder(id), { order });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamInvitations API wrapper for /api/v1/team-invitations/*.
|
|
3
|
+
*
|
|
4
|
+
* The accept endpoint is intentionally public-ish — it accepts an unauth
|
|
5
|
+
* caller carrying the one-time invite token in the body. The TeamMember
|
|
6
|
+
* row is created idempotently by the backend manager (startsim-tsm).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ApiClient } from './api-client';
|
|
10
|
+
import { ENDPOINTS } from '../constants/endpoints';
|
|
11
|
+
import type {
|
|
12
|
+
TeamInvitation,
|
|
13
|
+
TeamInvitationListParams,
|
|
14
|
+
CreateTeamInvitationInput,
|
|
15
|
+
} from '../types/team';
|
|
16
|
+
import type { PaginatedResponse } from '../types';
|
|
17
|
+
|
|
18
|
+
export class TeamInvitationsApi {
|
|
19
|
+
constructor(private client: ApiClient) {}
|
|
20
|
+
|
|
21
|
+
list(params?: TeamInvitationListParams): Promise<PaginatedResponse<TeamInvitation>> {
|
|
22
|
+
return this.client.fetch.get<PaginatedResponse<TeamInvitation>>(
|
|
23
|
+
ENDPOINTS.TEAM_INVITATIONS,
|
|
24
|
+
{ params },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Send a single invitation. The create response includes the raw token;
|
|
30
|
+
* subsequent reads will redact it.
|
|
31
|
+
*/
|
|
32
|
+
create(input: CreateTeamInvitationInput): Promise<TeamInvitation> {
|
|
33
|
+
return this.client.fetch.post<TeamInvitation>(ENDPOINTS.TEAM_INVITATIONS, input);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
revoke(id: string): Promise<void> {
|
|
37
|
+
return this.client.fetch.post<void>(ENDPOINTS.TEAM_INVITATION_REVOKE(id));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Accept an invitation. The caller posts the one-time token; the backend
|
|
42
|
+
* wires the TeamMember idempotently (startsim-tsm).
|
|
43
|
+
*/
|
|
44
|
+
accept(id: string, token: string): Promise<TeamInvitation> {
|
|
45
|
+
return this.client.fetch.post<TeamInvitation>(ENDPOINTS.TEAM_INVITATION_ACCEPT(id), {
|
|
46
|
+
token,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teams API wrapper for /api/v1/teams/* plus the bulk-mgmt actions shipped
|
|
3
|
+
* by startsim-tsm (bulk-invite, remove-member, update-role).
|
|
4
|
+
*
|
|
5
|
+
* The Django TeamViewSet accepts numeric id OR slug. Member-mutation
|
|
6
|
+
* endpoints take user_id in the body, not the URL. startsim-o7s.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ApiClient } from './api-client';
|
|
10
|
+
import { ENDPOINTS } from '../constants/endpoints';
|
|
11
|
+
import type {
|
|
12
|
+
Team,
|
|
13
|
+
TeamMember,
|
|
14
|
+
TeamListParams,
|
|
15
|
+
BulkInviteEntry,
|
|
16
|
+
BulkInviteResult,
|
|
17
|
+
TeamRole,
|
|
18
|
+
} from '../types/team';
|
|
19
|
+
import type { PaginatedResponse } from '../types';
|
|
20
|
+
|
|
21
|
+
/** Shape returned by /team-members/my-teams/ — a flat array of memberships. */
|
|
22
|
+
export interface MyTeamMembership extends TeamMember {
|
|
23
|
+
team?: Team;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TeamsApi {
|
|
27
|
+
constructor(private client: ApiClient) {}
|
|
28
|
+
|
|
29
|
+
/** List teams visible to the current user. */
|
|
30
|
+
list(params?: TeamListParams): Promise<PaginatedResponse<Team>> {
|
|
31
|
+
return this.client.fetch.get<PaginatedResponse<Team>>(ENDPOINTS.TEAMS, { params });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Retrieve a single team by id or slug. */
|
|
35
|
+
retrieve(idOrSlug: string): Promise<Team> {
|
|
36
|
+
return this.client.fetch.get<Team>(ENDPOINTS.TEAM(idOrSlug));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** List members of a team. Backend returns the full set (no pagination). */
|
|
40
|
+
members(idOrSlug: string): Promise<TeamMember[]> {
|
|
41
|
+
return this.client.fetch.get<TeamMember[]>(ENDPOINTS.TEAM_MEMBERS(idOrSlug));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bulk-invite endpoint shipped by startsim-tsm. Sends one POST with the
|
|
46
|
+
* full list of {email, role} pairs and returns invited + skipped.
|
|
47
|
+
*/
|
|
48
|
+
bulkInvite(idOrSlug: string, invitations: BulkInviteEntry[]): Promise<BulkInviteResult> {
|
|
49
|
+
return this.client.fetch.post<BulkInviteResult>(ENDPOINTS.TEAM_BULK_INVITE(idOrSlug), {
|
|
50
|
+
invitations,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Remove a member from the team. user_id goes in the body (not the URL). */
|
|
55
|
+
removeMember(idOrSlug: string, userId: string): Promise<void> {
|
|
56
|
+
return this.client.fetch.post<void>(ENDPOINTS.TEAM_REMOVE_MEMBER(idOrSlug), {
|
|
57
|
+
userId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Promote/demote a member — fails if it would leave zero OWNERs. */
|
|
62
|
+
updateRole(idOrSlug: string, userId: string, role: TeamRole): Promise<TeamMember> {
|
|
63
|
+
return this.client.fetch.post<TeamMember>(ENDPOINTS.TEAM_UPDATE_ROLE(idOrSlug), {
|
|
64
|
+
userId,
|
|
65
|
+
role,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Return the current user's membership rows across every team they touch. */
|
|
70
|
+
myTeams(): Promise<MyTeamMembership[]> {
|
|
71
|
+
return this.client.fetch.get<MyTeamMembership[]>(ENDPOINTS.TEAM_MEMBERS_MY_TEAMS);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/lib/users-api.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
UpdateProfileRequest,
|
|
8
8
|
ChangePasswordRequest,
|
|
9
9
|
ChangePasswordResponse,
|
|
10
|
+
EarlyRegisterRequest,
|
|
11
|
+
EarlyRegisterResponse,
|
|
10
12
|
} from '../types/user';
|
|
11
13
|
import { ENDPOINTS } from '../constants/endpoints';
|
|
12
14
|
import type { ApiClient } from './api-client';
|
|
@@ -37,4 +39,15 @@ export class UsersApi {
|
|
|
37
39
|
data
|
|
38
40
|
);
|
|
39
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Passwordless early-access registration (name + email only).
|
|
45
|
+
* Public endpoint — no auth token required.
|
|
46
|
+
*/
|
|
47
|
+
async earlyRegister(data: EarlyRegisterRequest): Promise<EarlyRegisterResponse> {
|
|
48
|
+
return this.client.fetch.post<EarlyRegisterResponse>(
|
|
49
|
+
ENDPOINTS.AUTH_EARLY_REGISTER,
|
|
50
|
+
data
|
|
51
|
+
);
|
|
52
|
+
}
|
|
40
53
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { WorkflowsApi } from './workflows-api';
|
|
3
|
+
|
|
4
|
+
function makeApi() {
|
|
5
|
+
const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
|
|
6
|
+
const api = new WorkflowsApi({ fetch } as never);
|
|
7
|
+
return { api, fetch };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('WorkflowsApi', () => {
|
|
11
|
+
let api: WorkflowsApi;
|
|
12
|
+
let fetch: {
|
|
13
|
+
get: ReturnType<typeof vi.fn>;
|
|
14
|
+
post: ReturnType<typeof vi.fn>;
|
|
15
|
+
patch: ReturnType<typeof vi.fn>;
|
|
16
|
+
delete: ReturnType<typeof vi.fn>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
({ api, fetch } = makeApi());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// --- Workflows CRUD ---
|
|
24
|
+
it('listWorkflows hits the workflows endpoint with params', async () => {
|
|
25
|
+
fetch.get.mockResolvedValue({ results: [], count: 0 });
|
|
26
|
+
await api.listWorkflows({ page: 2 });
|
|
27
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows', { params: { page: 2 } });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('getWorkflow fetches a single workflow by id', async () => {
|
|
31
|
+
fetch.get.mockResolvedValue({ id: 'w1', name: 'Onboarding' });
|
|
32
|
+
const w = await api.getWorkflow('w1');
|
|
33
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/w1');
|
|
34
|
+
expect(w.name).toBe('Onboarding');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('createWorkflow posts the workflow input', async () => {
|
|
38
|
+
fetch.post.mockResolvedValue({ id: 'w1', name: 'Onboarding' });
|
|
39
|
+
await api.createWorkflow({ name: 'Onboarding', description: 'desc' });
|
|
40
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows', {
|
|
41
|
+
name: 'Onboarding',
|
|
42
|
+
description: 'desc',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('updateWorkflow patches by id', async () => {
|
|
47
|
+
fetch.patch.mockResolvedValue({ id: 'w1', name: 'New' });
|
|
48
|
+
await api.updateWorkflow('w1', { name: 'New' });
|
|
49
|
+
expect(fetch.patch).toHaveBeenCalledWith('api/v1/workflows/w1', { name: 'New' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('deleteWorkflow deletes by id', async () => {
|
|
53
|
+
fetch.delete.mockResolvedValue(undefined);
|
|
54
|
+
await api.deleteWorkflow('w1');
|
|
55
|
+
expect(fetch.delete).toHaveBeenCalledWith('api/v1/workflows/w1');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- Node types ---
|
|
59
|
+
it('getNodeTypes fetches the node-type registry', async () => {
|
|
60
|
+
fetch.get.mockResolvedValue([
|
|
61
|
+
{ slug: 'http', category: 'action', label: 'HTTP Request', parameterSchema: {}, outputLabels: ['main'] },
|
|
62
|
+
]);
|
|
63
|
+
const types = await api.getNodeTypes();
|
|
64
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/node-types');
|
|
65
|
+
expect(types[0].slug).toBe('http');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- Execution ---
|
|
69
|
+
it('executeWorkflow posts context and returns an executionId', async () => {
|
|
70
|
+
fetch.post.mockResolvedValue({ executionId: 'e1', status: 'running' });
|
|
71
|
+
const res = await api.executeWorkflow('w1', { foo: 'bar' });
|
|
72
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/execute', { contextData: { foo: 'bar' } });
|
|
73
|
+
expect(res.executionId).toBe('e1');
|
|
74
|
+
expect(res.status).toBe('running');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('executeWorkflow works with no context data', async () => {
|
|
78
|
+
fetch.post.mockResolvedValue({ executionId: 'e2', status: 'pending' });
|
|
79
|
+
await api.executeWorkflow('w1');
|
|
80
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/execute', { contextData: undefined });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('getExecution fetches a single execution detail by executionId', async () => {
|
|
84
|
+
fetch.get.mockResolvedValue({ status: 'running', state: {}, nodeExecutions: [] });
|
|
85
|
+
const e = await api.getExecution('e1');
|
|
86
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/executions/e1');
|
|
87
|
+
expect(e.status).toBe('running');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('listExecutions fetches executions for a workflow with params', async () => {
|
|
91
|
+
fetch.get.mockResolvedValue({ results: [], count: 0 });
|
|
92
|
+
await api.listExecutions('w1', { page: 1 });
|
|
93
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/w1/executions', { params: { page: 1 } });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('cancelExecution posts to the cancel endpoint', async () => {
|
|
97
|
+
fetch.post.mockResolvedValue({ id: 'e1', status: 'cancelled' });
|
|
98
|
+
await api.cancelExecution('e1');
|
|
99
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/executions/e1/cancel', undefined);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('retryExecution posts to the retry endpoint and returns an executionId', async () => {
|
|
103
|
+
fetch.post.mockResolvedValue({ executionId: 'e2', status: 'pending' });
|
|
104
|
+
const res = await api.retryExecution('e1');
|
|
105
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/executions/e1/retry', undefined);
|
|
106
|
+
expect(res.executionId).toBe('e2');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// --- Lifecycle ---
|
|
110
|
+
it('cloneWorkflow posts to the clone endpoint', async () => {
|
|
111
|
+
fetch.post.mockResolvedValue({ id: 'w2', name: 'Onboarding (copy)' });
|
|
112
|
+
const w = await api.cloneWorkflow('w1');
|
|
113
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/clone', undefined);
|
|
114
|
+
expect(w.id).toBe('w2');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('publishWorkflow posts to the publish endpoint', async () => {
|
|
118
|
+
fetch.post.mockResolvedValue({ id: 'w1', isActive: true });
|
|
119
|
+
await api.publishWorkflow('w1');
|
|
120
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/publish', undefined);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('getVersions fetches the workflow version history', async () => {
|
|
124
|
+
fetch.get.mockResolvedValue([{ id: 'w1', version: 1 }]);
|
|
125
|
+
const versions = await api.getVersions('w1');
|
|
126
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/w1/versions');
|
|
127
|
+
expect(versions[0].version).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
});
|