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

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.
Files changed (40) hide show
  1. package/browser.json +2 -1
  2. package/dist/folder-picker-interactive-2kj361mt.js +1850 -0
  3. package/dist/folder-resolver-htdmzdhy.js +92 -0
  4. package/dist/index.js +5 -3
  5. package/dist/tool-0n9mj887.js +2448 -0
  6. package/dist/tool-rsk1xt86.js +6026 -0
  7. package/dist/{tool-vw6gha5d.js → tool-ww671age.js} +7469 -10469
  8. package/dist/tool.js +3 -2
  9. package/package.json +2 -2
  10. package/src/commands/_shared/catalog-options.spec.ts +114 -0
  11. package/src/commands/_shared/catalog-options.ts +122 -0
  12. package/src/commands/_shared/connect.spec.ts +52 -0
  13. package/src/commands/_shared/connect.ts +31 -0
  14. package/src/commands/_shared/constants.ts +1 -0
  15. package/src/commands/_shared/folder-picker-interactive.spec.ts +92 -0
  16. package/src/commands/_shared/folder-picker-interactive.ts +76 -0
  17. package/src/commands/_shared/folder-picker.spec.ts +159 -0
  18. package/src/commands/_shared/folder-picker.ts +113 -0
  19. package/src/commands/_shared/folder-resolver.spec.ts +152 -0
  20. package/src/commands/_shared/folder-resolver.ts +113 -0
  21. package/src/commands/_shared/parse.spec.ts +69 -0
  22. package/src/commands/_shared/parse.ts +43 -0
  23. package/src/commands/catalogs.ts +277 -0
  24. package/src/commands/comments.ts +160 -0
  25. package/src/commands/data.ts +137 -0
  26. package/src/commands/labels.ts +93 -0
  27. package/src/commands/metadata.ts +139 -0
  28. package/src/commands/tasks.spec.ts +1557 -2
  29. package/src/commands/tasks.ts +191 -280
  30. package/src/tool.ts +10 -0
  31. package/src/utils/sdk-client.ts +5 -0
  32. package/tests/assignable-users.e2e.test.ts +58 -4
  33. package/tests/catalogs.e2e.test.ts +393 -0
  34. package/tests/comments.e2e.test.ts +223 -0
  35. package/tests/complete.e2e.test.ts +9 -9
  36. package/tests/data.e2e.test.ts +187 -0
  37. package/tests/get.e2e.test.ts +4 -2
  38. package/tests/labels.e2e.test.ts +184 -0
  39. package/tests/list.e2e.test.ts +110 -0
  40. package/tests/metadata.e2e.test.ts +186 -0
package/dist/tool.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  metadata,
3
3
  registerCommands
4
- } from "./tool-vw6gha5d.js";
4
+ } from "./tool-ww671age.js";
5
+ import"./tool-rsk1xt86.js";
5
6
  import"./tool-9qecd4wb.js";
6
7
  import"./tool-7eva0peq.js";
7
8
  import"./tool-5arsyj36.js";
@@ -12,4 +13,4 @@ export {
12
13
  metadata
13
14
  };
14
15
 
15
- //# debugId=41CB38F55D92291A64756E2164756E21
16
+ //# debugId=106E34AF1A233DCE64756E2164756E21
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uipath/tasks-tool",
3
3
  "license": "MIT",
4
- "version": "1.201.0-preview.115",
4
+ "version": "1.201.0-preview.121",
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": "f1086b73654d7728cb71f280588b3e0c77d535fc"
17
+ "gitHead": "c70ccfc0b12e637441d67df1d212b71d7784b5f8"
18
18
  }
@@ -0,0 +1,114 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@uipath/common", async (importOriginal) => {
4
+ const actual = await importOriginal<typeof import("@uipath/common")>();
5
+ return {
6
+ ...actual,
7
+ OutputFormatter: { success: vi.fn(), error: vi.fn() },
8
+ };
9
+ });
10
+
11
+ import { OutputFormatter } from "@uipath/common";
12
+ import {
13
+ buildCatalogCreateOptions,
14
+ buildCatalogUpdateOptions,
15
+ } from "./catalog-options";
16
+
17
+ describe("buildCatalogCreateOptions", () => {
18
+ beforeEach(() => {
19
+ vi.resetAllMocks();
20
+ process.exitCode = undefined;
21
+ });
22
+
23
+ it("builds folderId only", () => {
24
+ expect(
25
+ buildCatalogCreateOptions({ folderId: "42", name: "x" }, 42),
26
+ ).toEqual({ folderId: 42 });
27
+ });
28
+
29
+ it("includes description, encryption, and retention", () => {
30
+ const result = buildCatalogCreateOptions(
31
+ {
32
+ folderId: "42",
33
+ name: "x",
34
+ description: "desc",
35
+ encrypted: true,
36
+ retentionAction: "Archive",
37
+ retentionPeriod: "30",
38
+ retentionBucketId: "7",
39
+ },
40
+ 42,
41
+ );
42
+ expect(result).toEqual({
43
+ folderId: 42,
44
+ description: "desc",
45
+ encrypted: true,
46
+ retentionAction: "Archive",
47
+ retentionPeriod: 30,
48
+ retentionBucketId: 7,
49
+ });
50
+ });
51
+
52
+ it("rejects an invalid retention action", () => {
53
+ const result = buildCatalogCreateOptions(
54
+ { folderId: "42", name: "x", retentionAction: "Purge" },
55
+ 42,
56
+ );
57
+ expect(result).toBeNull();
58
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
59
+ expect.objectContaining({
60
+ Message: "Invalid --retention-action 'Purge'",
61
+ }),
62
+ );
63
+ });
64
+
65
+ it.each([
66
+ ["retentionPeriod", "--retention-period"],
67
+ ["retentionBucketId", "--retention-bucket-id"],
68
+ ])("rejects an invalid %s", (field, flag) => {
69
+ const result = buildCatalogCreateOptions(
70
+ { folderId: "42", name: "x", [field]: "abc" },
71
+ 42,
72
+ );
73
+ expect(result).toBeNull();
74
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
75
+ expect.objectContaining({ Message: `Invalid ${flag}: abc` }),
76
+ );
77
+ });
78
+ });
79
+
80
+ describe("buildCatalogUpdateOptions", () => {
81
+ beforeEach(() => {
82
+ vi.resetAllMocks();
83
+ process.exitCode = undefined;
84
+ });
85
+
86
+ it("includes name when provided", () => {
87
+ expect(
88
+ buildCatalogUpdateOptions({ folderId: "42", name: "renamed" }, 42),
89
+ ).toEqual({ folderId: 42, name: "renamed" });
90
+ });
91
+
92
+ it("omits name when not provided and never sets encrypted", () => {
93
+ const result = buildCatalogUpdateOptions(
94
+ { folderId: "42", description: "d" },
95
+ 42,
96
+ );
97
+ expect(result).toEqual({ folderId: 42, description: "d" });
98
+ expect(result).not.toHaveProperty("name");
99
+ expect(result).not.toHaveProperty("encrypted");
100
+ });
101
+
102
+ it("rejects an invalid retention action", () => {
103
+ const result = buildCatalogUpdateOptions(
104
+ { folderId: "42", retentionAction: "Nope" },
105
+ 42,
106
+ );
107
+ expect(result).toBeNull();
108
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
109
+ expect.objectContaining({
110
+ Message: "Invalid --retention-action 'Nope'",
111
+ }),
112
+ );
113
+ });
114
+ });
@@ -0,0 +1,122 @@
1
+ import { OutputFormatter, processContext, RESULTS } from "@uipath/common";
2
+ import type {
3
+ TaskCatalogCreateOptions,
4
+ TaskCatalogUpdateOptions,
5
+ } from "@uipath/uipath-typescript";
6
+ import { TaskCatalogRetentionAction } from "@uipath/uipath-typescript";
7
+
8
+ import type { FolderScopeOptions } from "./folder-picker";
9
+ import { parsePositiveInt, reportInvalidInt } from "./parse";
10
+
11
+ // None is a read-only state, not a settable action, so the CLI only offers Delete and Archive.
12
+ const RETENTION_ACTIONS: TaskCatalogRetentionAction[] = [
13
+ TaskCatalogRetentionAction.Delete,
14
+ TaskCatalogRetentionAction.Archive,
15
+ ];
16
+
17
+ export interface CatalogListOptions extends FolderScopeOptions {
18
+ limit?: string;
19
+ }
20
+
21
+ export type CatalogGetOptions = FolderScopeOptions;
22
+
23
+ export interface CatalogCreateOptions extends FolderScopeOptions {
24
+ name: string;
25
+ description?: string;
26
+ encrypted?: boolean;
27
+ retentionAction?: string;
28
+ retentionPeriod?: string;
29
+ retentionBucketId?: string;
30
+ }
31
+
32
+ export interface CatalogUpdateOptions extends FolderScopeOptions {
33
+ name?: string;
34
+ description?: string;
35
+ retentionAction?: string;
36
+ retentionPeriod?: string;
37
+ retentionBucketId?: string;
38
+ }
39
+
40
+ // Common retention/description fields shared by create and update. Returns
41
+ // false after emitting an error + exit when a value is invalid.
42
+ function applyCommonFields(
43
+ target: {
44
+ description?: string;
45
+ retentionAction?: TaskCatalogRetentionAction;
46
+ retentionPeriod?: number;
47
+ retentionBucketId?: number;
48
+ },
49
+ options: {
50
+ description?: string;
51
+ retentionAction?: string;
52
+ retentionPeriod?: string;
53
+ retentionBucketId?: string;
54
+ },
55
+ ): boolean {
56
+ if (options.description !== undefined) {
57
+ target.description = options.description;
58
+ }
59
+ if (options.retentionAction !== undefined) {
60
+ if (
61
+ !RETENTION_ACTIONS.includes(
62
+ options.retentionAction as TaskCatalogRetentionAction,
63
+ )
64
+ ) {
65
+ OutputFormatter.error({
66
+ Result: RESULTS.Failure,
67
+ Message: `Invalid --retention-action '${options.retentionAction}'`,
68
+ Instructions: `Valid values: ${RETENTION_ACTIONS.join(", ")}`,
69
+ });
70
+ processContext.exit(1);
71
+ return false;
72
+ }
73
+ target.retentionAction =
74
+ options.retentionAction as TaskCatalogRetentionAction;
75
+ }
76
+ if (options.retentionPeriod !== undefined) {
77
+ const parsed = parsePositiveInt(
78
+ options.retentionPeriod,
79
+ "--retention-period",
80
+ );
81
+ if (!parsed.valid) {
82
+ reportInvalidInt(parsed);
83
+ return false;
84
+ }
85
+ target.retentionPeriod = parsed.value;
86
+ }
87
+ if (options.retentionBucketId !== undefined) {
88
+ const parsed = parsePositiveInt(
89
+ options.retentionBucketId,
90
+ "--retention-bucket-id",
91
+ );
92
+ if (!parsed.valid) {
93
+ reportInvalidInt(parsed);
94
+ return false;
95
+ }
96
+ target.retentionBucketId = parsed.value;
97
+ }
98
+ return true;
99
+ }
100
+
101
+ export function buildCatalogCreateOptions(
102
+ options: CatalogCreateOptions,
103
+ folderId: number,
104
+ ): TaskCatalogCreateOptions | null {
105
+ const result: TaskCatalogCreateOptions = { folderId };
106
+ if (options.encrypted) {
107
+ result.encrypted = true;
108
+ }
109
+ return applyCommonFields(result, options) ? result : null;
110
+ }
111
+
112
+ export function buildCatalogUpdateOptions(
113
+ options: CatalogUpdateOptions,
114
+ folderId: number,
115
+ ): TaskCatalogUpdateOptions | null {
116
+ // `encrypted` is immutable on update, so it is intentionally not set here.
117
+ const result: TaskCatalogUpdateOptions = { folderId };
118
+ if (options.name !== undefined) {
119
+ result.name = options.name;
120
+ }
121
+ return applyCommonFields(result, options) ? result : null;
122
+ }
@@ -0,0 +1,52 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("../../utils/sdk-client", () => ({
4
+ createTasksClient: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("@uipath/common", async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import("@uipath/common")>();
9
+ return {
10
+ ...actual,
11
+ OutputFormatter: { success: vi.fn(), error: vi.fn() },
12
+ };
13
+ });
14
+
15
+ import { OutputFormatter } from "@uipath/common";
16
+ import { createTasksClient } from "../../utils/sdk-client";
17
+ import { connectTasksClient } from "./connect";
18
+
19
+ describe("connectTasksClient", () => {
20
+ beforeEach(() => {
21
+ vi.resetAllMocks();
22
+ process.exitCode = undefined;
23
+ });
24
+
25
+ it("returns the client on success", async () => {
26
+ const sdk = { tasks: {} };
27
+ vi.mocked(createTasksClient).mockResolvedValue(sdk as never);
28
+
29
+ const result = await connectTasksClient("MyTenant");
30
+
31
+ expect(result).toBe(sdk);
32
+ expect(createTasksClient).toHaveBeenCalledWith("MyTenant");
33
+ expect(OutputFormatter.error).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it("reports, exits, and returns null on failure", async () => {
37
+ vi.mocked(createTasksClient).mockRejectedValue(
38
+ new Error("Not logged in"),
39
+ );
40
+
41
+ const result = await connectTasksClient();
42
+
43
+ expect(result).toBeNull();
44
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
45
+ expect.objectContaining({
46
+ Result: "Failure",
47
+ Message: "Error connecting to Action Center",
48
+ }),
49
+ );
50
+ expect(process.exitCode).toBe(1);
51
+ });
52
+ });
@@ -0,0 +1,31 @@
1
+ import {
2
+ catchError,
3
+ extractErrorMessage,
4
+ OutputFormatter,
5
+ processContext,
6
+ RESULTS,
7
+ } from "@uipath/common";
8
+ import type { UiPath } from "@uipath/uipath-typescript";
9
+
10
+ import { createTasksClient } from "../../utils/sdk-client";
11
+
12
+ /**
13
+ * Connects to Action Center for the given tenant. On failure, emits the
14
+ * standard error, exits with code 1, and returns null so the caller can
15
+ * `return` early. Keeps the connect boilerplate in one place.
16
+ */
17
+ export async function connectTasksClient(
18
+ tenant?: string,
19
+ ): Promise<UiPath | null> {
20
+ const [clientError, sdk] = await catchError(createTasksClient(tenant));
21
+ if (clientError) {
22
+ OutputFormatter.error({
23
+ Result: RESULTS.Failure,
24
+ Message: "Error connecting to Action Center",
25
+ Instructions: await extractErrorMessage(clientError),
26
+ });
27
+ processContext.exit(1);
28
+ return null;
29
+ }
30
+ return sdk;
31
+ }
@@ -0,0 +1 @@
1
+ export const DEFAULT_PAGE_SIZE = 100;
@@ -0,0 +1,92 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@inquirer/prompts", () => ({ select: vi.fn() }));
4
+
5
+ vi.mock("@uipath/orchestrator-sdk", () => ({
6
+ createApiClient: vi.fn(),
7
+ FoldersApi: class {},
8
+ }));
9
+
10
+ vi.mock("@uipath/common", async (importOriginal) => {
11
+ const actual = await importOriginal<typeof import("@uipath/common")>();
12
+ return {
13
+ ...actual,
14
+ OutputFormatter: { success: vi.fn(), error: vi.fn() },
15
+ };
16
+ });
17
+
18
+ import { select } from "@inquirer/prompts";
19
+ import { OutputFormatter } from "@uipath/common";
20
+ import { createApiClient } from "@uipath/orchestrator-sdk";
21
+ import { pickFolderInteractively } from "./folder-picker-interactive";
22
+
23
+ function mockFolders(pageItems: unknown[]) {
24
+ vi.mocked(createApiClient).mockResolvedValue({
25
+ foldersGetAllForCurrentUser: vi.fn().mockResolvedValue({ pageItems }),
26
+ } as never);
27
+ }
28
+
29
+ describe("pickFolderInteractively", () => {
30
+ beforeEach(() => {
31
+ vi.resetAllMocks();
32
+ process.exitCode = undefined;
33
+ });
34
+
35
+ it("lists folders and returns the selected id", async () => {
36
+ mockFolders([
37
+ { id: 7, displayName: "Finance" },
38
+ { id: 9, displayName: "HR" },
39
+ ]);
40
+ vi.mocked(select).mockResolvedValue(7 as never);
41
+
42
+ expect(await pickFolderInteractively()).toBe(7);
43
+ expect(select).toHaveBeenCalled();
44
+ });
45
+
46
+ it("passes the tenant through to the folder client", async () => {
47
+ mockFolders([{ id: 7, displayName: "Finance" }]);
48
+ vi.mocked(select).mockResolvedValue(7 as never);
49
+
50
+ expect(await pickFolderInteractively("my-tenant")).toBe(7);
51
+ expect(createApiClient).toHaveBeenCalledWith(expect.anything(), {
52
+ tenant: "my-tenant",
53
+ });
54
+ });
55
+
56
+ it("reports a connection error", async () => {
57
+ vi.mocked(createApiClient).mockRejectedValue(new Error("boom"));
58
+
59
+ expect(await pickFolderInteractively()).toBeNull();
60
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
61
+ expect.objectContaining({
62
+ Message: "Error connecting to Action Center",
63
+ }),
64
+ );
65
+ expect(process.exitCode).toBe(1);
66
+ });
67
+
68
+ it("reports a listing error", async () => {
69
+ vi.mocked(createApiClient).mockResolvedValue({
70
+ foldersGetAllForCurrentUser: vi
71
+ .fn()
72
+ .mockRejectedValue(new Error("nope")),
73
+ } as never);
74
+
75
+ expect(await pickFolderInteractively()).toBeNull();
76
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
77
+ expect.objectContaining({ Message: "Error listing folders" }),
78
+ );
79
+ });
80
+
81
+ it("fails when no selectable folders are returned", async () => {
82
+ mockFolders([{ displayName: "no id here" }]);
83
+
84
+ expect(await pickFolderInteractively()).toBeNull();
85
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
86
+ expect.objectContaining({
87
+ Message: "No folders available to select",
88
+ }),
89
+ );
90
+ expect(select).not.toHaveBeenCalled();
91
+ });
92
+ });
@@ -0,0 +1,76 @@
1
+ import { select } from "@inquirer/prompts";
2
+ import {
3
+ catchError,
4
+ extractErrorMessage,
5
+ OutputFormatter,
6
+ processContext,
7
+ RESULTS,
8
+ } from "@uipath/common";
9
+ import {
10
+ type CurrentUserFolderDto,
11
+ createApiClient,
12
+ FoldersApi,
13
+ } from "@uipath/orchestrator-sdk";
14
+
15
+ const FOLDER_PAGE_SIZE = 200;
16
+
17
+ /**
18
+ * Interactive folder picker. Pulls @inquirer/prompts, which is Node-only
19
+ * (TTY prompts), so it is excluded from the browser bundle
20
+ * (browser.json -> excludedImports) and only imported on a TTY.
21
+ * (@uipath/orchestrator-sdk itself is browser-safe — it ships a browser
22
+ * export and reaches the browser bundle through folder-resolver.)
23
+ *
24
+ * Returns null when listing/selection fails (after emitting an error + exit).
25
+ */
26
+ export async function pickFolderInteractively(
27
+ tenant?: string,
28
+ ): Promise<number | null> {
29
+ const [clientError, api] = await catchError(
30
+ createApiClient(FoldersApi, { tenant }),
31
+ );
32
+ if (clientError) {
33
+ OutputFormatter.error({
34
+ Result: RESULTS.Failure,
35
+ Message: "Error connecting to Action Center",
36
+ Instructions: await extractErrorMessage(clientError),
37
+ });
38
+ processContext.exit(1);
39
+ return null;
40
+ }
41
+
42
+ const [listError, result] = await catchError(
43
+ api.foldersGetAllForCurrentUser({ take: FOLDER_PAGE_SIZE }),
44
+ );
45
+ if (listError) {
46
+ OutputFormatter.error({
47
+ Result: RESULTS.Failure,
48
+ Message: "Error listing folders",
49
+ Instructions: await extractErrorMessage(listError),
50
+ });
51
+ processContext.exit(1);
52
+ return null;
53
+ }
54
+
55
+ const folders = (result.pageItems ?? []).filter(
56
+ (f: CurrentUserFolderDto): f is CurrentUserFolderDto & { id: number } =>
57
+ typeof f.id === "number",
58
+ );
59
+ if (folders.length === 0) {
60
+ OutputFormatter.error({
61
+ Result: RESULTS.Failure,
62
+ Message: "No folders available to select",
63
+ Instructions: "Pass --folder-id <id> instead.",
64
+ });
65
+ processContext.exit(1);
66
+ return null;
67
+ }
68
+
69
+ return select<number>({
70
+ message: "Select a folder",
71
+ choices: folders.map((f) => ({
72
+ name: f.fullyQualifiedName || f.displayName || String(f.id),
73
+ value: f.id,
74
+ })),
75
+ });
76
+ }
@@ -0,0 +1,159 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("./folder-picker-interactive", () => ({
4
+ pickFolderInteractively: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("./folder-resolver", () => ({
8
+ resolveFolderIdByKey: vi.fn(),
9
+ resolveFolderIdByPath: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@uipath/common", async (importOriginal) => {
13
+ const actual = await importOriginal<typeof import("@uipath/common")>();
14
+ return {
15
+ ...actual,
16
+ OutputFormatter: { success: vi.fn(), error: vi.fn() },
17
+ };
18
+ });
19
+
20
+ import { OutputFormatter } from "@uipath/common";
21
+ import { resolveFolderId, resolveFolderScope } from "./folder-picker";
22
+ import { pickFolderInteractively } from "./folder-picker-interactive";
23
+ import { resolveFolderIdByKey, resolveFolderIdByPath } from "./folder-resolver";
24
+
25
+ const FOLDER_KEY = "12345678-1234-1234-1234-123456789abc";
26
+
27
+ describe("resolveFolderScope", () => {
28
+ beforeEach(() => {
29
+ vi.resetAllMocks();
30
+ process.exitCode = undefined;
31
+ });
32
+
33
+ it("parses and returns the id when --folder-id is provided", async () => {
34
+ expect(await resolveFolderScope({ folderId: "42" })).toBe(42);
35
+ expect(OutputFormatter.error).not.toHaveBeenCalled();
36
+ expect(resolveFolderIdByKey).not.toHaveBeenCalled();
37
+ expect(resolveFolderIdByPath).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it("reports an invalid --folder-id", async () => {
41
+ expect(await resolveFolderScope({ folderId: "abc" })).toBeNull();
42
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
43
+ expect.objectContaining({ Message: "Invalid --folder-id: abc" }),
44
+ );
45
+ });
46
+
47
+ it("resolves --folder-key through the folder resolver", async () => {
48
+ vi.mocked(resolveFolderIdByKey).mockResolvedValue(55);
49
+
50
+ expect(
51
+ await resolveFolderScope({ folderKey: FOLDER_KEY, tenant: "t1" }),
52
+ ).toBe(55);
53
+ expect(resolveFolderIdByKey).toHaveBeenCalledWith(FOLDER_KEY, "t1");
54
+ expect(resolveFolderIdByPath).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it("resolves --folder-path through the folder resolver", async () => {
58
+ vi.mocked(resolveFolderIdByPath).mockResolvedValue(77);
59
+
60
+ expect(await resolveFolderScope({ folderPath: "Shared/Finance" })).toBe(
61
+ 77,
62
+ );
63
+ expect(resolveFolderIdByPath).toHaveBeenCalledWith(
64
+ "Shared/Finance",
65
+ undefined,
66
+ );
67
+ expect(resolveFolderIdByKey).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it("passes a resolver failure through as null", async () => {
71
+ vi.mocked(resolveFolderIdByKey).mockResolvedValue(null);
72
+
73
+ expect(await resolveFolderScope({ folderKey: FOLDER_KEY })).toBeNull();
74
+ });
75
+
76
+ it("returns undefined when no folder option is passed", async () => {
77
+ expect(await resolveFolderScope({})).toBeUndefined();
78
+ expect(OutputFormatter.error).not.toHaveBeenCalled();
79
+ });
80
+ });
81
+
82
+ describe("resolveFolderId", () => {
83
+ const originalTTY = process.stdout.isTTY;
84
+
85
+ beforeEach(() => {
86
+ vi.resetAllMocks();
87
+ process.exitCode = undefined;
88
+ });
89
+
90
+ afterEach(() => {
91
+ process.stdout.isTTY = originalTTY;
92
+ });
93
+
94
+ it("parses and returns the id when --folder-id is provided", async () => {
95
+ expect(await resolveFolderId({ folderId: "42" })).toBe(42);
96
+ expect(OutputFormatter.error).not.toHaveBeenCalled();
97
+ expect(pickFolderInteractively).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it("resolves --folder-path without falling back to the picker", async () => {
101
+ vi.mocked(resolveFolderIdByPath).mockResolvedValue(77);
102
+
103
+ expect(await resolveFolderId({ folderPath: "Shared" })).toBe(77);
104
+ expect(pickFolderInteractively).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it("fails loud when omitted on a non-interactive terminal", async () => {
108
+ process.stdout.isTTY = false;
109
+
110
+ expect(await resolveFolderId({})).toBeNull();
111
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
112
+ expect.objectContaining({
113
+ Message: "A folder is required",
114
+ Instructions: expect.stringContaining("--folder-id <id>"),
115
+ }),
116
+ );
117
+ expect(process.exitCode).toBe(1);
118
+ expect(pickFolderInteractively).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("uses the caller's instructions when the command's folder options differ", async () => {
122
+ process.stdout.isTTY = false;
123
+
124
+ expect(
125
+ await resolveFolderId({}, "Pass the folder-id argument."),
126
+ ).toBeNull();
127
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
128
+ expect.objectContaining({
129
+ Message: "A folder is required",
130
+ Instructions: "Pass the folder-id argument.",
131
+ }),
132
+ );
133
+ expect(process.exitCode).toBe(1);
134
+ });
135
+
136
+ it("delegates to the interactive picker when omitted on a TTY", async () => {
137
+ process.stdout.isTTY = true;
138
+ vi.mocked(pickFolderInteractively).mockResolvedValue(7);
139
+
140
+ expect(await resolveFolderId({})).toBe(7);
141
+ expect(pickFolderInteractively).toHaveBeenCalledOnce();
142
+ });
143
+
144
+ it("passes the tenant to the interactive picker", async () => {
145
+ process.stdout.isTTY = true;
146
+ vi.mocked(pickFolderInteractively).mockResolvedValue(7);
147
+
148
+ expect(await resolveFolderId({ tenant: "my-tenant" })).toBe(7);
149
+ expect(pickFolderInteractively).toHaveBeenCalledWith("my-tenant");
150
+ });
151
+
152
+ it("does not fall back to the picker when resolution fails", async () => {
153
+ process.stdout.isTTY = true;
154
+ vi.mocked(resolveFolderIdByKey).mockResolvedValue(null);
155
+
156
+ expect(await resolveFolderId({ folderKey: FOLDER_KEY })).toBeNull();
157
+ expect(pickFolderInteractively).not.toHaveBeenCalled();
158
+ });
159
+ });