@simplysm/core-node 13.0.69 → 13.0.71

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.
@@ -0,0 +1,192 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import path from "path";
3
+ import {
4
+ pathPosix,
5
+ pathNorm,
6
+ pathIsChildPath,
7
+ pathChangeFileDirectory,
8
+ pathBasenameWithoutExt,
9
+ pathFilterByTargets,
10
+ type NormPath,
11
+ } from "../../src/utils/path";
12
+
13
+ describe("path functions", () => {
14
+ //#region posix
15
+
16
+ describe("pathPosix", () => {
17
+ it("converts single path argument to POSIX style", () => {
18
+ const result = pathPosix("C:\\Users\\test\\file.txt");
19
+ expect(result).toBe("C:/Users/test/file.txt");
20
+ });
21
+
22
+ it("combines multiple path arguments and converts to POSIX style", () => {
23
+ const result = pathPosix("C:\\Users", "test", "file.txt");
24
+ expect(result).toBe("C:/Users/test/file.txt");
25
+ });
26
+
27
+ it("keeps already POSIX-style path as is", () => {
28
+ const result = pathPosix("/usr/local/bin");
29
+ expect(result).toBe("/usr/local/bin");
30
+ });
31
+
32
+ it("handles mixed path separators", () => {
33
+ const result = pathPosix("C:/Users\\test/file.txt");
34
+ expect(result).toBe("C:/Users/test/file.txt");
35
+ });
36
+ });
37
+
38
+ //#endregion
39
+
40
+ //#region norm
41
+
42
+ describe("pathNorm", () => {
43
+ it("normalizes path and returns NormPath type", () => {
44
+ const result: NormPath = pathNorm("./test/../file.txt");
45
+ expect(result).toBe(path.resolve("./test/../file.txt"));
46
+ });
47
+
48
+ it("combines multiple path arguments and normalizes", () => {
49
+ const basePath = path.resolve("/base");
50
+ const result = pathNorm(basePath, "sub", "file.txt");
51
+ expect(result).toBe(path.resolve(basePath, "sub", "file.txt"));
52
+ });
53
+
54
+ it("converts relative path to absolute path", () => {
55
+ const result = pathNorm("relative/path");
56
+ expect(path.isAbsolute(result)).toBe(true);
57
+ });
58
+ });
59
+
60
+ //#endregion
61
+
62
+ //#region isChildPath
63
+
64
+ describe("pathIsChildPath", () => {
65
+ it("returns true for child path", () => {
66
+ const parent = pathNorm("/parent/dir");
67
+ const child = pathNorm("/parent/dir/child/file.txt");
68
+ expect(pathIsChildPath(child, parent)).toBe(true);
69
+ });
70
+
71
+ it("returns false for same path", () => {
72
+ const parent = pathNorm("/parent/dir");
73
+ const child = pathNorm("/parent/dir");
74
+ expect(pathIsChildPath(child, parent)).toBe(false);
75
+ });
76
+
77
+ it("returns false for non-child path", () => {
78
+ const parent = pathNorm("/parent/dir");
79
+ const child = pathNorm("/other/dir/file.txt");
80
+ expect(pathIsChildPath(child, parent)).toBe(false);
81
+ });
82
+
83
+ it("returns false when only part of parent path matches", () => {
84
+ const parent = pathNorm("/parent/dir");
85
+ const child = pathNorm("/parent/directory/file.txt");
86
+ expect(pathIsChildPath(child, parent)).toBe(false);
87
+ });
88
+ });
89
+
90
+ //#endregion
91
+
92
+ //#region changeFileDirectory
93
+
94
+ describe("pathChangeFileDirectory", () => {
95
+ it("changes file directory", () => {
96
+ const file = pathNorm("/source/sub/file.txt");
97
+ const from = pathNorm("/source");
98
+ const to = pathNorm("/target");
99
+
100
+ const result = pathChangeFileDirectory(file, from, to);
101
+ expect(result).toBe(pathNorm("/target/sub/file.txt"));
102
+ });
103
+
104
+ it("changes directory in nested path", () => {
105
+ const file = pathNorm("/a/b/c/d/file.txt");
106
+ const from = pathNorm("/a/b");
107
+ const to = pathNorm("/x/y");
108
+
109
+ const result = pathChangeFileDirectory(file, from, to);
110
+ expect(result).toBe(pathNorm("/x/y/c/d/file.txt"));
111
+ });
112
+
113
+ it("throws error when file is not inside fromDirectory", () => {
114
+ const file = pathNorm("/other/path/file.txt");
115
+ const from = pathNorm("/source");
116
+ const to = pathNorm("/target");
117
+
118
+ expect(() => pathChangeFileDirectory(file, from, to)).toThrow();
119
+ });
120
+
121
+ it("returns toDirectory when filePath and fromDirectory are the same", () => {
122
+ const file = pathNorm("/source");
123
+ const from = pathNorm("/source");
124
+ const to = pathNorm("/target");
125
+
126
+ const result = pathChangeFileDirectory(file, from, to);
127
+ expect(result).toBe(to);
128
+ });
129
+ });
130
+
131
+ //#endregion
132
+
133
+ //#region basenameWithoutExt
134
+
135
+ describe("pathBasenameWithoutExt", () => {
136
+ it("removes single extension (returns basename only)", () => {
137
+ const result = pathBasenameWithoutExt("/path/to/file.txt");
138
+ expect(result).toBe("file");
139
+ });
140
+
141
+ it("removes only last extension in multiple extensions", () => {
142
+ const result = pathBasenameWithoutExt("/path/to/file.spec.ts");
143
+ expect(result).toBe("file.spec");
144
+ });
145
+
146
+ it("returns basename for file without extension", () => {
147
+ const result = pathBasenameWithoutExt("/path/to/file");
148
+ expect(result).toBe("file");
149
+ });
150
+
151
+ it("returns hidden file (starting with dot) as is", () => {
152
+ const result = pathBasenameWithoutExt("/path/to/.gitignore");
153
+ expect(result).toBe(".gitignore");
154
+ });
155
+ });
156
+
157
+ //#endregion
158
+
159
+ //#region filterByTargets
160
+
161
+ describe("pathFilterByTargets", () => {
162
+ const cwd = "/proj";
163
+ const files = ["/proj/src/a.ts", "/proj/src/b.ts", "/proj/tests/c.ts", "/proj/lib/d.ts"];
164
+
165
+ it("returns all files if targets array is empty", () => {
166
+ const result = pathFilterByTargets(files, [], cwd);
167
+ expect(result).toEqual(files);
168
+ });
169
+
170
+ it("filters by single target", () => {
171
+ const result = pathFilterByTargets(files, ["src"], cwd);
172
+ expect(result).toEqual(["/proj/src/a.ts", "/proj/src/b.ts"]);
173
+ });
174
+
175
+ it("filters by multiple targets", () => {
176
+ const result = pathFilterByTargets(files, ["src", "tests"], cwd);
177
+ expect(result).toEqual(["/proj/src/a.ts", "/proj/src/b.ts", "/proj/tests/c.ts"]);
178
+ });
179
+
180
+ it("returns empty array when no matching file is found", () => {
181
+ const result = pathFilterByTargets(files, ["nonexistent"], cwd);
182
+ expect(result).toEqual([]);
183
+ });
184
+
185
+ it("filters by exact file path", () => {
186
+ const result = pathFilterByTargets(files, ["src/a.ts"], cwd);
187
+ expect(result).toEqual(["/proj/src/a.ts"]);
188
+ });
189
+ });
190
+
191
+ //#endregion
192
+ });
@@ -0,0 +1,35 @@
1
+ import { createWorker } from "../../../src";
2
+
3
+ interface TestWorkerEvents extends Record<string, unknown> {
4
+ progress: number;
5
+ }
6
+
7
+ const methods = {
8
+ add: (a: number, b: number) => {
9
+ sender.send("progress", 50);
10
+ return a + b;
11
+ },
12
+ echo: (message: string) => `Echo: ${message}`,
13
+ throwError: () => {
14
+ throw new Error("Intentional error");
15
+ },
16
+ delay: async (ms: number) => {
17
+ await new Promise((resolve) => setTimeout(resolve, ms));
18
+ return ms;
19
+ },
20
+ noReturn: () => {},
21
+ logMessage: (message: string) => {
22
+ console.log(message);
23
+ return "logged";
24
+ },
25
+ crash: () => {
26
+ // Exit immediately
27
+ setImmediate(() => process.exit(1));
28
+ return "crashing...";
29
+ },
30
+ getEnv: (key: string) => process.env[key],
31
+ };
32
+
33
+ const sender = createWorker<typeof methods, TestWorkerEvents>(methods);
34
+
35
+ export default sender;
@@ -0,0 +1,189 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import path from "path";
3
+ import type { WorkerProxy } from "../../src/worker/types";
4
+ import { Worker } from "../../src/worker/worker";
5
+ import type * as TestWorkerModule from "./fixtures/test-worker";
6
+
7
+ describe("SdWorker", () => {
8
+ const workerPath = path.resolve(import.meta.dirname, "fixtures/test-worker.ts");
9
+ let worker: WorkerProxy<typeof TestWorkerModule> | undefined;
10
+
11
+ afterEach(async () => {
12
+ if (worker) {
13
+ await worker.terminate();
14
+ worker = undefined;
15
+ }
16
+ });
17
+
18
+ //#region Method invocation
19
+
20
+ describe("method invocation", () => {
21
+ it("calls worker method and returns result", async () => {
22
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
23
+
24
+ const result = await worker.add(10, 20);
25
+
26
+ expect(result).toBe(30);
27
+ });
28
+
29
+ it("calls method that returns string", async () => {
30
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
31
+
32
+ const result = await worker.echo("Hello");
33
+
34
+ expect(result).toBe("Echo: Hello");
35
+ });
36
+
37
+ it("rejects when error occurs in worker", async () => {
38
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
39
+
40
+ await expect(worker.throwError()).rejects.toThrow();
41
+ });
42
+
43
+ it("throws error when calling nonexistent method", async () => {
44
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
45
+
46
+ // Bypass type system to call nonexistent method
47
+ const unknownWorker = worker as unknown as { unknownMethod: () => Promise<void> };
48
+
49
+ await expect(unknownWorker.unknownMethod()).rejects.toThrow(
50
+ "Unknown method: unknownMethod",
51
+ );
52
+ });
53
+
54
+ it("handles multiple concurrent requests", async () => {
55
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
56
+
57
+ const [result1, result2, result3] = await Promise.all([
58
+ worker.add(1, 2),
59
+ worker.add(3, 4),
60
+ worker.add(5, 6),
61
+ ]);
62
+
63
+ expect(result1).toBe(3);
64
+ expect(result2).toBe(7);
65
+ expect(result3).toBe(11);
66
+ });
67
+
68
+ it("calls method that returns void", async () => {
69
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
70
+
71
+ const result = await worker.noReturn();
72
+
73
+ expect(result).toBeUndefined();
74
+ });
75
+ });
76
+
77
+ //#endregion
78
+
79
+ //#region Events
80
+
81
+ describe("events", () => {
82
+ it("receives events from worker", async () => {
83
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
84
+
85
+ const events: number[] = [];
86
+ worker.on("progress", (value) => {
87
+ events.push(value);
88
+ });
89
+
90
+ await worker.add(1, 2);
91
+
92
+ expect(events).toContain(50);
93
+ });
94
+
95
+ it("removes event listener with off()", async () => {
96
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
97
+
98
+ const events: number[] = [];
99
+ const listener = (value: number) => {
100
+ events.push(value);
101
+ };
102
+
103
+ worker.on("progress", listener);
104
+ await worker.add(1, 2);
105
+
106
+ // Remove listener
107
+ worker.off("progress", listener);
108
+ await worker.add(3, 4);
109
+
110
+ // Should only receive event from first call
111
+ expect(events).toHaveLength(1);
112
+ expect(events[0]).toBe(50);
113
+ });
114
+ });
115
+
116
+ //#endregion
117
+
118
+ //#region Terminate
119
+
120
+ describe("terminate", () => {
121
+ it("rejects pending requests with Worker terminated error", async () => {
122
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
123
+
124
+ const runPromise = worker.delay(5000);
125
+
126
+ // Prepare to catch error before terminating worker
127
+ const errorPromise = runPromise.catch((err: unknown) => err);
128
+
129
+ // Terminate worker
130
+ await worker.terminate();
131
+ worker = undefined;
132
+
133
+ const error = await errorPromise;
134
+ expect(error).toBeInstanceOf(Error);
135
+ expect((error as Error).message).toBe("Worker terminated (method: delay)");
136
+ });
137
+
138
+ it("rejects pending requests when worker crashes", async () => {
139
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
140
+
141
+ // Call long delay first to put it in pending state
142
+ const delayPromise = worker.delay(5000).catch((err: unknown) => err);
143
+
144
+ // Call crash after short wait (while delay is pending)
145
+ await new Promise((resolve) => setTimeout(resolve, 10));
146
+ await worker.crash();
147
+
148
+ // When worker crashes, pending delay request should be rejected
149
+ const error = await delayPromise;
150
+ expect(error).toBeInstanceOf(Error);
151
+ expect((error as Error).message).toContain("Worker crashed");
152
+ });
153
+ });
154
+
155
+ //#endregion
156
+
157
+ //#region stdout/stderr
158
+
159
+ describe("stdout/stderr", () => {
160
+ it("forwards worker console.log output to main process", async () => {
161
+ worker = Worker.create<typeof TestWorkerModule>(workerPath);
162
+
163
+ const result = await worker.logMessage("test message");
164
+
165
+ // If method returns normally, stdout piping is working
166
+ expect(result).toBe("logged");
167
+ });
168
+ });
169
+
170
+ //#endregion
171
+
172
+ //#region env option
173
+
174
+ describe("env option", () => {
175
+ it("passes env option to worker", async () => {
176
+ worker = Worker.create<typeof TestWorkerModule>(workerPath, {
177
+ env: {
178
+ TEST_ENV_VAR: "test-value-123",
179
+ },
180
+ });
181
+
182
+ const result = await worker.getEnv("TEST_ENV_VAR");
183
+
184
+ expect(result).toBe("test-value-123");
185
+ });
186
+ });
187
+
188
+ //#endregion
189
+ });