@pulse-editor/cli 0.1.10 → 0.1.12
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/dist/components/commands/login.js +3 -3
- package/dist/components/commands/publish.js +1 -1
- package/dist/components/header.js +1 -1
- package/dist/lib/__tests__/backend-url.test.d.ts +1 -0
- package/dist/lib/__tests__/backend-url.test.js +10 -0
- package/dist/lib/__tests__/cli-flags.test.d.ts +1 -0
- package/dist/lib/__tests__/cli-flags.test.js +38 -0
- package/dist/lib/__tests__/manual.test.d.ts +1 -0
- package/dist/lib/__tests__/manual.test.js +31 -0
- package/dist/lib/__tests__/token.test.d.ts +1 -0
- package/dist/lib/__tests__/token.test.js +132 -0
- package/dist/lib/__tests__/types.test.d.ts +1 -0
- package/dist/lib/__tests__/types.test.js +10 -0
- package/dist/lib/backend/__tests__/publish-app.test.d.ts +1 -0
- package/dist/lib/backend/__tests__/publish-app.test.js +40 -0
- package/dist/lib/backend-url.d.ts +1 -1
- package/dist/lib/backend-url.js +1 -1
- package/dist/lib/execa-utils/__tests__/clean.test.d.ts +1 -0
- package/dist/lib/execa-utils/__tests__/clean.test.js +23 -0
- package/dist/lib/execa-utils/__tests__/deps.test.d.ts +1 -0
- package/dist/lib/execa-utils/__tests__/deps.test.js +39 -0
- package/dist/lib/server/express.js +2 -2
- package/dist/lib/webpack/__tests__/compile.test.d.ts +1 -0
- package/dist/lib/webpack/__tests__/compile.test.js +62 -0
- package/dist/lib/webpack/__tests__/webpack-config.test.d.ts +1 -0
- package/dist/lib/webpack/__tests__/webpack-config.test.js +45 -0
- package/dist/lib/webpack/configs/__tests__/utils.test.d.ts +1 -0
- package/dist/lib/webpack/configs/__tests__/utils.test.js +691 -0
- package/package.json +5 -2
|
@@ -143,11 +143,11 @@ export default function Login({ cli }) {
|
|
|
143
143
|
}, {
|
|
144
144
|
isActive: loginMethod === "flow" && flowState === "idle" && Boolean(authUrl),
|
|
145
145
|
});
|
|
146
|
-
return (_jsxs(_Fragment, { children: [isShowLoginMethod && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Login to the
|
|
146
|
+
return (_jsxs(_Fragment, { children: [isShowLoginMethod && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Login to the Palmos Platform" }), _jsx(SelectInput, { items: loginMethodItems, onSelect: (item) => {
|
|
147
147
|
setLoginMethod(item.value);
|
|
148
148
|
}, isFocused: loginMethod === undefined }), _jsx(Text, { children: " " })] })), isMethodSelected &&
|
|
149
149
|
loginMethod === "token" &&
|
|
150
|
-
(token.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Enter your
|
|
150
|
+
(token.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Enter your Palmos access token:" }), _jsx(TextInput, { mask: "*", value: tokenInput, onChange: setTokenInput, onSubmit: (value) => {
|
|
151
151
|
if (value.length === 0) {
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
@@ -167,5 +167,5 @@ export default function Login({ cli }) {
|
|
|
167
167
|
else {
|
|
168
168
|
exit();
|
|
169
169
|
}
|
|
170
|
-
} })] })] })), isTokenSaved && (_jsxs(Text, { children: ["Token saved to ", path.join(os.homedir(), ".
|
|
170
|
+
} })] })] })), isTokenSaved && (_jsxs(Text, { children: ["Token saved to ", path.join(os.homedir(), ".palmos")] }))] })) : (_jsx(Text, { children: "Authentication error: please enter valid credentials." }))), isMethodSelected && loginMethod === "flow" && (_jsx(_Fragment, { children: flowState === "error" ? (_jsxs(Text, { color: "red", children: ["Error: ", flowError] })) : flowState === "success" ? (_jsx(Text, { children: "\u2705 Login successful! Saving credentials..." })) : flowState === "waiting" || flowState === "opening" ? (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Waiting for browser authentication..." })] }), _jsx(Text, { dimColor: true, children: "If the browser did not open, visit: " }), authUrl && _jsx(TerminalLink, { url: authUrl })] })) : authUrl ? (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { children: ["Press ", _jsx(Text, { bold: true, children: "Enter" }), " to open your browser and login:"] }), _jsx(TerminalLink, { url: authUrl, label: "Open browser to login \u2192" })] })) : (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Preparing login..." })] })) }))] }));
|
|
171
171
|
}
|
|
@@ -108,5 +108,5 @@ export default function Publish({ cli }) {
|
|
|
108
108
|
buildExtension();
|
|
109
109
|
}
|
|
110
110
|
}, [isAuthenticated]);
|
|
111
|
-
return (_jsx(_Fragment, { children: !isInProjectDir ? (_jsx(Text, { color: 'redBright', children: "\u26D4 The current directory does not contain a Pulse Editor project." })) : isCheckingAuth ? (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Checking authentication..." })] })) : !isAuthenticated ? (_jsxs(Text, { children: ["You are not authenticated or your access token is invalid. Publishing to Extension Marketplace is in Beta access. Please visit", _jsx(Text, { color: 'blueBright', children: " https://
|
|
111
|
+
return (_jsx(_Fragment, { children: !isInProjectDir ? (_jsx(Text, { color: 'redBright', children: "\u26D4 The current directory does not contain a Pulse Editor project." })) : isCheckingAuth ? (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Checking authentication..." })] })) : !isAuthenticated ? (_jsxs(Text, { children: ["You are not authenticated or your access token is invalid. Publishing to Extension Marketplace is in Beta access. Please visit", _jsx(Text, { color: 'blueBright', children: " https://palmos.ai/beta " }), "to apply for Beta access."] })) : (_jsxs(_Fragment, { children: [isBuilding && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Building..." })] })), isBuildingError && (_jsx(Text, { color: 'redBright', children: "\u274C Error building the extension. Please run `npm run build` to see the error." })), isZipping && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Compressing build..." })] })), isZippingError && (_jsxs(Text, { color: 'redBright', children: ["\u274C Error zipping the build output. ", failureMessage] })), isPublishing && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Publishing..." })] })), isPublishingError && (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'redBright', children: "\u274C Failed to publish extension." }), failureMessage && (_jsxs(Text, { color: 'redBright', children: ["Error: ", failureMessage] }))] })), isPublished && (_jsx(Text, { color: 'greenBright', children: "\u2705 Extension published successfully." }))] })) }));
|
|
112
112
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
export default function Header() {
|
|
4
|
-
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: 'whiteBright', children: "
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: 'whiteBright', children: "Palmos CLI" }), _jsx(Text, { children: "Version: 0.0.1" }), _jsx(Text, { color: 'blueBright', children: "https://palmos.ai" })] }));
|
|
5
5
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getBackendUrl } from "../backend-url.js";
|
|
3
|
+
describe("getBackendUrl", () => {
|
|
4
|
+
it("should return localhost URL when stage is true", () => {
|
|
5
|
+
expect(getBackendUrl(true)).toBe("https://localhost:8080");
|
|
6
|
+
});
|
|
7
|
+
it("should return production URL when stage is false", () => {
|
|
8
|
+
expect(getBackendUrl(false)).toBe("https://palmos.ai");
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { flags } from "../cli-flags.js";
|
|
3
|
+
describe("flags", () => {
|
|
4
|
+
it("should define all expected flags", () => {
|
|
5
|
+
expect(flags.token).toBeDefined();
|
|
6
|
+
expect(flags.flow).toBeDefined();
|
|
7
|
+
expect(flags.framework).toBeDefined();
|
|
8
|
+
expect(flags.stage).toBeDefined();
|
|
9
|
+
expect(flags.name).toBeDefined();
|
|
10
|
+
expect(flags.visibility).toBeDefined();
|
|
11
|
+
expect(flags.target).toBeDefined();
|
|
12
|
+
expect(flags.beta).toBeDefined();
|
|
13
|
+
expect(flags.build).toBeDefined();
|
|
14
|
+
expect(flags.path).toBeDefined();
|
|
15
|
+
expect(flags.displayName).toBeDefined();
|
|
16
|
+
expect(flags.description).toBeDefined();
|
|
17
|
+
expect(flags.continue).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
it("should have correct types", () => {
|
|
20
|
+
expect(flags.token.type).toBe("boolean");
|
|
21
|
+
expect(flags.framework.type).toBe("string");
|
|
22
|
+
expect(flags.stage.type).toBe("boolean");
|
|
23
|
+
expect(flags.name.type).toBe("string");
|
|
24
|
+
});
|
|
25
|
+
it("should have correct defaults", () => {
|
|
26
|
+
expect(flags.stage.default).toBe(false);
|
|
27
|
+
expect(flags.build.default).toBe(true);
|
|
28
|
+
expect(flags.continue.default).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("should have correct short flags", () => {
|
|
31
|
+
expect(flags.framework.shortFlag).toBe("f");
|
|
32
|
+
expect(flags.name.shortFlag).toBe("n");
|
|
33
|
+
expect(flags.visibility.shortFlag).toBe("v");
|
|
34
|
+
expect(flags.target.shortFlag).toBe("t");
|
|
35
|
+
expect(flags.path.shortFlag).toBe("p");
|
|
36
|
+
expect(flags.description.shortFlag).toBe("d");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { commandsManual } from "../manual.js";
|
|
3
|
+
describe("commandsManual", () => {
|
|
4
|
+
it("should have all expected commands", () => {
|
|
5
|
+
const expectedCommands = [
|
|
6
|
+
"help",
|
|
7
|
+
"chat",
|
|
8
|
+
"code",
|
|
9
|
+
"login",
|
|
10
|
+
"logout",
|
|
11
|
+
"publish",
|
|
12
|
+
"create",
|
|
13
|
+
"preview",
|
|
14
|
+
"dev",
|
|
15
|
+
"build",
|
|
16
|
+
"start",
|
|
17
|
+
"clean",
|
|
18
|
+
"upgrade",
|
|
19
|
+
"skill",
|
|
20
|
+
];
|
|
21
|
+
for (const cmd of expectedCommands) {
|
|
22
|
+
expect(commandsManual).toHaveProperty(cmd);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it("should have non-empty string values", () => {
|
|
26
|
+
for (const [key, value] of Object.entries(commandsManual)) {
|
|
27
|
+
expect(typeof value).toBe("string");
|
|
28
|
+
expect(value.length).toBeGreaterThan(0);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { saveToken, getToken, isTokenInEnv, checkToken } from "../token.js";
|
|
3
|
+
vi.mock("fs", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
existsSync: vi.fn(),
|
|
6
|
+
readFileSync: vi.fn(),
|
|
7
|
+
writeFileSync: vi.fn(),
|
|
8
|
+
mkdirSync: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("os", () => ({
|
|
12
|
+
default: {
|
|
13
|
+
homedir: vi.fn(() => "/home/testuser"),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("../backend-url.js", () => ({
|
|
17
|
+
getBackendUrl: vi.fn((stage) => stage ? "https://localhost:8080" : "https://palmos.ai"),
|
|
18
|
+
}));
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
describe("saveToken", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
it("should create config dir if it does not exist and save prod token", () => {
|
|
25
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(false).mockReturnValueOnce(false);
|
|
26
|
+
vi.mocked(fs.readFileSync).mockReturnValue("{}");
|
|
27
|
+
saveToken("my-token", false);
|
|
28
|
+
expect(fs.mkdirSync).toHaveBeenCalled();
|
|
29
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining("config.json"), expect.stringContaining('"accessToken": "my-token"'));
|
|
30
|
+
});
|
|
31
|
+
it("should save dev token when devMode is true", () => {
|
|
32
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true);
|
|
33
|
+
saveToken("dev-token", true);
|
|
34
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('"devAccessToken": "dev-token"'));
|
|
35
|
+
});
|
|
36
|
+
it("should merge with existing config", () => {
|
|
37
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(true).mockReturnValueOnce(true);
|
|
38
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{"existingKey": "value"}');
|
|
39
|
+
saveToken("new-token", false);
|
|
40
|
+
const written = vi.mocked(fs.writeFileSync).mock.calls[0][1];
|
|
41
|
+
const parsed = JSON.parse(written);
|
|
42
|
+
expect(parsed.existingKey).toBe("value");
|
|
43
|
+
expect(parsed.accessToken).toBe("new-token");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("getToken", () => {
|
|
47
|
+
const originalEnv = process.env;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
process.env = { ...originalEnv };
|
|
51
|
+
});
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
process.env = originalEnv;
|
|
54
|
+
});
|
|
55
|
+
it("should return env var PE_ACCESS_TOKEN when set", () => {
|
|
56
|
+
process.env["PE_ACCESS_TOKEN"] = "env-token";
|
|
57
|
+
expect(getToken(false)).toBe("env-token");
|
|
58
|
+
});
|
|
59
|
+
it("should return env var PE_DEV_ACCESS_TOKEN when devMode", () => {
|
|
60
|
+
process.env["PE_DEV_ACCESS_TOKEN"] = "dev-env-token";
|
|
61
|
+
expect(getToken(true)).toBe("dev-env-token");
|
|
62
|
+
});
|
|
63
|
+
it("should read from config file when env not set", () => {
|
|
64
|
+
delete process.env["PE_ACCESS_TOKEN"];
|
|
65
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
66
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{"accessToken": "file-token"}');
|
|
67
|
+
expect(getToken(false)).toBe("file-token");
|
|
68
|
+
});
|
|
69
|
+
it("should return undefined when no env and no file", () => {
|
|
70
|
+
delete process.env["PE_ACCESS_TOKEN"];
|
|
71
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
72
|
+
expect(getToken(false)).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
it("should handle malformed JSON gracefully", () => {
|
|
75
|
+
delete process.env["PE_ACCESS_TOKEN"];
|
|
76
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
77
|
+
vi.mocked(fs.readFileSync).mockReturnValue("not json");
|
|
78
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
79
|
+
expect(getToken(false)).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
it("should return devAccessToken in dev mode from file", () => {
|
|
82
|
+
delete process.env["PE_DEV_ACCESS_TOKEN"];
|
|
83
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
84
|
+
vi.mocked(fs.readFileSync).mockReturnValue('{"devAccessToken": "dev-file-token"}');
|
|
85
|
+
expect(getToken(true)).toBe("dev-file-token");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("isTokenInEnv", () => {
|
|
89
|
+
const originalEnv = process.env;
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
process.env = { ...originalEnv };
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
process.env = originalEnv;
|
|
95
|
+
});
|
|
96
|
+
it("should return true when PE_ACCESS_TOKEN is set", () => {
|
|
97
|
+
process.env["PE_ACCESS_TOKEN"] = "token";
|
|
98
|
+
expect(isTokenInEnv(false)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it("should return false when PE_ACCESS_TOKEN is not set", () => {
|
|
101
|
+
delete process.env["PE_ACCESS_TOKEN"];
|
|
102
|
+
expect(isTokenInEnv(false)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
it("should check PE_DEV_ACCESS_TOKEN in dev mode", () => {
|
|
105
|
+
process.env["PE_DEV_ACCESS_TOKEN"] = "dev-token";
|
|
106
|
+
expect(isTokenInEnv(true)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe("checkToken", () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
});
|
|
113
|
+
it("should return true on 200 status", async () => {
|
|
114
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 });
|
|
115
|
+
const result = await checkToken("valid-token", false);
|
|
116
|
+
expect(result).toBe(true);
|
|
117
|
+
expect(fetch).toHaveBeenCalledWith("https://palmos.ai/api/api-keys/check", expect.objectContaining({
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: JSON.stringify({ token: "valid-token" }),
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
it("should return false on non-200 status", async () => {
|
|
123
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 });
|
|
124
|
+
const result = await checkToken("bad-token", false);
|
|
125
|
+
expect(result).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
it("should use stage URL in dev mode", async () => {
|
|
128
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 });
|
|
129
|
+
await checkToken("token", true);
|
|
130
|
+
expect(fetch).toHaveBeenCalledWith("https://localhost:8080/api/api-keys/check", expect.any(Object));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
// types.ts only exports a type, which doesn't generate runtime code.
|
|
3
|
+
// Import something from the file to register it for coverage.
|
|
4
|
+
describe("types", () => {
|
|
5
|
+
it("should export Item type (compile-time only)", async () => {
|
|
6
|
+
// This is a type-only file, importing it registers coverage
|
|
7
|
+
const mod = await import("../types.js");
|
|
8
|
+
expect(mod).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { publishApp } from "../publish-app.js";
|
|
3
|
+
vi.mock("fs", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
readFileSync: vi.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("../../token.js", () => ({
|
|
9
|
+
getToken: vi.fn(() => "test-token"),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("../../backend-url.js", () => ({
|
|
12
|
+
getBackendUrl: vi.fn((stage) => stage ? "https://localhost:8080" : "https://palmos.ai"),
|
|
13
|
+
}));
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
describe("publishApp", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 });
|
|
19
|
+
});
|
|
20
|
+
it("should read config and zip, then POST to publish endpoint", async () => {
|
|
21
|
+
vi.mocked(fs.readFileSync)
|
|
22
|
+
.mockReturnValueOnce('{"visibility": "public"}')
|
|
23
|
+
.mockReturnValueOnce(Buffer.from("fake-zip"));
|
|
24
|
+
const res = await publishApp(false);
|
|
25
|
+
expect(fetch).toHaveBeenCalledWith("https://palmos.ai/api/app/publish", expect.objectContaining({
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: expect.objectContaining({
|
|
28
|
+
Authorization: "Bearer test-token",
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
expect(res.status).toBe(200);
|
|
32
|
+
});
|
|
33
|
+
it("should use stage URL when isStage is true", async () => {
|
|
34
|
+
vi.mocked(fs.readFileSync)
|
|
35
|
+
.mockReturnValueOnce('{"visibility": "private"}')
|
|
36
|
+
.mockReturnValueOnce(Buffer.from("fake-zip"));
|
|
37
|
+
await publishApp(true);
|
|
38
|
+
expect(fetch).toHaveBeenCalledWith("https://localhost:8080/api/app/publish", expect.any(Object));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function getBackendUrl(stage: boolean): "https://localhost:8080" | "https://
|
|
1
|
+
export declare function getBackendUrl(stage: boolean): "https://localhost:8080" | "https://palmos.ai";
|
package/dist/lib/backend-url.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { cleanDist } from "../clean.js";
|
|
3
|
+
vi.mock("execa", () => ({
|
|
4
|
+
execa: vi.fn().mockResolvedValue(undefined),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock("../deps.js", () => ({
|
|
7
|
+
getDepsBinPath: vi.fn(() => "npx rimraf"),
|
|
8
|
+
}));
|
|
9
|
+
import { execa } from "execa";
|
|
10
|
+
describe("cleanDist", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
vi.spyOn(console, "log").mockImplementation(() => { });
|
|
14
|
+
});
|
|
15
|
+
it("should call execa with rimraf dist", async () => {
|
|
16
|
+
await cleanDist();
|
|
17
|
+
expect(execa).toHaveBeenCalledWith("npx rimraf dist", { shell: true });
|
|
18
|
+
});
|
|
19
|
+
it("should log cleaning messages", async () => {
|
|
20
|
+
await cleanDist();
|
|
21
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("Cleaning dist"));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { getDepsBinPath } from "../deps.js";
|
|
3
|
+
vi.mock("fs", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
existsSync: vi.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
describe("getDepsBinPath", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
it("should return npx when package exists in local node_modules", () => {
|
|
14
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(true);
|
|
15
|
+
expect(getDepsBinPath("rimraf")).toBe("npx rimraf");
|
|
16
|
+
});
|
|
17
|
+
it("should return full path with .cmd on Windows", () => {
|
|
18
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true);
|
|
19
|
+
const originalPlatform = process.platform;
|
|
20
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
21
|
+
const result = getDepsBinPath("rimraf");
|
|
22
|
+
expect(result).toContain("rimraf.cmd");
|
|
23
|
+
expect(result).toContain("@pulse-editor/cli/node_modules/.bin/");
|
|
24
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
25
|
+
});
|
|
26
|
+
it("should return full path without .cmd on non-Windows", () => {
|
|
27
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true);
|
|
28
|
+
const originalPlatform = process.platform;
|
|
29
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
30
|
+
const result = getDepsBinPath("rimraf");
|
|
31
|
+
expect(result).toContain("@pulse-editor/cli/node_modules/.bin/rimraf");
|
|
32
|
+
expect(result).not.toContain(".cmd");
|
|
33
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
34
|
+
});
|
|
35
|
+
it("should throw when dependency not found", () => {
|
|
36
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
37
|
+
expect(() => getDepsBinPath("nonexistent")).toThrow("Dependency nonexistent not found.");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -33,14 +33,14 @@ const skillActions = pulseConfig?.actions || [];
|
|
|
33
33
|
const skillActionNames = skillActions.map((a) => a.name);
|
|
34
34
|
const app = express();
|
|
35
35
|
app.use(cors());
|
|
36
|
-
// Inject the client-side livereload script into HTML responses
|
|
36
|
+
// Inject the client-side livereload script into HTML responses in workspace
|
|
37
37
|
app.use(
|
|
38
38
|
// The port might not be right here for the ingress.
|
|
39
39
|
// I need this route to be exposed
|
|
40
40
|
connectLivereload({
|
|
41
41
|
// @ts-expect-error override server options
|
|
42
42
|
host: workspaceId
|
|
43
|
-
? `${workspaceId}.workspace.
|
|
43
|
+
? `${workspaceId}.workspace.palmos.ai"`
|
|
44
44
|
: undefined,
|
|
45
45
|
port: workspaceId ? 443 : 35729,
|
|
46
46
|
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { webpackCompile } from "../compile.js";
|
|
3
|
+
vi.mock("webpack", () => {
|
|
4
|
+
const mockCompiler = {
|
|
5
|
+
run: vi.fn((cb) => cb(null)),
|
|
6
|
+
watch: vi.fn((_opts, cb) => cb(null)),
|
|
7
|
+
};
|
|
8
|
+
const webpack = vi.fn(() => mockCompiler);
|
|
9
|
+
return { default: webpack, __mockCompiler: mockCompiler };
|
|
10
|
+
});
|
|
11
|
+
vi.mock("../configs/utils.js", () => ({
|
|
12
|
+
generateTempTsConfig: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("../webpack-config.js", () => ({
|
|
15
|
+
createWebpackConfig: vi.fn().mockResolvedValue([{ name: "config" }]),
|
|
16
|
+
}));
|
|
17
|
+
import webpack from "webpack";
|
|
18
|
+
import { generateTempTsConfig } from "../configs/utils.js";
|
|
19
|
+
import { createWebpackConfig } from "../webpack-config.js";
|
|
20
|
+
describe("webpackCompile", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
it("should call generateTempTsConfig", async () => {
|
|
25
|
+
await webpackCompile("production");
|
|
26
|
+
expect(generateTempTsConfig).toHaveBeenCalled();
|
|
27
|
+
});
|
|
28
|
+
it("should pass correct params for production mode", async () => {
|
|
29
|
+
await webpackCompile("production");
|
|
30
|
+
expect(createWebpackConfig).toHaveBeenCalledWith(false, "both", "production");
|
|
31
|
+
});
|
|
32
|
+
it("should pass correct params for development mode", async () => {
|
|
33
|
+
await webpackCompile("development");
|
|
34
|
+
expect(createWebpackConfig).toHaveBeenCalledWith(false, "both", "development");
|
|
35
|
+
});
|
|
36
|
+
it("should pass correct params for preview mode", async () => {
|
|
37
|
+
await webpackCompile("preview");
|
|
38
|
+
expect(createWebpackConfig).toHaveBeenCalledWith(true, "both", "development");
|
|
39
|
+
});
|
|
40
|
+
it("should pass buildTarget when specified", async () => {
|
|
41
|
+
await webpackCompile("production", "client");
|
|
42
|
+
expect(createWebpackConfig).toHaveBeenCalledWith(false, "client", "production");
|
|
43
|
+
});
|
|
44
|
+
it("should return compiler in watch mode", async () => {
|
|
45
|
+
const result = await webpackCompile("development", undefined, true);
|
|
46
|
+
expect(result).toBeDefined();
|
|
47
|
+
const mockCompiler = webpack().__proto__; // just verify it returns
|
|
48
|
+
});
|
|
49
|
+
it("should handle webpack run errors", async () => {
|
|
50
|
+
const { __mockCompiler } = await import("webpack");
|
|
51
|
+
__mockCompiler.run.mockImplementationOnce((cb) => cb(new Error("build error")));
|
|
52
|
+
await expect(webpackCompile("production")).rejects.toThrow("build error");
|
|
53
|
+
});
|
|
54
|
+
it("should log error when watch mode has errors", async () => {
|
|
55
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
56
|
+
const { __mockCompiler } = await import("webpack");
|
|
57
|
+
__mockCompiler.watch.mockImplementationOnce((_opts, cb) => cb(new Error("watch error")));
|
|
58
|
+
const result = await webpackCompile("development", undefined, true);
|
|
59
|
+
expect(result).toBeDefined();
|
|
60
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Webpack build failed"), expect.any(Error));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { createWebpackConfig } from "../webpack-config.js";
|
|
3
|
+
vi.mock("../configs/mf-client.js", () => ({
|
|
4
|
+
makeMFClientConfig: vi.fn().mockResolvedValue({ name: "client-config" }),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock("../configs/mf-server.js", () => ({
|
|
7
|
+
makeMFServerConfig: vi.fn().mockResolvedValue({ name: "server-config" }),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("../configs/preview.js", () => ({
|
|
10
|
+
makePreviewClientConfig: vi
|
|
11
|
+
.fn()
|
|
12
|
+
.mockResolvedValue({ name: "preview-config" }),
|
|
13
|
+
}));
|
|
14
|
+
import { makeMFClientConfig } from "../configs/mf-client.js";
|
|
15
|
+
import { makeMFServerConfig } from "../configs/mf-server.js";
|
|
16
|
+
import { makePreviewClientConfig } from "../configs/preview.js";
|
|
17
|
+
describe("createWebpackConfig", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
it("should return preview + server configs when isPreview", async () => {
|
|
22
|
+
const result = await createWebpackConfig(true, "both", "development");
|
|
23
|
+
expect(result).toHaveLength(2);
|
|
24
|
+
expect(makePreviewClientConfig).toHaveBeenCalledWith("development");
|
|
25
|
+
expect(makeMFServerConfig).toHaveBeenCalledWith("development");
|
|
26
|
+
});
|
|
27
|
+
it("should return only server config when buildTarget is server", async () => {
|
|
28
|
+
const result = await createWebpackConfig(false, "server", "production");
|
|
29
|
+
expect(result).toHaveLength(1);
|
|
30
|
+
expect(makeMFServerConfig).toHaveBeenCalledWith("production");
|
|
31
|
+
expect(makeMFClientConfig).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
it("should return only client config when buildTarget is client", async () => {
|
|
34
|
+
const result = await createWebpackConfig(false, "client", "production");
|
|
35
|
+
expect(result).toHaveLength(1);
|
|
36
|
+
expect(makeMFClientConfig).toHaveBeenCalledWith("production");
|
|
37
|
+
expect(makeMFServerConfig).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
it("should return both configs when buildTarget is both", async () => {
|
|
40
|
+
const result = await createWebpackConfig(false, "both", "development");
|
|
41
|
+
expect(result).toHaveLength(2);
|
|
42
|
+
expect(makeMFClientConfig).toHaveBeenCalledWith("development");
|
|
43
|
+
expect(makeMFServerConfig).toHaveBeenCalledWith("development");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { getLocalNetworkIP, discoverServerFunctions, normalizeJSDocPropertyName, getActionType, isPromiseLikeType, unwrapPromiseLikeType, parseTypeDefs, generateTempTsConfig, readConfigFile, discoverAppSkillActions, compileAppActionSkills, } from "../utils.js";
|
|
3
|
+
vi.mock("os", () => ({
|
|
4
|
+
networkInterfaces: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock("glob", () => ({
|
|
7
|
+
globSync: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("fs", () => ({
|
|
10
|
+
existsSync: vi.fn(),
|
|
11
|
+
writeFileSync: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("fs/promises", () => ({
|
|
14
|
+
default: {
|
|
15
|
+
access: vi.fn(),
|
|
16
|
+
readFile: vi.fn(),
|
|
17
|
+
copyFile: vi.fn(),
|
|
18
|
+
rm: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
// Mock ts-morph for discoverAppSkillActions and compileAppActionSkills
|
|
22
|
+
const mockGetName = vi.fn().mockReturnValue("testAction");
|
|
23
|
+
const mockGetText = vi.fn().mockReturnValue("initializer");
|
|
24
|
+
const mockGetInitializer = vi.fn().mockReturnValue({ getText: mockGetText });
|
|
25
|
+
const mockBindingElement = {
|
|
26
|
+
getName: mockGetName,
|
|
27
|
+
getInitializer: mockGetInitializer,
|
|
28
|
+
getKind: vi.fn().mockReturnValue(208), // BindingElement
|
|
29
|
+
};
|
|
30
|
+
const mockNameNode = {
|
|
31
|
+
getElements: vi.fn().mockReturnValue([]),
|
|
32
|
+
};
|
|
33
|
+
const mockFuncParam = {
|
|
34
|
+
getNameNode: vi.fn().mockReturnValue(mockNameNode),
|
|
35
|
+
getType: vi.fn().mockReturnValue({
|
|
36
|
+
getProperties: vi.fn().mockReturnValue([]),
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
const mockFuncDecl = {
|
|
40
|
+
getName: vi.fn().mockReturnValue("testAction"),
|
|
41
|
+
getJsDocs: vi.fn().mockReturnValue([
|
|
42
|
+
{
|
|
43
|
+
getDescription: vi.fn().mockReturnValue("A test action"),
|
|
44
|
+
getFullText: vi.fn().mockReturnValue("/** A test action */"),
|
|
45
|
+
},
|
|
46
|
+
]),
|
|
47
|
+
getParameters: vi.fn().mockReturnValue([]),
|
|
48
|
+
getReturnType: vi.fn().mockReturnValue({
|
|
49
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
50
|
+
getProperties: vi.fn().mockReturnValue([]),
|
|
51
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
52
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
53
|
+
}),
|
|
54
|
+
getKind: vi.fn().mockReturnValue(272), // FunctionDeclaration
|
|
55
|
+
asKind: vi.fn().mockReturnThis(),
|
|
56
|
+
asKindOrThrow: vi.fn().mockReturnThis(),
|
|
57
|
+
};
|
|
58
|
+
const mockSourceFile = {
|
|
59
|
+
getDefaultExportSymbol: vi.fn().mockReturnValue({
|
|
60
|
+
getDeclarations: vi.fn().mockReturnValue([mockFuncDecl]),
|
|
61
|
+
}),
|
|
62
|
+
getDescendantsOfKind: vi.fn().mockReturnValue([]),
|
|
63
|
+
};
|
|
64
|
+
vi.mock("ts-morph", () => {
|
|
65
|
+
class MockProject {
|
|
66
|
+
addSourceFileAtPath = vi.fn().mockReturnValue(mockSourceFile);
|
|
67
|
+
constructor(_opts) { }
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
Project: MockProject,
|
|
71
|
+
Node: {
|
|
72
|
+
isObjectBindingPattern: vi.fn().mockReturnValue(false),
|
|
73
|
+
isBindingElement: vi.fn().mockReturnValue(false),
|
|
74
|
+
},
|
|
75
|
+
SyntaxKind: {
|
|
76
|
+
FunctionDeclaration: 272,
|
|
77
|
+
JSDoc: 348,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
import { networkInterfaces } from "os";
|
|
82
|
+
import { globSync } from "glob";
|
|
83
|
+
import { existsSync, writeFileSync } from "fs";
|
|
84
|
+
describe("getLocalNetworkIP", () => {
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
vi.clearAllMocks();
|
|
87
|
+
});
|
|
88
|
+
it("should return first non-internal IPv4 address", () => {
|
|
89
|
+
vi.mocked(networkInterfaces).mockReturnValue({
|
|
90
|
+
eth0: [
|
|
91
|
+
{
|
|
92
|
+
address: "192.168.1.100",
|
|
93
|
+
family: "IPv4",
|
|
94
|
+
internal: false,
|
|
95
|
+
netmask: "255.255.255.0",
|
|
96
|
+
mac: "00:00:00:00:00:00",
|
|
97
|
+
cidr: "192.168.1.100/24",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
expect(getLocalNetworkIP()).toBe("192.168.1.100");
|
|
102
|
+
});
|
|
103
|
+
it("should skip internal addresses", () => {
|
|
104
|
+
vi.mocked(networkInterfaces).mockReturnValue({
|
|
105
|
+
lo: [
|
|
106
|
+
{
|
|
107
|
+
address: "127.0.0.1",
|
|
108
|
+
family: "IPv4",
|
|
109
|
+
internal: true,
|
|
110
|
+
netmask: "255.0.0.0",
|
|
111
|
+
mac: "00:00:00:00:00:00",
|
|
112
|
+
cidr: "127.0.0.1/8",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
expect(getLocalNetworkIP()).toBe("localhost");
|
|
117
|
+
});
|
|
118
|
+
it("should skip IPv6 addresses", () => {
|
|
119
|
+
vi.mocked(networkInterfaces).mockReturnValue({
|
|
120
|
+
eth0: [
|
|
121
|
+
{
|
|
122
|
+
address: "::1",
|
|
123
|
+
family: "IPv6",
|
|
124
|
+
internal: false,
|
|
125
|
+
netmask: "ffff:ffff:ffff:ffff::",
|
|
126
|
+
mac: "00:00:00:00:00:00",
|
|
127
|
+
cidr: "::1/128",
|
|
128
|
+
scopeid: 0,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
expect(getLocalNetworkIP()).toBe("localhost");
|
|
133
|
+
});
|
|
134
|
+
it("should return localhost when no interfaces", () => {
|
|
135
|
+
vi.mocked(networkInterfaces).mockReturnValue({});
|
|
136
|
+
expect(getLocalNetworkIP()).toBe("localhost");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("discoverServerFunctions", () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
vi.clearAllMocks();
|
|
142
|
+
});
|
|
143
|
+
it("should map server function files to entry points", () => {
|
|
144
|
+
vi.mocked(globSync).mockReturnValue([
|
|
145
|
+
"src/server-function/hello.ts",
|
|
146
|
+
"src/server-function/utils/helper.ts",
|
|
147
|
+
]);
|
|
148
|
+
const result = discoverServerFunctions();
|
|
149
|
+
expect(result).toEqual({
|
|
150
|
+
"./hello": "./src/server-function/hello.ts",
|
|
151
|
+
"./utils/helper": "./src/server-function/utils/helper.ts",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
it("should handle backslash paths (Windows)", () => {
|
|
155
|
+
vi.mocked(globSync).mockReturnValue([
|
|
156
|
+
"src\\server-function\\hello.ts",
|
|
157
|
+
]);
|
|
158
|
+
const result = discoverServerFunctions();
|
|
159
|
+
expect(result).toEqual({
|
|
160
|
+
"./hello": "./src/server-function/hello.ts",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
it("should return empty object when no files", () => {
|
|
164
|
+
vi.mocked(globSync).mockReturnValue([]);
|
|
165
|
+
const result = discoverServerFunctions();
|
|
166
|
+
expect(result).toEqual({});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("normalizeJSDocPropertyName", () => {
|
|
170
|
+
it("should remove brackets and defaults", () => {
|
|
171
|
+
expect(normalizeJSDocPropertyName("[name=default]")).toBe("name");
|
|
172
|
+
});
|
|
173
|
+
it("should handle plain name", () => {
|
|
174
|
+
expect(normalizeJSDocPropertyName("name")).toBe("name");
|
|
175
|
+
});
|
|
176
|
+
it("should return empty for undefined", () => {
|
|
177
|
+
expect(normalizeJSDocPropertyName(undefined)).toBe("");
|
|
178
|
+
});
|
|
179
|
+
it("should handle brackets without default", () => {
|
|
180
|
+
expect(normalizeJSDocPropertyName("[optional]")).toBe("optional");
|
|
181
|
+
});
|
|
182
|
+
it("should trim whitespace", () => {
|
|
183
|
+
expect(normalizeJSDocPropertyName(" name ")).toBe("name");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe("getActionType", () => {
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
189
|
+
});
|
|
190
|
+
it("should return string for 'string'", () => {
|
|
191
|
+
expect(getActionType("string")).toBe("string");
|
|
192
|
+
});
|
|
193
|
+
it("should return number for 'number'", () => {
|
|
194
|
+
expect(getActionType("number")).toBe("number");
|
|
195
|
+
});
|
|
196
|
+
it("should return boolean for 'boolean'", () => {
|
|
197
|
+
expect(getActionType("boolean")).toBe("boolean");
|
|
198
|
+
});
|
|
199
|
+
it("should return object for 'any'", () => {
|
|
200
|
+
expect(getActionType("any")).toBe("object");
|
|
201
|
+
});
|
|
202
|
+
it("should handle arrays", () => {
|
|
203
|
+
expect(getActionType("string[]")).toEqual(["string"]);
|
|
204
|
+
});
|
|
205
|
+
it("should handle nested arrays", () => {
|
|
206
|
+
expect(getActionType("number[][]")).toEqual([["number"]]);
|
|
207
|
+
});
|
|
208
|
+
it("should return undefined for empty string", () => {
|
|
209
|
+
expect(getActionType("")).toBe("undefined");
|
|
210
|
+
});
|
|
211
|
+
it("should return the text and warn for unknown types", () => {
|
|
212
|
+
expect(getActionType("CustomType")).toBe("CustomType");
|
|
213
|
+
expect(console.warn).toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
describe("isPromiseLikeType", () => {
|
|
217
|
+
it("should return true for Promise type", () => {
|
|
218
|
+
const mockType = {
|
|
219
|
+
getSymbol: () => ({ getName: () => "Promise" }),
|
|
220
|
+
};
|
|
221
|
+
expect(isPromiseLikeType(mockType)).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
it("should return true for PromiseLike type", () => {
|
|
224
|
+
const mockType = {
|
|
225
|
+
getSymbol: () => ({ getName: () => "PromiseLike" }),
|
|
226
|
+
};
|
|
227
|
+
expect(isPromiseLikeType(mockType)).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
it("should return false for other types", () => {
|
|
230
|
+
const mockType = {
|
|
231
|
+
getSymbol: () => ({ getName: () => "Observable" }),
|
|
232
|
+
};
|
|
233
|
+
expect(isPromiseLikeType(mockType)).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
it("should return false when no symbol", () => {
|
|
236
|
+
const mockType = { getSymbol: () => undefined };
|
|
237
|
+
expect(isPromiseLikeType(mockType)).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
describe("unwrapPromiseLikeType", () => {
|
|
241
|
+
it("should unwrap Promise type argument", () => {
|
|
242
|
+
const innerType = { text: "string" };
|
|
243
|
+
const mockType = {
|
|
244
|
+
getSymbol: () => ({ getName: () => "Promise" }),
|
|
245
|
+
getTypeArguments: () => [innerType],
|
|
246
|
+
};
|
|
247
|
+
expect(unwrapPromiseLikeType(mockType)).toBe(innerType);
|
|
248
|
+
});
|
|
249
|
+
it("should return original type if not Promise", () => {
|
|
250
|
+
const mockType = {
|
|
251
|
+
getSymbol: () => ({ getName: () => "Observable" }),
|
|
252
|
+
getTypeArguments: () => [],
|
|
253
|
+
};
|
|
254
|
+
expect(unwrapPromiseLikeType(mockType)).toBe(mockType);
|
|
255
|
+
});
|
|
256
|
+
it("should return original type if Promise has no type args", () => {
|
|
257
|
+
const mockType = {
|
|
258
|
+
getSymbol: () => ({ getName: () => "Promise" }),
|
|
259
|
+
getTypeArguments: () => [],
|
|
260
|
+
};
|
|
261
|
+
expect(unwrapPromiseLikeType(mockType)).toBe(mockType);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe("parseTypeDefs", () => {
|
|
265
|
+
it("should parse typedef and property from JSDoc", () => {
|
|
266
|
+
const mockDoc = {
|
|
267
|
+
getFullText: () => `/**
|
|
268
|
+
* @typedef {Object} input
|
|
269
|
+
* @property {string} name - The name of the user
|
|
270
|
+
* @property {number} age - The age
|
|
271
|
+
*/`,
|
|
272
|
+
};
|
|
273
|
+
const result = parseTypeDefs([mockDoc]);
|
|
274
|
+
expect(result["input"]).toBeDefined();
|
|
275
|
+
expect(result["input"]["name"]).toEqual({
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "The name of the user",
|
|
278
|
+
});
|
|
279
|
+
expect(result["input"]["age"]).toEqual({
|
|
280
|
+
type: "number",
|
|
281
|
+
description: "The age",
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
it("should handle optional properties with brackets", () => {
|
|
285
|
+
const mockDoc = {
|
|
286
|
+
getFullText: () => `/**
|
|
287
|
+
* @typedef {Object} input
|
|
288
|
+
* @property {string} [name=default] - Optional name
|
|
289
|
+
*/`,
|
|
290
|
+
};
|
|
291
|
+
const result = parseTypeDefs([mockDoc]);
|
|
292
|
+
expect(result["input"]["name"]).toEqual({
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "Optional name",
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
it("should return empty for no typedefs", () => {
|
|
298
|
+
const mockDoc = {
|
|
299
|
+
getFullText: () => `/** Just a comment */`,
|
|
300
|
+
};
|
|
301
|
+
const result = parseTypeDefs([mockDoc]);
|
|
302
|
+
expect(result).toEqual({});
|
|
303
|
+
});
|
|
304
|
+
it("should handle multiple typedefs in multiple docs", () => {
|
|
305
|
+
const mockDoc1 = {
|
|
306
|
+
getFullText: () => `/**
|
|
307
|
+
* @typedef {Object} input
|
|
308
|
+
* @property {string} name - The name
|
|
309
|
+
*/`,
|
|
310
|
+
};
|
|
311
|
+
const mockDoc2 = {
|
|
312
|
+
getFullText: () => `/**
|
|
313
|
+
* @typedef {Object} output
|
|
314
|
+
* @property {number} count - The count
|
|
315
|
+
*/`,
|
|
316
|
+
};
|
|
317
|
+
const result = parseTypeDefs([mockDoc1, mockDoc2]);
|
|
318
|
+
expect(result["input"]).toBeDefined();
|
|
319
|
+
expect(result["output"]).toBeDefined();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
describe("generateTempTsConfig", () => {
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
vi.clearAllMocks();
|
|
325
|
+
});
|
|
326
|
+
it("should skip if config already exists", () => {
|
|
327
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
328
|
+
generateTempTsConfig();
|
|
329
|
+
expect(writeFileSync).not.toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
it("should write tsconfig if it does not exist", () => {
|
|
332
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
333
|
+
generateTempTsConfig();
|
|
334
|
+
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining("tsconfig.server.json"), expect.stringContaining("ES2020"));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
describe("readConfigFile", () => {
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
vi.clearAllMocks();
|
|
340
|
+
});
|
|
341
|
+
it("should read and parse config file", async () => {
|
|
342
|
+
const fsPromises = (await import("fs/promises")).default;
|
|
343
|
+
vi.mocked(fsPromises.access).mockResolvedValue(undefined);
|
|
344
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('{"id": "test-app", "version": "1.0.0"}');
|
|
345
|
+
const result = await readConfigFile();
|
|
346
|
+
expect(result).toEqual({ id: "test-app", version: "1.0.0" });
|
|
347
|
+
});
|
|
348
|
+
it("should retry when file does not exist yet", async () => {
|
|
349
|
+
const fsPromises = (await import("fs/promises")).default;
|
|
350
|
+
let callCount = 0;
|
|
351
|
+
vi.mocked(fsPromises.access).mockImplementation(async () => {
|
|
352
|
+
callCount++;
|
|
353
|
+
if (callCount < 3)
|
|
354
|
+
throw new Error("ENOENT");
|
|
355
|
+
});
|
|
356
|
+
vi.mocked(fsPromises.readFile).mockResolvedValue('{"id": "test"}');
|
|
357
|
+
const result = await readConfigFile();
|
|
358
|
+
expect(result).toEqual({ id: "test" });
|
|
359
|
+
expect(callCount).toBe(3);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
describe("discoverAppSkillActions", () => {
|
|
363
|
+
beforeEach(() => {
|
|
364
|
+
vi.clearAllMocks();
|
|
365
|
+
vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
366
|
+
});
|
|
367
|
+
it("should map skill action files to entry points", () => {
|
|
368
|
+
vi.mocked(globSync).mockReturnValue([
|
|
369
|
+
"src/skill/myAction/action.ts",
|
|
370
|
+
]);
|
|
371
|
+
const result = discoverAppSkillActions();
|
|
372
|
+
expect(result).toEqual({
|
|
373
|
+
"./skill/myAction": "./src/skill/myAction/action.ts",
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
it("should return empty object when no skill files", () => {
|
|
377
|
+
vi.mocked(globSync).mockReturnValue([]);
|
|
378
|
+
const result = discoverAppSkillActions();
|
|
379
|
+
expect(result).toEqual({});
|
|
380
|
+
});
|
|
381
|
+
it("should skip files without default export", () => {
|
|
382
|
+
vi.mocked(globSync).mockReturnValue([
|
|
383
|
+
"src/skill/myAction/action.ts",
|
|
384
|
+
]);
|
|
385
|
+
mockSourceFile.getDefaultExportSymbol.mockReturnValueOnce(null);
|
|
386
|
+
const result = discoverAppSkillActions();
|
|
387
|
+
expect(result).toEqual({});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
describe("compileAppActionSkills", () => {
|
|
391
|
+
beforeEach(() => {
|
|
392
|
+
vi.clearAllMocks();
|
|
393
|
+
vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
394
|
+
});
|
|
395
|
+
it("should compile simple action with no params and no returns", () => {
|
|
396
|
+
vi.mocked(globSync).mockReturnValue([
|
|
397
|
+
"src/skill/testAction/action.ts",
|
|
398
|
+
]);
|
|
399
|
+
// Reset mockFuncDecl for this test
|
|
400
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
401
|
+
{
|
|
402
|
+
getDescription: vi.fn().mockReturnValue("A test action"),
|
|
403
|
+
getFullText: vi.fn().mockReturnValue("/** A test action */"),
|
|
404
|
+
},
|
|
405
|
+
]);
|
|
406
|
+
mockFuncDecl.getParameters.mockReturnValue([]);
|
|
407
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
408
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
409
|
+
getProperties: vi.fn().mockReturnValue([]),
|
|
410
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
411
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
412
|
+
});
|
|
413
|
+
mockFuncDecl.getKind.mockReturnValue(272);
|
|
414
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([]);
|
|
415
|
+
const config = {};
|
|
416
|
+
const result = compileAppActionSkills(config);
|
|
417
|
+
expect(result.actions).toHaveLength(1);
|
|
418
|
+
expect(result.actions[0].name).toBe("testAction");
|
|
419
|
+
expect(result.actions[0].description).toContain("test action");
|
|
420
|
+
});
|
|
421
|
+
it("should throw when action has no JSDoc description", () => {
|
|
422
|
+
vi.mocked(globSync).mockReturnValue([
|
|
423
|
+
"src/skill/noDoc/action.ts",
|
|
424
|
+
]);
|
|
425
|
+
mockFuncDecl.getJsDocs.mockReturnValue([]);
|
|
426
|
+
expect(() => compileAppActionSkills({})).toThrow("missing a JSDoc description");
|
|
427
|
+
});
|
|
428
|
+
it("should skip files without default export", () => {
|
|
429
|
+
vi.mocked(globSync).mockReturnValue([
|
|
430
|
+
"src/skill/noExport/action.ts",
|
|
431
|
+
]);
|
|
432
|
+
mockSourceFile.getDefaultExportSymbol.mockReturnValueOnce(null);
|
|
433
|
+
const config = {};
|
|
434
|
+
const result = compileAppActionSkills(config);
|
|
435
|
+
expect(result.actions).toHaveLength(0);
|
|
436
|
+
});
|
|
437
|
+
it("should skip when return type is not object", () => {
|
|
438
|
+
vi.mocked(globSync).mockReturnValue([
|
|
439
|
+
"src/skill/testAction/action.ts",
|
|
440
|
+
]);
|
|
441
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
442
|
+
{
|
|
443
|
+
getDescription: vi.fn().mockReturnValue("desc"),
|
|
444
|
+
getFullText: vi.fn().mockReturnValue("/** desc */"),
|
|
445
|
+
},
|
|
446
|
+
]);
|
|
447
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
448
|
+
isObject: vi.fn().mockReturnValue(false),
|
|
449
|
+
getProperties: vi.fn().mockReturnValue([]),
|
|
450
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
451
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
452
|
+
});
|
|
453
|
+
const config = {};
|
|
454
|
+
const result = compileAppActionSkills(config);
|
|
455
|
+
expect(result.actions).toHaveLength(0);
|
|
456
|
+
});
|
|
457
|
+
it("should throw when params exist but no input typedef", () => {
|
|
458
|
+
vi.mocked(globSync).mockReturnValue(["src/skill/testAction/action.ts"]);
|
|
459
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
460
|
+
{
|
|
461
|
+
getDescription: vi.fn().mockReturnValue("desc"),
|
|
462
|
+
getFullText: vi.fn().mockReturnValue("/** desc */"),
|
|
463
|
+
},
|
|
464
|
+
]);
|
|
465
|
+
const mockProp = {
|
|
466
|
+
getName: vi.fn().mockReturnValue("name"),
|
|
467
|
+
isOptional: vi.fn().mockReturnValue(false),
|
|
468
|
+
getTypeAtLocation: vi.fn().mockReturnValue({ getText: () => "string" }),
|
|
469
|
+
};
|
|
470
|
+
const mockParam = {
|
|
471
|
+
getNameNode: vi.fn().mockReturnValue({ getElements: vi.fn().mockReturnValue([]) }),
|
|
472
|
+
getType: vi.fn().mockReturnValue({
|
|
473
|
+
getProperties: vi.fn().mockReturnValue([mockProp]),
|
|
474
|
+
}),
|
|
475
|
+
};
|
|
476
|
+
mockFuncDecl.getParameters.mockReturnValue([mockParam]);
|
|
477
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([]); // no typedefs
|
|
478
|
+
expect(() => compileAppActionSkills({})).toThrow('missing an');
|
|
479
|
+
});
|
|
480
|
+
it("should compile action with params and input typedef", () => {
|
|
481
|
+
vi.mocked(globSync).mockReturnValue(["src/skill/testAction/action.ts"]);
|
|
482
|
+
const jsdocText = `/**
|
|
483
|
+
* A test action
|
|
484
|
+
* @typedef {Object} input
|
|
485
|
+
* @property {string} name - User name
|
|
486
|
+
*/`;
|
|
487
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
488
|
+
{
|
|
489
|
+
getDescription: vi.fn().mockReturnValue("A test action"),
|
|
490
|
+
getFullText: vi.fn().mockReturnValue(jsdocText),
|
|
491
|
+
},
|
|
492
|
+
]);
|
|
493
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([
|
|
494
|
+
{ getFullText: () => jsdocText },
|
|
495
|
+
]);
|
|
496
|
+
const mockProp = {
|
|
497
|
+
getName: vi.fn().mockReturnValue("name"),
|
|
498
|
+
isOptional: vi.fn().mockReturnValue(false),
|
|
499
|
+
getTypeAtLocation: vi.fn().mockReturnValue({ getText: () => "string" }),
|
|
500
|
+
};
|
|
501
|
+
const mockParam = {
|
|
502
|
+
getNameNode: vi.fn().mockReturnValue({ getElements: vi.fn().mockReturnValue([]) }),
|
|
503
|
+
getType: vi.fn().mockReturnValue({
|
|
504
|
+
getProperties: vi.fn().mockReturnValue([mockProp]),
|
|
505
|
+
}),
|
|
506
|
+
};
|
|
507
|
+
mockFuncDecl.getParameters.mockReturnValue([mockParam]);
|
|
508
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
509
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
510
|
+
getProperties: vi.fn().mockReturnValue([]),
|
|
511
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
512
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
513
|
+
});
|
|
514
|
+
const config = {};
|
|
515
|
+
const result = compileAppActionSkills(config);
|
|
516
|
+
expect(result.actions).toHaveLength(1);
|
|
517
|
+
expect(result.actions[0].parameters).toHaveProperty("name");
|
|
518
|
+
expect(result.actions[0].parameters.name.type).toBe("string");
|
|
519
|
+
});
|
|
520
|
+
it("should throw on missing property in input typedef", () => {
|
|
521
|
+
vi.mocked(globSync).mockReturnValue(["src/skill/testAction/action.ts"]);
|
|
522
|
+
const jsdocText = `/**
|
|
523
|
+
* A test action
|
|
524
|
+
* @typedef {Object} input
|
|
525
|
+
* @property {string} other - Other field
|
|
526
|
+
*/`;
|
|
527
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
528
|
+
{
|
|
529
|
+
getDescription: vi.fn().mockReturnValue("A test action"),
|
|
530
|
+
getFullText: vi.fn().mockReturnValue(jsdocText),
|
|
531
|
+
},
|
|
532
|
+
]);
|
|
533
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([
|
|
534
|
+
{ getFullText: () => jsdocText },
|
|
535
|
+
]);
|
|
536
|
+
const mockProp = {
|
|
537
|
+
getName: vi.fn().mockReturnValue("name"),
|
|
538
|
+
isOptional: vi.fn().mockReturnValue(false),
|
|
539
|
+
};
|
|
540
|
+
const mockParam = {
|
|
541
|
+
getNameNode: vi.fn().mockReturnValue({ getElements: vi.fn().mockReturnValue([]) }),
|
|
542
|
+
getType: vi.fn().mockReturnValue({
|
|
543
|
+
getProperties: vi.fn().mockReturnValue([mockProp]),
|
|
544
|
+
}),
|
|
545
|
+
};
|
|
546
|
+
mockFuncDecl.getParameters.mockReturnValue([mockParam]);
|
|
547
|
+
expect(() => compileAppActionSkills({})).toThrow('missing');
|
|
548
|
+
});
|
|
549
|
+
it("should compile action with return properties and output typedef", () => {
|
|
550
|
+
vi.mocked(globSync).mockReturnValue(["src/skill/testAction/action.ts"]);
|
|
551
|
+
const jsdocText = `/**
|
|
552
|
+
* A test action
|
|
553
|
+
* @typedef {Object} output
|
|
554
|
+
* @property {string} result - The result
|
|
555
|
+
*/`;
|
|
556
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
557
|
+
{
|
|
558
|
+
getDescription: vi.fn().mockReturnValue("A test action"),
|
|
559
|
+
getFullText: vi.fn().mockReturnValue(jsdocText),
|
|
560
|
+
},
|
|
561
|
+
]);
|
|
562
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([
|
|
563
|
+
{ getFullText: () => jsdocText },
|
|
564
|
+
]);
|
|
565
|
+
mockFuncDecl.getParameters.mockReturnValue([]);
|
|
566
|
+
const retProp = {
|
|
567
|
+
getName: vi.fn().mockReturnValue("result"),
|
|
568
|
+
isOptional: vi.fn().mockReturnValue(false),
|
|
569
|
+
getTypeAtLocation: vi.fn().mockReturnValue({ getText: () => "string" }),
|
|
570
|
+
};
|
|
571
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
572
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
573
|
+
getProperties: vi.fn().mockReturnValue([retProp]),
|
|
574
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
575
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
576
|
+
});
|
|
577
|
+
const config = {};
|
|
578
|
+
const result = compileAppActionSkills(config);
|
|
579
|
+
expect(result.actions).toHaveLength(1);
|
|
580
|
+
expect(result.actions[0].returns).toHaveProperty("result");
|
|
581
|
+
});
|
|
582
|
+
it("should throw on return props without output typedef (non-promise)", () => {
|
|
583
|
+
vi.mocked(globSync).mockReturnValue(["src/skill/testAction/action.ts"]);
|
|
584
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
585
|
+
{
|
|
586
|
+
getDescription: vi.fn().mockReturnValue("desc"),
|
|
587
|
+
getFullText: vi.fn().mockReturnValue("/** desc */"),
|
|
588
|
+
},
|
|
589
|
+
]);
|
|
590
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([]);
|
|
591
|
+
mockFuncDecl.getParameters.mockReturnValue([]);
|
|
592
|
+
const retProp = {
|
|
593
|
+
getName: vi.fn().mockReturnValue("result"),
|
|
594
|
+
isOptional: vi.fn().mockReturnValue(false),
|
|
595
|
+
getTypeAtLocation: vi.fn().mockReturnValue({ getText: () => "string" }),
|
|
596
|
+
};
|
|
597
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
598
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
599
|
+
getProperties: vi.fn().mockReturnValue([retProp]),
|
|
600
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
601
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
602
|
+
});
|
|
603
|
+
expect(() => compileAppActionSkills({})).toThrow('missing');
|
|
604
|
+
});
|
|
605
|
+
it("should warn and fallback for Promise return without output typedef", () => {
|
|
606
|
+
vi.mocked(globSync).mockReturnValue(["src/skill/testAction/action.ts"]);
|
|
607
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
608
|
+
{
|
|
609
|
+
getDescription: vi.fn().mockReturnValue("desc"),
|
|
610
|
+
getFullText: vi.fn().mockReturnValue("/** desc */"),
|
|
611
|
+
},
|
|
612
|
+
]);
|
|
613
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([]);
|
|
614
|
+
mockFuncDecl.getParameters.mockReturnValue([]);
|
|
615
|
+
const retProp = {
|
|
616
|
+
getName: vi.fn().mockReturnValue("data"),
|
|
617
|
+
isOptional: vi.fn().mockReturnValue(true),
|
|
618
|
+
getTypeAtLocation: vi.fn().mockReturnValue({ getText: () => "string" }),
|
|
619
|
+
};
|
|
620
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
621
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
622
|
+
getProperties: vi.fn().mockReturnValue([retProp]),
|
|
623
|
+
getSymbol: vi.fn().mockReturnValue({ getName: () => "Promise" }),
|
|
624
|
+
getTypeArguments: vi.fn().mockReturnValue([
|
|
625
|
+
{
|
|
626
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
627
|
+
getProperties: vi.fn().mockReturnValue([retProp]),
|
|
628
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
629
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
630
|
+
},
|
|
631
|
+
]),
|
|
632
|
+
});
|
|
633
|
+
const config = {};
|
|
634
|
+
const result = compileAppActionSkills(config);
|
|
635
|
+
expect(result.actions).toHaveLength(1);
|
|
636
|
+
expect(result.actions[0].returns).toHaveProperty("data");
|
|
637
|
+
expect(result.actions[0].returns.data.optional).toBe(true);
|
|
638
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Falling back to TypeScript-inferred"));
|
|
639
|
+
});
|
|
640
|
+
it("should throw on missing output property in output typedef", () => {
|
|
641
|
+
vi.mocked(globSync).mockReturnValue(["src/skill/testAction/action.ts"]);
|
|
642
|
+
const jsdocText = `/**
|
|
643
|
+
* A test action
|
|
644
|
+
* @typedef {Object} output
|
|
645
|
+
* @property {string} other - Other field
|
|
646
|
+
*/`;
|
|
647
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
648
|
+
{
|
|
649
|
+
getDescription: vi.fn().mockReturnValue("desc"),
|
|
650
|
+
getFullText: vi.fn().mockReturnValue(jsdocText),
|
|
651
|
+
},
|
|
652
|
+
]);
|
|
653
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([
|
|
654
|
+
{ getFullText: () => jsdocText },
|
|
655
|
+
]);
|
|
656
|
+
mockFuncDecl.getParameters.mockReturnValue([]);
|
|
657
|
+
const retProp = {
|
|
658
|
+
getName: vi.fn().mockReturnValue("missing"),
|
|
659
|
+
isOptional: vi.fn().mockReturnValue(false),
|
|
660
|
+
getTypeAtLocation: vi.fn().mockReturnValue({ getText: () => "string" }),
|
|
661
|
+
};
|
|
662
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
663
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
664
|
+
getProperties: vi.fn().mockReturnValue([retProp]),
|
|
665
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
666
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
667
|
+
});
|
|
668
|
+
expect(() => compileAppActionSkills({})).toThrow('return property "missing" is missing');
|
|
669
|
+
});
|
|
670
|
+
it("should throw on duplicate action names", () => {
|
|
671
|
+
vi.mocked(globSync).mockReturnValue([
|
|
672
|
+
"src/skill/testAction/action.ts",
|
|
673
|
+
"src/skill/testAction/action.ts",
|
|
674
|
+
]);
|
|
675
|
+
mockFuncDecl.getJsDocs.mockReturnValue([
|
|
676
|
+
{
|
|
677
|
+
getDescription: vi.fn().mockReturnValue("desc"),
|
|
678
|
+
getFullText: vi.fn().mockReturnValue("/** desc */"),
|
|
679
|
+
},
|
|
680
|
+
]);
|
|
681
|
+
mockFuncDecl.getParameters.mockReturnValue([]);
|
|
682
|
+
mockFuncDecl.getReturnType.mockReturnValue({
|
|
683
|
+
isObject: vi.fn().mockReturnValue(true),
|
|
684
|
+
getProperties: vi.fn().mockReturnValue([]),
|
|
685
|
+
getSymbol: vi.fn().mockReturnValue(null),
|
|
686
|
+
getTypeArguments: vi.fn().mockReturnValue([]),
|
|
687
|
+
});
|
|
688
|
+
mockSourceFile.getDescendantsOfKind.mockReturnValue([]);
|
|
689
|
+
expect(() => compileAppActionSkills({})).toThrow('Duplicate action name');
|
|
690
|
+
});
|
|
691
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-editor/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pulse": "dist/cli.js"
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsx build.ts",
|
|
14
14
|
"dev": "tsx watch --include \"./src/**/*\" build.ts",
|
|
15
|
-
"test": "
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:coverage": "vitest run --coverage",
|
|
16
17
|
"link": "npm link",
|
|
17
18
|
"pack": "npm run build && npm pack --pack-destination dist"
|
|
18
19
|
},
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"@types/livereload": "^0.9.5",
|
|
59
60
|
"@types/react": "^19.2.14",
|
|
60
61
|
"@types/react-dom": "^19.2.3",
|
|
62
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
61
63
|
"ava": "^6.4.1",
|
|
62
64
|
"chalk": "^5.6.2",
|
|
63
65
|
"eslint-config-xo-react": "^0.29.0",
|
|
@@ -68,6 +70,7 @@
|
|
|
68
70
|
"react-dom": "19.2.4",
|
|
69
71
|
"ts-node": "^10.9.2",
|
|
70
72
|
"typescript": "^5.9.3",
|
|
73
|
+
"vitest": "^4.1.2",
|
|
71
74
|
"xo": "^1.2.3"
|
|
72
75
|
},
|
|
73
76
|
"peerDependencies": {
|