@uipath/coder-tool 1.202.0-preview.141 → 1.202.0-preview.143
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/LICENSE.txt +3 -0
- package/dist/THIRD-PARTY-NOTICES.md +323 -0
- package/dist/index.js +1 -1
- package/dist/jmespath-231m3hyn.js +1485 -0
- package/dist/{tool-07f1r0dc.js → tool-7yxhv5hx.js} +7 -4
- package/dist/tool.js +1 -1
- package/package.json +6 -3
- package/dist/index-gpd94m28.js +0 -2058
- package/src/commands/chat.spec.ts +0 -224
- package/src/commands/chat.ts +0 -156
- package/src/index.ts +0 -27
- package/src/providers/uipath.spec.ts +0 -296
- package/src/providers/uipath.ts +0 -195
- package/src/tool.spec.ts +0 -38
- package/src/tool.ts +0 -20
- package/templates/uipath-cli.md +0 -38
- package/tests/coding-agent.e2e.test.ts +0 -17
- package/tests/performance.e2e.test.ts +0 -32
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -1
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
5
|
-
main: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
vi.mock("../providers/uipath", () => ({
|
|
9
|
-
createUiPathProviderExtension: vi.fn(() => ({
|
|
10
|
-
name: "uipath-llm-gateway",
|
|
11
|
-
factory: vi.fn(),
|
|
12
|
-
})),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
let guideExists = true;
|
|
16
|
-
const GUIDE_TEXT = "# UiPath CLI (`uip`) — agent guidance\n(test copy)";
|
|
17
|
-
vi.mock("@uipath/filesystem", () => ({
|
|
18
|
-
getFileSystem: () => ({
|
|
19
|
-
exists: (path: string) =>
|
|
20
|
-
Promise.resolve(guideExists && path.includes("uipath-cli.md")),
|
|
21
|
-
readFile: () => Promise.resolve(GUIDE_TEXT),
|
|
22
|
-
}),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
import { main } from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import { OutputFormatter } from "@uipath/common";
|
|
27
|
-
import { registerChatCommand, withAgentGuide } from "./chat";
|
|
28
|
-
|
|
29
|
-
function buildProgram(context?: { exit: (code: number) => void }): Command {
|
|
30
|
-
const program = new Command();
|
|
31
|
-
program.name("coder").exitOverride();
|
|
32
|
-
registerChatCommand(program, context, vi.fn());
|
|
33
|
-
return program;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function piArgs(): string[] {
|
|
37
|
-
return (vi.mocked(main).mock.calls[0]?.[0] ?? []) as string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("uip coder", () => {
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
vi.clearAllMocks();
|
|
43
|
-
guideExists = true;
|
|
44
|
-
process.exitCode = undefined;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
vi.restoreAllMocks();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("launches pi with the UiPath agent guide appended to the system prompt", async () => {
|
|
52
|
-
const program = buildProgram();
|
|
53
|
-
|
|
54
|
-
await program.parseAsync([], { from: "user" });
|
|
55
|
-
|
|
56
|
-
expect(main).toHaveBeenCalledTimes(1);
|
|
57
|
-
const args = piArgs();
|
|
58
|
-
expect(args[0]).toBe("--append-system-prompt");
|
|
59
|
-
// The guide is passed as text, not a file path, so the injection
|
|
60
|
-
// does not depend on Pi's file-or-literal argument resolution.
|
|
61
|
-
expect(args[1]).toBe(GUIDE_TEXT);
|
|
62
|
-
expect(args).toHaveLength(2);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("forwards positional arguments and unknown options to pi", async () => {
|
|
66
|
-
const program = buildProgram();
|
|
67
|
-
|
|
68
|
-
await program.parseAsync(
|
|
69
|
-
["-p", "--provider", "uipath", "--model", "gpt-4o", "hello"],
|
|
70
|
-
{ from: "user" },
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
expect(piArgs().slice(2)).toEqual([
|
|
74
|
-
"-p",
|
|
75
|
-
"--provider",
|
|
76
|
-
"uipath",
|
|
77
|
-
"--model",
|
|
78
|
-
"gpt-4o",
|
|
79
|
-
"hello",
|
|
80
|
-
]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("launches pi without the guide when the template is missing", async () => {
|
|
84
|
-
guideExists = false;
|
|
85
|
-
const program = buildProgram();
|
|
86
|
-
|
|
87
|
-
await program.parseAsync(["-p", "hi"], { from: "user" });
|
|
88
|
-
|
|
89
|
-
expect(piArgs()).toEqual(["-p", "hi"]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("does not inject the guide before pi package-management subcommands", () => {
|
|
93
|
-
expect(withAgentGuide(["install", "some-ext"], "/guide.md")).toEqual([
|
|
94
|
-
"install",
|
|
95
|
-
"some-ext",
|
|
96
|
-
]);
|
|
97
|
-
expect(withAgentGuide(["-p", "hi"], "/guide.md")).toEqual([
|
|
98
|
-
"--append-system-prompt",
|
|
99
|
-
"/guide.md",
|
|
100
|
-
"-p",
|
|
101
|
-
"hi",
|
|
102
|
-
]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("keeps the guide for prompts whose first word matches a subcommand name", () => {
|
|
106
|
-
// Pi routes package commands on args[0] only, so an unquoted prompt
|
|
107
|
-
// like `-p update the readme` is a prompt, not `pi update`.
|
|
108
|
-
expect(
|
|
109
|
-
withAgentGuide(["-p", "update", "the", "readme"], "/guide.md"),
|
|
110
|
-
).toEqual([
|
|
111
|
-
"--append-system-prompt",
|
|
112
|
-
"/guide.md",
|
|
113
|
-
"-p",
|
|
114
|
-
"update",
|
|
115
|
-
"the",
|
|
116
|
-
"readme",
|
|
117
|
-
]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("passes the uipath provider extension to pi", async () => {
|
|
121
|
-
const program = buildProgram();
|
|
122
|
-
|
|
123
|
-
await program.parseAsync([], { from: "user" });
|
|
124
|
-
|
|
125
|
-
const [, options] = vi.mocked(main).mock.calls[0] ?? [];
|
|
126
|
-
const factories =
|
|
127
|
-
(options as { extensionFactories?: unknown[] })
|
|
128
|
-
?.extensionFactories ?? [];
|
|
129
|
-
expect(factories).toHaveLength(1);
|
|
130
|
-
expect(factories[0]).toMatchObject({ name: "uipath-llm-gateway" });
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("turns pi's non-zero process.exit into a failure envelope", async () => {
|
|
134
|
-
const errorSpy = vi
|
|
135
|
-
.spyOn(OutputFormatter, "error")
|
|
136
|
-
.mockImplementation(() => {});
|
|
137
|
-
vi.mocked(main).mockImplementation(async () => {
|
|
138
|
-
// Pi hard-exits on its own error paths instead of throwing.
|
|
139
|
-
process.exit(2);
|
|
140
|
-
});
|
|
141
|
-
const exit = vi.fn();
|
|
142
|
-
const program = buildProgram({ exit });
|
|
143
|
-
|
|
144
|
-
await program.parseAsync(["-p", "hi"], { from: "user" });
|
|
145
|
-
|
|
146
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
147
|
-
expect.objectContaining({
|
|
148
|
-
Result: "Failure",
|
|
149
|
-
Message: expect.stringContaining("exited with code 2"),
|
|
150
|
-
}),
|
|
151
|
-
);
|
|
152
|
-
expect(exit).toHaveBeenCalledWith(2);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("handles process.exit from a fire-and-forget shutdown without an unhandled rejection", async () => {
|
|
156
|
-
// Pi's interactive quit (Ctrl+C / Ctrl+D) calls process.exit(0) from
|
|
157
|
-
// a floating promise chain, not from main()'s awaited stack.
|
|
158
|
-
const rejections: unknown[] = [];
|
|
159
|
-
const onRejection = (reason: unknown) => rejections.push(reason);
|
|
160
|
-
process.on("unhandledRejection", onRejection);
|
|
161
|
-
vi.mocked(main).mockImplementation(
|
|
162
|
-
() =>
|
|
163
|
-
new Promise<void>(() => {
|
|
164
|
-
// never resolves — like an interactive session
|
|
165
|
-
setTimeout(() => {
|
|
166
|
-
void Promise.resolve().then(() => process.exit(0));
|
|
167
|
-
}, 0);
|
|
168
|
-
}),
|
|
169
|
-
);
|
|
170
|
-
const exit = vi.fn();
|
|
171
|
-
const program = buildProgram({ exit });
|
|
172
|
-
|
|
173
|
-
await program.parseAsync([], { from: "user" });
|
|
174
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
175
|
-
|
|
176
|
-
process.off("unhandledRejection", onRejection);
|
|
177
|
-
expect(rejections).toEqual([]);
|
|
178
|
-
expect(exit).not.toHaveBeenCalled();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("treats pi's process.exit(0) as success", async () => {
|
|
182
|
-
const errorSpy = vi
|
|
183
|
-
.spyOn(OutputFormatter, "error")
|
|
184
|
-
.mockImplementation(() => {});
|
|
185
|
-
vi.mocked(main).mockImplementation(async () => {
|
|
186
|
-
process.exit(0);
|
|
187
|
-
});
|
|
188
|
-
const exit = vi.fn();
|
|
189
|
-
const program = buildProgram({ exit });
|
|
190
|
-
|
|
191
|
-
await program.parseAsync(["-p", "hi"], { from: "user" });
|
|
192
|
-
|
|
193
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
194
|
-
expect(exit).not.toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("restores process.exit after pi finishes", async () => {
|
|
198
|
-
const originalExit = process.exit;
|
|
199
|
-
const program = buildProgram();
|
|
200
|
-
|
|
201
|
-
await program.parseAsync([], { from: "user" });
|
|
202
|
-
|
|
203
|
-
expect(process.exit).toBe(originalExit);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("reports a failure envelope and exits 1 when pi fails to start", async () => {
|
|
207
|
-
const errorSpy = vi
|
|
208
|
-
.spyOn(OutputFormatter, "error")
|
|
209
|
-
.mockImplementation(() => {});
|
|
210
|
-
vi.mocked(main).mockRejectedValueOnce(new Error("pi exploded"));
|
|
211
|
-
const exit = vi.fn();
|
|
212
|
-
const program = buildProgram({ exit });
|
|
213
|
-
|
|
214
|
-
await program.parseAsync([], { from: "user" });
|
|
215
|
-
|
|
216
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
217
|
-
expect.objectContaining({
|
|
218
|
-
Result: "Failure",
|
|
219
|
-
Message: expect.stringContaining("pi exploded"),
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
222
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
223
|
-
});
|
|
224
|
-
});
|
package/src/commands/chat.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import {
|
|
3
|
-
type CommandContext,
|
|
4
|
-
OutputFormatter,
|
|
5
|
-
processContext,
|
|
6
|
-
RESULTS,
|
|
7
|
-
} from "@uipath/common";
|
|
8
|
-
import { getFileSystem } from "@uipath/filesystem";
|
|
9
|
-
import type { Command } from "commander";
|
|
10
|
-
import { createUiPathProviderExtension } from "../providers/uipath";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Pi package-management subcommands. They must stay the first argv token,
|
|
14
|
-
* and they don't start an agent session, so no system prompt is injected.
|
|
15
|
-
*/
|
|
16
|
-
const PI_SUBCOMMANDS = new Set([
|
|
17
|
-
"install",
|
|
18
|
-
"remove",
|
|
19
|
-
"uninstall",
|
|
20
|
-
"update",
|
|
21
|
-
"list",
|
|
22
|
-
"config",
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Load the bundled UiPath CLI guide appended to Pi's system prompt.
|
|
27
|
-
* Lives at dist/templates/ in the published package (copied by
|
|
28
|
-
* tools/build-tool.ts) and at the package root when running from source.
|
|
29
|
-
* Returns the guide TEXT (not the path) so the injection does not depend
|
|
30
|
-
* on Pi's file-or-literal resolution of `--append-system-prompt`.
|
|
31
|
-
*/
|
|
32
|
-
export async function resolveAgentGuide(): Promise<string | undefined> {
|
|
33
|
-
const fs = getFileSystem();
|
|
34
|
-
const candidates = [
|
|
35
|
-
new URL("./templates/uipath-cli.md", import.meta.url),
|
|
36
|
-
new URL("../../templates/uipath-cli.md", import.meta.url),
|
|
37
|
-
];
|
|
38
|
-
for (const url of candidates) {
|
|
39
|
-
const path = fileURLToPath(url);
|
|
40
|
-
if (await fs.exists(path)) {
|
|
41
|
-
const text = await fs.readFile(path, "utf-8");
|
|
42
|
-
return text ?? undefined;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Prepend `--append-system-prompt <guide>` so the agent knows how to drive
|
|
50
|
-
* the UiPath platform through `uip`. Skipped for Pi's package-management
|
|
51
|
-
* subcommands, which Pi routes on the first argv token only — a prompt like
|
|
52
|
-
* `-p update the readme` must still get the guide.
|
|
53
|
-
*/
|
|
54
|
-
export function withAgentGuide(
|
|
55
|
-
args: string[],
|
|
56
|
-
guideText: string | undefined,
|
|
57
|
-
): string[] {
|
|
58
|
-
if (!guideText) {
|
|
59
|
-
return args;
|
|
60
|
-
}
|
|
61
|
-
const first = args[0];
|
|
62
|
-
if (first !== undefined && PI_SUBCOMMANDS.has(first)) {
|
|
63
|
-
return args;
|
|
64
|
-
}
|
|
65
|
-
return ["--append-system-prompt", guideText, ...args];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type HardExit = (code: number) => void;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Run Pi's CLI entry point with `process.exit` intercepted. Pi hard-exits
|
|
72
|
-
* on both success and failure (deliberately, so misbehaving extensions
|
|
73
|
-
* cannot keep one-shot commands alive); the interception turns that into a
|
|
74
|
-
* return value so failures can be reported through the CLI envelope. The
|
|
75
|
-
* hard exit is re-applied after the command finishes its bookkeeping.
|
|
76
|
-
*
|
|
77
|
-
* The interception resolves a promise instead of throwing: Pi's interactive
|
|
78
|
-
* quit paths (Ctrl+C / Ctrl+D / signal handlers) call `process.exit` from
|
|
79
|
-
* fire-and-forget shutdown chains, where a throw would surface as an
|
|
80
|
-
* unhandled rejection instead of a clean exit.
|
|
81
|
-
*/
|
|
82
|
-
async function runPi(args: string[], hardExit?: HardExit): Promise<number> {
|
|
83
|
-
const { main } = await import("@earendil-works/pi-coding-agent");
|
|
84
|
-
const originalExit = process.exit;
|
|
85
|
-
let requestedExitCode: number | undefined;
|
|
86
|
-
let resolveExit: (code: number) => void = () => {};
|
|
87
|
-
const exitRequested = new Promise<number>((resolve) => {
|
|
88
|
-
resolveExit = resolve;
|
|
89
|
-
});
|
|
90
|
-
process.exit = ((code?: number | string | null) => {
|
|
91
|
-
requestedExitCode =
|
|
92
|
-
typeof code === "number" ? code : (process.exitCode ?? 0);
|
|
93
|
-
resolveExit(requestedExitCode);
|
|
94
|
-
return undefined as never;
|
|
95
|
-
}) as typeof process.exit;
|
|
96
|
-
try {
|
|
97
|
-
const mainDone = main(args, {
|
|
98
|
-
extensionFactories: [createUiPathProviderExtension()],
|
|
99
|
-
}).then(() => requestedExitCode ?? 0);
|
|
100
|
-
// If main settles after an exit was already requested, that outcome
|
|
101
|
-
// has nowhere to go — swallow it so it cannot become an unhandled
|
|
102
|
-
// rejection. The raced result below already carries the exit code.
|
|
103
|
-
mainDone.catch(() => {});
|
|
104
|
-
return await Promise.race([mainDone, exitRequested]);
|
|
105
|
-
} finally {
|
|
106
|
-
process.exit = originalExit;
|
|
107
|
-
// Preserve Pi's lifecycle: force the exit once telemetry and output
|
|
108
|
-
// (which run in microtasks before I/O callbacks) have completed.
|
|
109
|
-
const exit: HardExit = hardExit ?? ((code) => originalExit(code));
|
|
110
|
-
setImmediate(() => exit(process.exitCode ?? requestedExitCode ?? 0));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* `uip coder [args...]` — the UiPath coding agent, powered by Pi.
|
|
116
|
-
*
|
|
117
|
-
* All arguments are forwarded verbatim to Pi, so the full Pi CLI surface
|
|
118
|
-
* stays available: no args opens the interactive TUI; `-p "<prompt>"` runs
|
|
119
|
-
* one prompt and exits; `--list-models` shows every provider/model;
|
|
120
|
-
* `--provider uipath --model <id>` pins the UiPath LLM Gateway;
|
|
121
|
-
* `--continue` resumes the previous session. See `uip coder --help`
|
|
122
|
-
* (rendered by Pi) for everything else.
|
|
123
|
-
*/
|
|
124
|
-
export function registerChatCommand(
|
|
125
|
-
command: Command,
|
|
126
|
-
context: CommandContext = processContext,
|
|
127
|
-
hardExit?: HardExit,
|
|
128
|
-
): void {
|
|
129
|
-
command
|
|
130
|
-
.argument(
|
|
131
|
-
"[args...]",
|
|
132
|
-
"arguments forwarded to the Pi coding agent (see 'uip coder --help')",
|
|
133
|
-
)
|
|
134
|
-
.allowUnknownOption()
|
|
135
|
-
.allowExcessArguments(true)
|
|
136
|
-
.helpOption(false)
|
|
137
|
-
.trackedAction(
|
|
138
|
-
context,
|
|
139
|
-
async (_args: string[], _options: unknown, cmd: Command) => {
|
|
140
|
-
const guide = await resolveAgentGuide();
|
|
141
|
-
const exitCode = await runPi(
|
|
142
|
-
withAgentGuide(cmd.args, guide),
|
|
143
|
-
hardExit,
|
|
144
|
-
);
|
|
145
|
-
if (exitCode !== 0) {
|
|
146
|
-
OutputFormatter.error({
|
|
147
|
-
Result: RESULTS.Failure,
|
|
148
|
-
Message: `The coding agent exited with code ${exitCode}.`,
|
|
149
|
-
Instructions:
|
|
150
|
-
"Check the agent output above for the underlying error. Run 'uip coder --help' for usage, or 'uip login' if the uipath provider is missing.",
|
|
151
|
-
});
|
|
152
|
-
context.exit(exitCode);
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
);
|
|
156
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Standalone entry point for coder-tool
|
|
5
|
-
* This allows the tool to be run independently for testing
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Side-effect import: installs Command.prototype.trackedAction on this
|
|
9
|
-
// bundle's commander copy before registerCommands touches it.
|
|
10
|
-
import { setPreviewBuild } from "@uipath/common";
|
|
11
|
-
import { Command } from "commander";
|
|
12
|
-
import { metadata, registerCommands } from "./tool.js";
|
|
13
|
-
|
|
14
|
-
// The standalone bin is a dev/test surface — opt into the preview-gated
|
|
15
|
-
// command unconditionally. The CLI host gates on its own release channel.
|
|
16
|
-
setPreviewBuild(true);
|
|
17
|
-
|
|
18
|
-
const program = new Command();
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.name(metadata.commandPrefix)
|
|
22
|
-
.description(metadata.description)
|
|
23
|
-
.version(metadata.version);
|
|
24
|
-
|
|
25
|
-
await registerCommands(program);
|
|
26
|
-
|
|
27
|
-
program.parse(process.argv);
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("@uipath/auth", () => ({
|
|
4
|
-
getLoginStatusAsync: 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
|
-
logger: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
12
|
-
};
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
import { getLoginStatusAsync } from "@uipath/auth";
|
|
16
|
-
import { createUiPathProviderExtension, UIPATH_PROVIDER_ID } from "./uipath";
|
|
17
|
-
|
|
18
|
-
type EventHandler = (event: unknown) => Promise<unknown>;
|
|
19
|
-
|
|
20
|
-
function buildFakePi() {
|
|
21
|
-
const handlers = new Map<string, EventHandler>();
|
|
22
|
-
return {
|
|
23
|
-
registerProvider: vi.fn(),
|
|
24
|
-
on: vi.fn((event: string, handler: EventHandler) => {
|
|
25
|
-
handlers.set(event, handler);
|
|
26
|
-
}),
|
|
27
|
-
handlers,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function loggedInStatus(expiresInMs: number) {
|
|
32
|
-
return {
|
|
33
|
-
loginStatus: "Logged in" as const,
|
|
34
|
-
accessToken: "header.payload.signature",
|
|
35
|
-
baseUrl: "https://cloud.uipath.com",
|
|
36
|
-
organizationId: "org-id",
|
|
37
|
-
organizationName: "myorg",
|
|
38
|
-
tenantName: "mytenant",
|
|
39
|
-
expiration: new Date(Date.now() + expiresInMs),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe("createUiPathProviderExtension", () => {
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
vi.resetAllMocks();
|
|
46
|
-
vi.unstubAllGlobals();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("skips registration when not logged in", async () => {
|
|
50
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue({
|
|
51
|
-
loginStatus: "Not logged in",
|
|
52
|
-
});
|
|
53
|
-
const pi = buildFakePi();
|
|
54
|
-
|
|
55
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
56
|
-
|
|
57
|
-
expect(pi.registerProvider).not.toHaveBeenCalled();
|
|
58
|
-
expect(pi.on).not.toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("registers discovered models against the agenthub_ gateway route", async () => {
|
|
62
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
63
|
-
loggedInStatus(60 * 60 * 1000),
|
|
64
|
-
);
|
|
65
|
-
vi.stubGlobal(
|
|
66
|
-
"fetch",
|
|
67
|
-
vi.fn().mockResolvedValue({
|
|
68
|
-
ok: true,
|
|
69
|
-
json: () =>
|
|
70
|
-
Promise.resolve({
|
|
71
|
-
supportedModels: [
|
|
72
|
-
{
|
|
73
|
-
model: "gpt-4o-2024-11-20",
|
|
74
|
-
contextWindow: 128000,
|
|
75
|
-
},
|
|
76
|
-
"gpt-4o-mini-2024-07-18",
|
|
77
|
-
],
|
|
78
|
-
}),
|
|
79
|
-
}),
|
|
80
|
-
);
|
|
81
|
-
const pi = buildFakePi();
|
|
82
|
-
|
|
83
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
84
|
-
|
|
85
|
-
expect(pi.registerProvider).toHaveBeenCalledWith(
|
|
86
|
-
UIPATH_PROVIDER_ID,
|
|
87
|
-
expect.objectContaining({
|
|
88
|
-
name: "UiPath LLM Gateway",
|
|
89
|
-
api: "openai-completions",
|
|
90
|
-
apiKey: "header.payload.signature",
|
|
91
|
-
baseUrl:
|
|
92
|
-
"https://cloud.uipath.com/org-id/mytenant/agenthub_/llm/openai",
|
|
93
|
-
}),
|
|
94
|
-
);
|
|
95
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
96
|
-
models: Array<{ id: string; baseUrl: string }>;
|
|
97
|
-
};
|
|
98
|
-
expect(config.models).toHaveLength(2);
|
|
99
|
-
expect(config.models[0]?.baseUrl).toBe(
|
|
100
|
-
"https://cloud.uipath.com/org-id/mytenant/agenthub_/llm/openai/deployments/gpt-4o-2024-11-20",
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("accepts a bare-array discovery response", async () => {
|
|
105
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
106
|
-
loggedInStatus(60 * 60 * 1000),
|
|
107
|
-
);
|
|
108
|
-
vi.stubGlobal(
|
|
109
|
-
"fetch",
|
|
110
|
-
vi.fn().mockResolvedValue({
|
|
111
|
-
ok: true,
|
|
112
|
-
json: () => Promise.resolve(["gpt-4o-2024-11-20"]),
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
const pi = buildFakePi();
|
|
116
|
-
|
|
117
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
118
|
-
|
|
119
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
120
|
-
models: Array<{ id: string }>;
|
|
121
|
-
};
|
|
122
|
-
expect(config.models.map((m) => m.id)).toEqual(["gpt-4o-2024-11-20"]);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("falls back to default models when discovery returns null", async () => {
|
|
126
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
127
|
-
loggedInStatus(60 * 60 * 1000),
|
|
128
|
-
);
|
|
129
|
-
vi.stubGlobal(
|
|
130
|
-
"fetch",
|
|
131
|
-
vi.fn().mockResolvedValue({
|
|
132
|
-
ok: true,
|
|
133
|
-
json: () => Promise.resolve(null),
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
136
|
-
const pi = buildFakePi();
|
|
137
|
-
|
|
138
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
139
|
-
|
|
140
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
141
|
-
models: Array<{ id: string }>;
|
|
142
|
-
};
|
|
143
|
-
expect(config.models.length).toBeGreaterThan(0);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("passes a timeout signal to the discovery request", async () => {
|
|
147
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
148
|
-
loggedInStatus(60 * 60 * 1000),
|
|
149
|
-
);
|
|
150
|
-
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
|
151
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
152
|
-
const pi = buildFakePi();
|
|
153
|
-
|
|
154
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
155
|
-
|
|
156
|
-
const init = fetchMock.mock.calls[0]?.[1] as { signal?: AbortSignal };
|
|
157
|
-
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("falls back to default models when discovery fails", async () => {
|
|
161
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
162
|
-
loggedInStatus(60 * 60 * 1000),
|
|
163
|
-
);
|
|
164
|
-
vi.stubGlobal(
|
|
165
|
-
"fetch",
|
|
166
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
167
|
-
);
|
|
168
|
-
const pi = buildFakePi();
|
|
169
|
-
|
|
170
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
171
|
-
|
|
172
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
173
|
-
models: Array<{ id: string }>;
|
|
174
|
-
};
|
|
175
|
-
expect(config.models.length).toBeGreaterThan(0);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("encodes model ids when building deployment routes", async () => {
|
|
179
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
180
|
-
loggedInStatus(60 * 60 * 1000),
|
|
181
|
-
);
|
|
182
|
-
vi.stubGlobal(
|
|
183
|
-
"fetch",
|
|
184
|
-
vi.fn().mockResolvedValue({
|
|
185
|
-
ok: true,
|
|
186
|
-
json: () =>
|
|
187
|
-
Promise.resolve({ supportedModels: ["openai/gpt-4o"] }),
|
|
188
|
-
}),
|
|
189
|
-
);
|
|
190
|
-
const pi = buildFakePi();
|
|
191
|
-
|
|
192
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
193
|
-
|
|
194
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
195
|
-
models: Array<{ id: string; baseUrl: string }>;
|
|
196
|
-
};
|
|
197
|
-
expect(config.models[0]?.id).toBe("openai/gpt-4o");
|
|
198
|
-
expect(config.models[0]?.baseUrl).toBe(
|
|
199
|
-
"https://cloud.uipath.com/org-id/mytenant/agenthub_/llm/openai/deployments/openai%2Fgpt-4o",
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("skips registration when resolving the login status fails", async () => {
|
|
204
|
-
vi.mocked(getLoginStatusAsync).mockRejectedValue(
|
|
205
|
-
new Error("auth file unreadable"),
|
|
206
|
-
);
|
|
207
|
-
const pi = buildFakePi();
|
|
208
|
-
|
|
209
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
210
|
-
|
|
211
|
-
expect(pi.registerProvider).not.toHaveBeenCalled();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("falls back to default models when discovery returns an unusable shape", async () => {
|
|
215
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
216
|
-
loggedInStatus(60 * 60 * 1000),
|
|
217
|
-
);
|
|
218
|
-
vi.stubGlobal(
|
|
219
|
-
"fetch",
|
|
220
|
-
vi.fn().mockResolvedValue({
|
|
221
|
-
ok: true,
|
|
222
|
-
json: () => Promise.resolve({ unexpected: true }),
|
|
223
|
-
}),
|
|
224
|
-
);
|
|
225
|
-
const pi = buildFakePi();
|
|
226
|
-
|
|
227
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
228
|
-
|
|
229
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
230
|
-
models: Array<{ id: string }>;
|
|
231
|
-
};
|
|
232
|
-
expect(config.models.length).toBeGreaterThan(0);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("uses organizationName in the gateway path when organizationId is missing", async () => {
|
|
236
|
-
const status = loggedInStatus(60 * 60 * 1000);
|
|
237
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue({
|
|
238
|
-
...status,
|
|
239
|
-
organizationId: undefined,
|
|
240
|
-
});
|
|
241
|
-
vi.stubGlobal(
|
|
242
|
-
"fetch",
|
|
243
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
244
|
-
);
|
|
245
|
-
const pi = buildFakePi();
|
|
246
|
-
|
|
247
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
248
|
-
|
|
249
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
250
|
-
baseUrl: string;
|
|
251
|
-
};
|
|
252
|
-
expect(config.baseUrl).toBe(
|
|
253
|
-
"https://cloud.uipath.com/myorg/mytenant/agenthub_/llm/openai",
|
|
254
|
-
);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("does not re-register on agent_start while the token is fresh", async () => {
|
|
258
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
259
|
-
loggedInStatus(60 * 60 * 1000),
|
|
260
|
-
);
|
|
261
|
-
vi.stubGlobal(
|
|
262
|
-
"fetch",
|
|
263
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
264
|
-
);
|
|
265
|
-
const pi = buildFakePi();
|
|
266
|
-
|
|
267
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
268
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(1);
|
|
269
|
-
|
|
270
|
-
await pi.handlers.get("agent_start")?.({ type: "agent_start" });
|
|
271
|
-
|
|
272
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(1);
|
|
273
|
-
expect(getLoginStatusAsync).toHaveBeenCalledTimes(1);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("re-registers with a fresh token on agent_start when close to expiry", async () => {
|
|
277
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
278
|
-
loggedInStatus(60 * 1000),
|
|
279
|
-
);
|
|
280
|
-
vi.stubGlobal(
|
|
281
|
-
"fetch",
|
|
282
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
283
|
-
);
|
|
284
|
-
const pi = buildFakePi();
|
|
285
|
-
|
|
286
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
287
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(1);
|
|
288
|
-
|
|
289
|
-
const handler = pi.handlers.get("agent_start");
|
|
290
|
-
expect(handler).toBeDefined();
|
|
291
|
-
await handler?.({ type: "agent_start" });
|
|
292
|
-
|
|
293
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(2);
|
|
294
|
-
expect(getLoginStatusAsync).toHaveBeenCalledTimes(2);
|
|
295
|
-
});
|
|
296
|
-
});
|