@uipath/tasks-tool 1.199.0 → 1.201.0-preview.115

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uipath/tasks-tool",
3
3
  "license": "MIT",
4
- "version": "1.199.0",
4
+ "version": "1.201.0-preview.115",
5
5
  "description": "Manage Action Center tasks.",
6
6
  "type": "module",
7
7
  "main": "./dist/tool.js",
@@ -14,5 +14,5 @@
14
14
  "publishConfig": {
15
15
  "registry": "https://registry.npmjs.org/"
16
16
  },
17
- "gitHead": "723e6801b77b5926ba75e75b6a756cc38b1b7adc"
17
+ "gitHead": "f1086b73654d7728cb71f280588b3e0c77d535fc"
18
18
  }
@@ -22,6 +22,7 @@ const VALID_TASK_TYPES = [
22
22
  "DocumentValidationTask",
23
23
  "DocumentClassificationTask",
24
24
  "DataLabelingTask",
25
+ "QuickFormTask",
25
26
  ] as const;
26
27
  type ValidTaskType = (typeof VALID_TASK_TYPES)[number];
27
28
 
@@ -221,7 +222,7 @@ export const registerTasksCommand = (program: Command) => {
221
222
  )
222
223
  .option(
223
224
  "--task-type <type>",
224
- "Task type (FormTask, ExternalTask, AppTask, DocumentValidationTask, DocumentClassificationTask, DataLabelingTask)",
225
+ "Task type (FormTask, ExternalTask, AppTask, DocumentValidationTask, DocumentClassificationTask, DataLabelingTask, QuickFormTask)",
225
226
  )
226
227
  .option(
227
228
  "--folder-id <id>",
@@ -397,7 +398,7 @@ export const registerTasksCommand = (program: Command) => {
397
398
  .argument("<task-id>", "Task ID")
398
399
  .requiredOption(
399
400
  "--type <type>",
400
- "Task type (FormTask, ExternalTask, AppTask, DocumentValidationTask, DocumentClassificationTask, DataLabelingTask)",
401
+ "Task type (FormTask, ExternalTask, AppTask, DocumentValidationTask, DocumentClassificationTask, DataLabelingTask, QuickFormTask)",
401
402
  )
402
403
  .requiredOption("--folder-id <id>", "Folder ID (required)")
403
404
  .addOption(
@@ -0,0 +1,166 @@
1
+ import { RESULTS } from "@uipath/common";
2
+ import { beforeAll, describe, expect, it } from "vitest";
3
+ import {
4
+ type E2EManifest,
5
+ MANIFEST_PATH,
6
+ } from "../../../tests/e2e-shared/global-setup";
7
+ import {
8
+ EXEC_TIMEOUT,
9
+ localCliRaw,
10
+ parseOutput,
11
+ readManifest,
12
+ } from "../../../tests/e2e-shared/helpers";
13
+
14
+ // Scenario: scenarios/tasks-tool/assign.md
15
+ // `uip tasks assign` / `reassign` / `unassign` — assignment lifecycle.
16
+
17
+ interface AssignEnvelope {
18
+ Result: string;
19
+ Code?: string;
20
+ Data?: unknown;
21
+ Message?: string;
22
+ Instructions?: string;
23
+ }
24
+
25
+ // `tasks list`/`get` pass through the raw SDK task payload, which is
26
+ // PascalCase (Id/FolderId/Status/...) — confirmed against a live tenant.
27
+ interface TaskSummary {
28
+ Id?: number;
29
+ FolderId?: number;
30
+ Status?: string;
31
+ }
32
+
33
+ const { tempDir } = readManifest<E2EManifest>(MANIFEST_PATH).sharedInstall;
34
+
35
+ describe("tasks assign e2e", () => {
36
+ let taskId: string | undefined;
37
+ let folderId: string | undefined;
38
+ let userIds: string[] = [];
39
+
40
+ beforeAll(() => {
41
+ // Fetch a batch and pick an actually-unassigned task — assigning an
42
+ // already-assigned task is a valid but different code path, and
43
+ // asserting `TaskAssigned` against it would be a flaky fixture choice.
44
+ const listRaw = localCliRaw(
45
+ tempDir,
46
+ "tasks list --limit 50 --output json",
47
+ );
48
+ if (listRaw.exitCode === 0) {
49
+ const listRes = parseOutput(listRaw.stdout, "json") as {
50
+ Data?: TaskSummary[];
51
+ };
52
+ const task = (listRes.Data ?? []).find(
53
+ (t) => t.Status === "Unassigned",
54
+ );
55
+ if (task?.Id !== undefined) {
56
+ taskId = String(task.Id);
57
+ folderId =
58
+ task.FolderId !== undefined
59
+ ? String(task.FolderId)
60
+ : undefined;
61
+ }
62
+ }
63
+
64
+ if (folderId) {
65
+ const usersRaw = localCliRaw(
66
+ tempDir,
67
+ `tasks users ${folderId} --output json`,
68
+ );
69
+ if (usersRaw.exitCode === 0) {
70
+ // `tasks users` passes through the raw SDK payload, which is
71
+ // PascalCase (Id/Name/...) — unlike `tasks list`/`tasks get`,
72
+ // which return camelCase. Confirmed against a live tenant.
73
+ const usersRes = parseOutput(usersRaw.stdout, "json") as {
74
+ Data?: Array<{ Id?: number; Type?: string }>;
75
+ };
76
+ userIds = (usersRes.Data ?? [])
77
+ .filter((u) => u.Type === "DirectoryUser")
78
+ .map((u) => u.Id)
79
+ .filter((id): id is number => id !== undefined)
80
+ .map(String);
81
+ }
82
+ }
83
+ }, EXEC_TIMEOUT);
84
+
85
+ // ── Deterministic (no tenant) ────────────────────────────────────────────
86
+
87
+ it(
88
+ "S1: assign without --user-id or --user is rejected",
89
+ () => {
90
+ const raw = localCliRaw(tempDir, "tasks assign 5 --output json");
91
+ expect(raw.exitCode).toBe(1);
92
+ const res = parseOutput(raw.stdout, "json") as AssignEnvelope;
93
+ expect(res.Result).toBe(RESULTS.Failure);
94
+ expect(res.Message).toBe("Missing assignee");
95
+ expect(res.Instructions).toBe(
96
+ "Provide either --user-id or --user to specify the assignee.",
97
+ );
98
+ },
99
+ EXEC_TIMEOUT,
100
+ );
101
+
102
+ it(
103
+ "S2: reassign without --user-id or --user is rejected",
104
+ () => {
105
+ const raw = localCliRaw(tempDir, "tasks reassign 5 --output json");
106
+ expect(raw.exitCode).toBe(1);
107
+ const res = parseOutput(raw.stdout, "json") as AssignEnvelope;
108
+ expect(res.Result).toBe(RESULTS.Failure);
109
+ expect(res.Message).toBe("Missing assignee");
110
+ expect(res.Instructions).toBe(
111
+ "Provide either --user-id or --user to specify the new assignee.",
112
+ );
113
+ },
114
+ EXEC_TIMEOUT,
115
+ );
116
+
117
+ // ── Tenant read/write (guarded — skip if the tenant has no usable fixture) ─
118
+
119
+ it(
120
+ "S3: assigns a task to a user by --user-id",
121
+ () => {
122
+ if (!taskId || userIds.length === 0) return;
123
+ const raw = localCliRaw(
124
+ tempDir,
125
+ `tasks assign ${taskId} --user-id ${userIds[0]} --output json`,
126
+ );
127
+ expect(raw.exitCode).toBe(0);
128
+ const res = parseOutput(raw.stdout, "json") as AssignEnvelope;
129
+ expect(res.Result).toBe(RESULTS.Success);
130
+ expect(res.Code).toBe("TaskAssigned");
131
+ },
132
+ EXEC_TIMEOUT,
133
+ );
134
+
135
+ it(
136
+ "S4: reassigns a task to a different user",
137
+ () => {
138
+ if (!taskId || userIds.length < 2) return;
139
+ const raw = localCliRaw(
140
+ tempDir,
141
+ `tasks reassign ${taskId} --user-id ${userIds[1]} --output json`,
142
+ );
143
+ expect(raw.exitCode).toBe(0);
144
+ const res = parseOutput(raw.stdout, "json") as AssignEnvelope;
145
+ expect(res.Result).toBe(RESULTS.Success);
146
+ expect(res.Code).toBe("TaskReassigned");
147
+ },
148
+ EXEC_TIMEOUT,
149
+ );
150
+
151
+ it(
152
+ "S5: unassign removes the assignee",
153
+ () => {
154
+ if (!taskId || userIds.length === 0) return;
155
+ const raw = localCliRaw(
156
+ tempDir,
157
+ `tasks unassign ${taskId} --output json`,
158
+ );
159
+ expect(raw.exitCode).toBe(0);
160
+ const res = parseOutput(raw.stdout, "json") as AssignEnvelope;
161
+ expect(res.Result).toBe(RESULTS.Success);
162
+ expect(res.Code).toBe("TaskUnassigned");
163
+ },
164
+ EXEC_TIMEOUT,
165
+ );
166
+ });
@@ -0,0 +1,113 @@
1
+ import { RESULTS } from "@uipath/common";
2
+ import { beforeAll, describe, expect, it } from "vitest";
3
+ import {
4
+ type E2EManifest,
5
+ MANIFEST_PATH,
6
+ } from "../../../tests/e2e-shared/global-setup";
7
+ import {
8
+ EXEC_TIMEOUT,
9
+ localCliRaw,
10
+ parseOutput,
11
+ readManifest,
12
+ } from "../../../tests/e2e-shared/helpers";
13
+
14
+ // Scenario: scenarios/tasks-tool/assignable-users.md
15
+ // `uip tasks users <folder-id>` — discover assignable users in a folder.
16
+
17
+ interface UsersEnvelope {
18
+ Result: string;
19
+ Code?: string;
20
+ Data?: unknown;
21
+ Message?: string;
22
+ Instructions?: string;
23
+ ErrorCode?: string;
24
+ }
25
+
26
+ // `tasks list` passes through the raw SDK task payload, which is
27
+ // PascalCase (Id/FolderId/...) — confirmed against a live tenant.
28
+ interface TaskSummary {
29
+ Id?: number;
30
+ FolderId?: number;
31
+ }
32
+
33
+ const { tempDir } = readManifest<E2EManifest>(MANIFEST_PATH).sharedInstall;
34
+
35
+ describe("tasks users e2e", () => {
36
+ let folderId: string | undefined;
37
+
38
+ beforeAll(() => {
39
+ const listRaw = localCliRaw(
40
+ tempDir,
41
+ "tasks list --limit 1 --output json",
42
+ );
43
+ if (listRaw.exitCode !== 0) return;
44
+ const listRes = parseOutput(listRaw.stdout, "json") as {
45
+ Data?: TaskSummary[];
46
+ };
47
+ const fid = listRes.Data?.[0]?.FolderId;
48
+ folderId = fid !== undefined ? String(fid) : undefined;
49
+ }, EXEC_TIMEOUT);
50
+
51
+ it(
52
+ "S1: missing folder-id argument fails fast",
53
+ () => {
54
+ const raw = localCliRaw(tempDir, "tasks users --output json");
55
+ expect(raw.exitCode).not.toBe(0);
56
+ const res = parseOutput(raw.stdout, "json") as UsersEnvelope;
57
+ expect(res.Result).toBe("ValidationError");
58
+ expect(res.ErrorCode).toBe("invalid_argument");
59
+ expect(res.Message).toMatch(/folder-id/);
60
+ },
61
+ EXEC_TIMEOUT,
62
+ );
63
+
64
+ it(
65
+ "S2: lists users with task permissions in a folder",
66
+ () => {
67
+ if (!folderId) return; // no tenant task/folder to discover from
68
+ const raw = localCliRaw(
69
+ tempDir,
70
+ `tasks users ${folderId} --output json`,
71
+ );
72
+ expect(raw.exitCode).toBe(0);
73
+ const res = parseOutput(raw.stdout, "json") as UsersEnvelope;
74
+ expect(res.Result).toBe(RESULTS.Success);
75
+ expect(res.Code).toBe("TaskUserList");
76
+ expect(Array.isArray(res.Data)).toBe(true);
77
+ },
78
+ EXEC_TIMEOUT,
79
+ );
80
+
81
+ it(
82
+ "S3: limits the number of returned users",
83
+ () => {
84
+ if (!folderId) return;
85
+ const raw = localCliRaw(
86
+ tempDir,
87
+ `tasks users ${folderId} --limit 1 --output json`,
88
+ );
89
+ expect(raw.exitCode).toBe(0);
90
+ const res = parseOutput(raw.stdout, "json") as UsersEnvelope;
91
+ expect(res.Result).toBe(RESULTS.Success);
92
+ expect(res.Code).toBe("TaskUserList");
93
+ expect((res.Data as unknown[]).length).toBeLessThanOrEqual(1);
94
+ },
95
+ EXEC_TIMEOUT,
96
+ );
97
+
98
+ it(
99
+ "S4: non-numeric folder-id is rejected",
100
+ () => {
101
+ const raw = localCliRaw(
102
+ tempDir,
103
+ "tasks users not-a-number --output json",
104
+ );
105
+ expect(raw.exitCode).not.toBe(0);
106
+ const res = parseOutput(raw.stdout, "json") as UsersEnvelope;
107
+ expect(res.Result).toBe(RESULTS.Failure);
108
+ expect(res.Message).toMatch(/Invalid <folder-id>/);
109
+ expect(res.Instructions).toBe("Must be a positive integer.");
110
+ },
111
+ EXEC_TIMEOUT,
112
+ );
113
+ });
@@ -0,0 +1,207 @@
1
+ import { RESULTS } from "@uipath/common";
2
+ import { beforeAll, describe, expect, it } from "vitest";
3
+ import {
4
+ type E2EManifest,
5
+ MANIFEST_PATH,
6
+ } from "../../../tests/e2e-shared/global-setup";
7
+ import {
8
+ EXEC_TIMEOUT,
9
+ localCliRaw,
10
+ parseOutput,
11
+ readManifest,
12
+ } from "../../../tests/e2e-shared/helpers";
13
+
14
+ // Scenario: scenarios/tasks-tool/complete.md
15
+ // `uip tasks complete <task-id>` — completion with action and data.
16
+
17
+ interface CompleteEnvelope {
18
+ Result: string;
19
+ Code?: string;
20
+ Data?: unknown;
21
+ Message?: string;
22
+ Instructions?: string;
23
+ ErrorCode?: string;
24
+ }
25
+
26
+ // `tasks list` passes through the raw SDK task payload, which is
27
+ // PascalCase (Id/FolderId/Type/Status/...) — confirmed against a live tenant.
28
+ interface TaskSummary {
29
+ Id?: number;
30
+ FolderId?: number;
31
+ Type?: string;
32
+ Status?: string;
33
+ }
34
+
35
+ const { tempDir } = readManifest<E2EManifest>(MANIFEST_PATH).sharedInstall;
36
+
37
+ describe("tasks complete e2e", () => {
38
+ let formTaskId: string | undefined;
39
+ let formTaskFolderId: string | undefined;
40
+ let quickFormTaskId: string | undefined;
41
+ let quickFormTaskFolderId: string | undefined;
42
+
43
+ beforeAll(() => {
44
+ const listRaw = localCliRaw(tempDir, "tasks list --output json");
45
+ if (listRaw.exitCode !== 0) return;
46
+ const listRes = parseOutput(listRaw.stdout, "json") as {
47
+ Data?: TaskSummary[];
48
+ };
49
+ const tasks = listRes.Data ?? [];
50
+
51
+ const formTask = tasks.find(
52
+ (t) => t.Type === "FormTask" && t.Status !== "Completed",
53
+ );
54
+ if (formTask?.Id !== undefined) {
55
+ formTaskId = String(formTask.Id);
56
+ formTaskFolderId =
57
+ formTask.FolderId !== undefined
58
+ ? String(formTask.FolderId)
59
+ : undefined;
60
+ }
61
+
62
+ const quickFormTask = tasks.find(
63
+ (t) => t.Type === "QuickFormTask" && t.Status === "Pending",
64
+ );
65
+ if (quickFormTask?.Id !== undefined) {
66
+ quickFormTaskId = String(quickFormTask.Id);
67
+ quickFormTaskFolderId =
68
+ quickFormTask.FolderId !== undefined
69
+ ? String(quickFormTask.FolderId)
70
+ : undefined;
71
+ }
72
+ }, EXEC_TIMEOUT);
73
+
74
+ // ── Deterministic (no tenant) ────────────────────────────────────────────
75
+
76
+ it(
77
+ "S1: missing --type fails fast",
78
+ () => {
79
+ const raw = localCliRaw(
80
+ tempDir,
81
+ "tasks complete 5 --folder-id 1 --output json",
82
+ );
83
+ expect(raw.exitCode).toBe(3);
84
+ const res = parseOutput(raw.stdout, "json") as CompleteEnvelope;
85
+ expect(res.Result).toBe("ValidationError");
86
+ expect(res.ErrorCode).toBe("invalid_argument");
87
+ expect(res.Message).toBe(
88
+ "error: required option '--type <type>' not specified",
89
+ );
90
+ },
91
+ EXEC_TIMEOUT,
92
+ );
93
+
94
+ it(
95
+ "S2: missing --folder-id fails fast",
96
+ () => {
97
+ const raw = localCliRaw(
98
+ tempDir,
99
+ "tasks complete 5 --type FormTask --output json",
100
+ );
101
+ expect(raw.exitCode).toBe(3);
102
+ const res = parseOutput(raw.stdout, "json") as CompleteEnvelope;
103
+ expect(res.Result).toBe("ValidationError");
104
+ expect(res.ErrorCode).toBe("invalid_argument");
105
+ expect(res.Message).toBe(
106
+ "error: required option '--folder-id <id>' not specified",
107
+ );
108
+ },
109
+ EXEC_TIMEOUT,
110
+ );
111
+
112
+ // ── Tenant read (requires login) ─────────────────────────────────────────
113
+
114
+ it(
115
+ "S3: invalid task type value is rejected",
116
+ () => {
117
+ const raw = localCliRaw(
118
+ tempDir,
119
+ "tasks complete 5 --type NotARealType --folder-id 1 --output json",
120
+ );
121
+ expect(raw.exitCode).not.toBe(0);
122
+ const res = parseOutput(raw.stdout, "json") as CompleteEnvelope;
123
+ expect(res.Result).toBe(RESULTS.Failure);
124
+ expect(res.Message).toBe("Invalid task type 'NotARealType'");
125
+ expect(res.Instructions).toMatch(/FormTask/);
126
+ },
127
+ EXEC_TIMEOUT,
128
+ );
129
+
130
+ it(
131
+ "S4: invalid JSON in --data is rejected",
132
+ () => {
133
+ const raw = localCliRaw(
134
+ tempDir,
135
+ `tasks complete 5 --type FormTask --folder-id 1 --data "{not json" --output json`,
136
+ );
137
+ expect(raw.exitCode).not.toBe(0);
138
+ const res = parseOutput(raw.stdout, "json") as CompleteEnvelope;
139
+ expect(res.Result).toBe(RESULTS.Failure);
140
+ expect(res.Message).toBe("Invalid JSON in --data");
141
+ },
142
+ EXEC_TIMEOUT,
143
+ );
144
+
145
+ // ── Tenant read/write (guarded — skip if no completable FormTask exists) ──
146
+ //
147
+ // The action/button name a FormTask actually accepts comes from its form
148
+ // layout definition, which `uip tasks` has no way to discover — `--action`
149
+ // is validated purely server-side against buttons this CLI can't inspect.
150
+ // Confirmed live: a task with a real FormLayoutId still rejected both
151
+ // "Approve" and "Submit" with "Provided form action does not match any
152
+ // button". So this scenario can only assert that a *wrong* action fails
153
+ // cleanly (already covered structurally) plus, opportunistically, that a
154
+ // guessed common action name either completes the task or fails with
155
+ // that specific button-mismatch message — never a connection/auth error.
156
+ it(
157
+ "S5: completes a task with an action and data",
158
+ () => {
159
+ if (!formTaskId || !formTaskFolderId) return;
160
+ const raw = localCliRaw(
161
+ tempDir,
162
+ `tasks complete ${formTaskId} --type FormTask --folder-id ${formTaskFolderId} --action Submit --data "{}" --output json`,
163
+ );
164
+ const res = parseOutput(raw.stdout, "json") as CompleteEnvelope;
165
+ if (res.Result === RESULTS.Success) {
166
+ expect(raw.exitCode).toBe(0);
167
+ expect(res.Code).toBe("TaskCompleted");
168
+ return;
169
+ }
170
+ // No way to know this fixture's real button name — accept the
171
+ // known button-mismatch failure rather than asserting success.
172
+ expect(res.Result).toBe(RESULTS.Failure);
173
+ expect(res.Instructions).toBe(
174
+ "Provided form action does not match any button",
175
+ );
176
+ },
177
+ EXEC_TIMEOUT,
178
+ );
179
+
180
+ // `QuickFormTask` (Maestro HITL QuickForm nodes) was previously missing
181
+ // from VALID_TASK_TYPES — completion was impossible for this type before
182
+ // this PR. "Submit" is the default single-outcome QuickForm action name;
183
+ // a QuickForm with custom outcomes would need a different name this CLI
184
+ // has no way to discover, so this scenario applies the same defensive
185
+ // accept-known-button-mismatch pattern as S5.
186
+ it(
187
+ "S6: completes a QuickFormTask",
188
+ () => {
189
+ if (!quickFormTaskId || !quickFormTaskFolderId) return;
190
+ const raw = localCliRaw(
191
+ tempDir,
192
+ `tasks complete ${quickFormTaskId} --type QuickFormTask --folder-id ${quickFormTaskFolderId} --action Submit --data "{}" --output json`,
193
+ );
194
+ const res = parseOutput(raw.stdout, "json") as CompleteEnvelope;
195
+ if (res.Result === RESULTS.Success) {
196
+ expect(raw.exitCode).toBe(0);
197
+ expect(res.Code).toBe("TaskCompleted");
198
+ return;
199
+ }
200
+ expect(res.Result).toBe(RESULTS.Failure);
201
+ expect(res.Instructions).toBe(
202
+ "Provided form action does not match any button",
203
+ );
204
+ },
205
+ EXEC_TIMEOUT,
206
+ );
207
+ });
@@ -0,0 +1,101 @@
1
+ import { RESULTS } from "@uipath/common";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ type E2EManifest,
5
+ MANIFEST_PATH,
6
+ } from "../../../tests/e2e-shared/global-setup";
7
+ import {
8
+ EXEC_TIMEOUT,
9
+ localCliRaw,
10
+ parseOutput,
11
+ readManifest,
12
+ } from "../../../tests/e2e-shared/helpers";
13
+
14
+ // Scenario: scenarios/tasks-tool/get.md
15
+ // `uip tasks get <id>` — single task detail retrieval.
16
+
17
+ interface GetEnvelope {
18
+ Result: string;
19
+ Code?: string;
20
+ Data?: unknown;
21
+ Message?: string;
22
+ Instructions?: string;
23
+ ErrorCode?: string;
24
+ }
25
+
26
+ const { tempDir } = readManifest<E2EManifest>(MANIFEST_PATH).sharedInstall;
27
+
28
+ /** Discover a real task ID via `tasks list`, or undefined if the tenant has none. */
29
+ function discoverTaskId(): string | undefined {
30
+ const raw = localCliRaw(tempDir, "tasks list --limit 1 --output json");
31
+ if (raw.exitCode !== 0) return undefined;
32
+ const res = parseOutput(raw.stdout, "json") as GetEnvelope;
33
+ // `tasks list` passes through the raw SDK task payload, which is
34
+ // PascalCase (Id/...) — confirmed against a live tenant.
35
+ const items = res.Data as Array<{ Id?: number }> | undefined;
36
+ const id = items?.[0]?.Id;
37
+ return typeof id === "number" ? String(id) : undefined;
38
+ }
39
+
40
+ describe("tasks get e2e", () => {
41
+ it(
42
+ "S1: missing id argument fails fast",
43
+ () => {
44
+ const raw = localCliRaw(tempDir, "tasks get --output json");
45
+ expect(raw.exitCode).toBe(3);
46
+ const res = parseOutput(raw.stdout, "json") as GetEnvelope;
47
+ expect(res.Result).toBe("ValidationError");
48
+ expect(res.ErrorCode).toBe("invalid_argument");
49
+ expect(res.Message).toBe("error: missing required argument 'id'");
50
+ },
51
+ EXEC_TIMEOUT,
52
+ );
53
+
54
+ it(
55
+ "S2: gets a task's details when one exists",
56
+ () => {
57
+ const id = discoverTaskId();
58
+ if (!id) return; // tenant has no tasks to fetch
59
+ const raw = localCliRaw(tempDir, `tasks get ${id} --output json`);
60
+ expect(raw.exitCode).toBe(0);
61
+ const res = parseOutput(raw.stdout, "json") as GetEnvelope;
62
+ expect(res.Result).toBe(RESULTS.Success);
63
+ expect(res.Code).toBe("TaskDetails");
64
+ expect(res.Data).not.toBeNull();
65
+ },
66
+ EXEC_TIMEOUT,
67
+ );
68
+
69
+ it(
70
+ "S3: unknown task id reports a clean not-found failure",
71
+ () => {
72
+ const raw = localCliRaw(
73
+ tempDir,
74
+ "tasks get 999999999 --output json",
75
+ );
76
+ expect(raw.exitCode).not.toBe(0);
77
+ const res = parseOutput(raw.stdout, "json") as GetEnvelope;
78
+ expect(res.Result).toBe(RESULTS.Failure);
79
+ expect(res.Message).toBe("Error getting task '999999999'");
80
+ },
81
+ EXEC_TIMEOUT,
82
+ );
83
+
84
+ it(
85
+ "S4: --task-type without --folder-id is rejected",
86
+ () => {
87
+ const raw = localCliRaw(
88
+ tempDir,
89
+ "tasks get 1 --task-type FormTask --output json",
90
+ );
91
+ expect(raw.exitCode).not.toBe(0);
92
+ const res = parseOutput(raw.stdout, "json") as GetEnvelope;
93
+ expect(res.Result).toBe(RESULTS.Failure);
94
+ expect(res.Message).toBe(
95
+ "--folder-id is required when --task-type is specified",
96
+ );
97
+ expect(res.Instructions).toMatch(/--folder-id/);
98
+ },
99
+ EXEC_TIMEOUT,
100
+ );
101
+ });