@mux-magic/tools 0.1.0

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,90 @@
1
+ import { join } from "node:path"
2
+ import { vol } from "memfs"
3
+ import { EmptyError, firstValueFrom, toArray } from "rxjs"
4
+ import { beforeEach, describe, expect, test } from "vitest"
5
+ import { captureLogMessage } from "./captureLogMessage.js"
6
+ import {
7
+ type FolderInfo,
8
+ filterFolderAtPath,
9
+ getFolder,
10
+ } from "./getFolder.js"
11
+ import { getOperatorValue } from "./test-runners.js"
12
+
13
+ describe(filterFolderAtPath.name, () => {
14
+ beforeEach(() => {
15
+ vol.fromJSON({
16
+ "/movies/Super Mario Bros (1993)/Super Mario Bros (1993).mkv":
17
+ "",
18
+ })
19
+ })
20
+
21
+ test("emits if path is a directory", async () => {
22
+ const inputValue = "/movies/Super Mario Bros (1993)"
23
+
24
+ await expect(
25
+ getOperatorValue(
26
+ filterFolderAtPath((filePath) => filePath),
27
+ inputValue,
28
+ ),
29
+ ).resolves.toBe(inputValue)
30
+ })
31
+
32
+ test("throws an error if path is a file", async () => {
33
+ const inputValue =
34
+ "/movies/Super Mario Bros (1993)/Super Mario Bros (1993).mkv"
35
+
36
+ await expect(
37
+ getOperatorValue(
38
+ filterFolderAtPath((filePath) => filePath),
39
+ inputValue,
40
+ ),
41
+ ).rejects.toThrow(EmptyError)
42
+ })
43
+ })
44
+
45
+ describe(getFolder.name, () => {
46
+ test("errors if source path can't be found", async () => {
47
+ await captureLogMessage("error", async () => {
48
+ await expect(
49
+ firstValueFrom(
50
+ getFolder({
51
+ sourcePath: "non-existent-path",
52
+ }),
53
+ ),
54
+ ).rejects.toThrow("ENOENT")
55
+ })
56
+ })
57
+
58
+ test("emits folders from source path", async () => {
59
+ vol.fromJSON({
60
+ "/movies/Star Wars (1977)/Star Wars (1977).mkv": "",
61
+ "/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv":
62
+ "",
63
+ "/movies/Super Mario Bros (1993)/Super Mario Bros (1993).mkv":
64
+ "",
65
+ })
66
+
67
+ const expected: FolderInfo[] = [
68
+ {
69
+ folderName: "Star Wars (1977)",
70
+ fullPath: join("/movies", "Star Wars (1977)"),
71
+ renameFolder: expect.any(Function),
72
+ },
73
+ {
74
+ folderName: "Super Mario Bros (1993)",
75
+ fullPath: join(
76
+ "/movies",
77
+ "Super Mario Bros (1993)",
78
+ ),
79
+ renameFolder: expect.any(Function),
80
+ },
81
+ ]
82
+ const actual = await firstValueFrom(
83
+ getFolder({
84
+ sourcePath: "/movies",
85
+ }).pipe(toArray()),
86
+ )
87
+ expect(actual).toEqual(expect.arrayContaining(expected))
88
+ expect(actual).toHaveLength(expected.length)
89
+ })
90
+ })
@@ -0,0 +1,74 @@
1
+ import { readdir, stat } from "node:fs/promises"
2
+ import { join } from "node:path"
3
+ import {
4
+ catchError,
5
+ concatAll,
6
+ concatMap,
7
+ defer,
8
+ EMPTY,
9
+ filter,
10
+ from,
11
+ map,
12
+ type Observable,
13
+ type OperatorFunction,
14
+ } from "rxjs"
15
+
16
+ import { createRenameFileOrFolderObservable } from "./createRenameFileOrFolder.js"
17
+ import { logAndRethrowPipelineError } from "./logAndRethrowPipelineError.js"
18
+
19
+ export type FolderInfo = {
20
+ folderName: string
21
+ fullPath: string
22
+ renameFolder: (newFolderName: string) => Observable<{
23
+ newPath: string
24
+ oldPath: string
25
+ }>
26
+ }
27
+
28
+ export const getIsFolder = (folderPath: string) =>
29
+ from(stat(folderPath)).pipe(
30
+ filter((stats) => stats.isDirectory()),
31
+ )
32
+
33
+ export const filterFolderAtPath = <PipelineValue>(
34
+ getFullPath: (pipelineValue: PipelineValue) => string,
35
+ ): OperatorFunction<PipelineValue, PipelineValue> =>
36
+ concatMap((pipelineValue) =>
37
+ from(stat(getFullPath(pipelineValue))).pipe(
38
+ filter((stats) => stats.isDirectory()),
39
+ map(() => pipelineValue),
40
+ catchError((error) => {
41
+ if (error.code === "ENOENT") {
42
+ return EMPTY
43
+ }
44
+
45
+ throw error
46
+ }),
47
+ ),
48
+ )
49
+
50
+ export const getFolder = ({
51
+ sourcePath,
52
+ }: {
53
+ sourcePath: string
54
+ }): Observable<FolderInfo> =>
55
+ defer(() => readdir(sourcePath)).pipe(
56
+ concatAll(),
57
+ map((folderName) => ({
58
+ folderName,
59
+ fullPath: join(sourcePath, folderName),
60
+ })),
61
+ filterFolderAtPath(({ fullPath }) => fullPath),
62
+ map(
63
+ ({ folderName, fullPath }) =>
64
+ ({
65
+ folderName,
66
+ fullPath,
67
+ renameFolder: createRenameFileOrFolderObservable({
68
+ fullPath,
69
+ sourcePath,
70
+ }),
71
+ }) satisfies FolderInfo as FolderInfo,
72
+ ),
73
+ logAndRethrowPipelineError(getFolder),
74
+ )
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ // Public entry for @mux-magic/tools. THIS IS THE ONLY ALLOWED BARREL FILE
2
+ // in the entire repository — see AGENTS.md "Module exports — no barrel files".
3
+ // It exists because external consumers (the Gallery-Downloader sibling repo,
4
+ // plus any future npm consumers) need a single stable import path; without it
5
+ // every release would re-publish the package's internal file layout into
6
+ // consumer code.
7
+ //
8
+ // Inside this monorepo, never import from "@mux-magic/tools" — import the
9
+ // individual file directly (e.g. "@mux-magic/tools/src/naturalSort").
10
+
11
+ export {
12
+ aclSafeCopyFile,
13
+ type CopyOptions,
14
+ type CopyProgressEvent,
15
+ } from "./aclSafeCopyFile.js"
16
+ export { addFolderNameBeforeFilename } from "./addFolderNameBeforeFilename.js"
17
+ export { captureConsoleMessage } from "./captureConsoleMessage.js"
18
+ export { captureLogMessage } from "./captureLogMessage.js"
19
+ export { cleanupFilename } from "./cleanupFilename.js"
20
+ export {
21
+ createRenameFileOrFolderObservable,
22
+ getLastItemInFilePath,
23
+ renameFileOrFolder,
24
+ } from "./createRenameFileOrFolder.js"
25
+ export {
26
+ type FileInfo,
27
+ filterFileAtPath,
28
+ getFiles,
29
+ } from "./getFiles.js"
30
+ export { getFilesAtDepth } from "./getFilesAtDepth.js"
31
+ export {
32
+ type FolderInfo,
33
+ filterFolderAtPath,
34
+ getFolder,
35
+ getIsFolder,
36
+ } from "./getFolder.js"
37
+ export { insertIntoArray } from "./insertIntoArray.js"
38
+ export {
39
+ type DirectoryEntry,
40
+ type ListDirectoryEntriesResult,
41
+ listDirectoryEntries,
42
+ } from "./listDirectoryEntries.js"
43
+ export { logAndRethrowPipelineError } from "./logAndRethrowPipelineError.js"
44
+ export { logAndSwallowPipelineError } from "./logAndSwallowPipelineError.js"
45
+ export {
46
+ createAddColorToChalk,
47
+ createLogMessage,
48
+ logError,
49
+ logInfo,
50
+ logWarning,
51
+ messageTemplate,
52
+ } from "./logMessage.js"
53
+ export { makeDirectory } from "./makeDirectory.js"
54
+ export { naturalSort } from "./naturalSort.js"
55
+ export { replaceFileExtension } from "./replaceFileExtension.js"
56
+ export {
57
+ getOperatorValue,
58
+ runPromiseScheduler,
59
+ runTestScheduler,
60
+ } from "./test-runners.js"
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "vitest"
2
+
3
+ import { insertIntoArray } from "./insertIntoArray.js"
4
+
5
+ describe(insertIntoArray.name, () => {
6
+ test("inserts at the beginning", async () => {
7
+ expect(
8
+ insertIntoArray({
9
+ array: [1, 2, 3],
10
+ index: 0,
11
+ value: 0,
12
+ }),
13
+ ).toEqual([0, 1, 2, 3])
14
+ })
15
+
16
+ test("inserts after the first value", async () => {
17
+ expect(
18
+ insertIntoArray({
19
+ array: [1, 2, 3],
20
+ index: 1,
21
+ value: 0,
22
+ }),
23
+ ).toEqual([1, 0, 2, 3])
24
+ })
25
+
26
+ test("inserts at the end", async () => {
27
+ expect(
28
+ insertIntoArray({
29
+ array: [1, 2, 3],
30
+ index: 3,
31
+ value: 0,
32
+ }),
33
+ ).toEqual([1, 2, 3, 0])
34
+ })
35
+ })
@@ -0,0 +1,10 @@
1
+ export const insertIntoArray = <Value>({
2
+ array,
3
+ index,
4
+ value,
5
+ }: {
6
+ array: Value[]
7
+ index: number
8
+ value: Value
9
+ }) =>
10
+ array.slice(0, index).concat(value, array.slice(index))
@@ -0,0 +1,56 @@
1
+ import { readdir, stat } from "node:fs/promises"
2
+ import {
3
+ dirname,
4
+ sep as nativePathSeparator,
5
+ } from "node:path"
6
+ import { from, type Observable } from "rxjs"
7
+
8
+ export type DirectoryEntry = {
9
+ isDirectory: boolean
10
+ name: string
11
+ }
12
+
13
+ export type ListDirectoryEntriesResult = {
14
+ entries: DirectoryEntry[]
15
+ separator: string
16
+ }
17
+
18
+ // One-shot directory listing for the path-field typeahead.
19
+ //
20
+ // Returns both the entries and the OS-native path separator ("\\" on Windows,
21
+ // "/" on Linux/macOS) so the client can join paths with the right separator
22
+ // regardless of what the user has typed so far.
23
+ //
24
+ // If `path` is a file, lists its parent directory instead — the typeahead
25
+ // flow expects siblings of a partially-typed file path to surface.
26
+ // Errors (path missing, permission denied, etc.) propagate so the calling
27
+ // route can package them into the response's optional `error` field.
28
+ export const listDirectoryEntries = (
29
+ path: string,
30
+ ): Observable<ListDirectoryEntriesResult> =>
31
+ from(
32
+ (async () => {
33
+ let lookupPath = path
34
+ try {
35
+ const stats = await stat(path)
36
+ if (!stats.isDirectory()) {
37
+ lookupPath = dirname(path)
38
+ }
39
+ } catch {
40
+ // Path doesn't exist as-is; let readdir below decide whether the
41
+ // parent works (and surface the actual error message if not).
42
+ lookupPath = dirname(path)
43
+ }
44
+
45
+ const entries = await readdir(lookupPath, {
46
+ withFileTypes: true,
47
+ })
48
+ return {
49
+ entries: entries.map((entry) => ({
50
+ isDirectory: entry.isDirectory(),
51
+ name: entry.name,
52
+ })),
53
+ separator: nativePathSeparator,
54
+ }
55
+ })(),
56
+ )
@@ -0,0 +1,53 @@
1
+ import { throwError } from "rxjs"
2
+ import { describe, expect, test } from "vitest"
3
+
4
+ import { captureLogMessage } from "./captureLogMessage.js"
5
+ import { logAndRethrowPipelineError } from "./logAndRethrowPipelineError.js"
6
+ import { runTestScheduler } from "./test-runners.js"
7
+
8
+ describe(logAndRethrowPipelineError.name, () => {
9
+ test("logs the error and re-emits it so downstream catchError handlers fire", async () => {
10
+ captureLogMessage("error", (logMessageSpy) => {
11
+ runTestScheduler(({ expectObservable }) => {
12
+ // "#" = error notification (vs. "|" for clean complete).
13
+ // logAndSwallowPipelineError returns "|";
14
+ // logAndRethrowPipelineError must return "#" with the original
15
+ // error preserved.
16
+ expectObservable(
17
+ throwError(() => "test error").pipe(
18
+ logAndRethrowPipelineError("testFunction"),
19
+ ),
20
+ ).toBe("#", undefined, "test error")
21
+ })
22
+
23
+ expect(logMessageSpy).toHaveBeenCalledOnce()
24
+
25
+ expect(
26
+ logMessageSpy.mock.calls[0].find((text) =>
27
+ text.includes("testFunction"),
28
+ ),
29
+ ).toContain("testFunction")
30
+ })
31
+ })
32
+
33
+ test("logs an error buffer and re-emits the original error", async () => {
34
+ captureLogMessage("error", (logMessageSpy) => {
35
+ runTestScheduler(({ expectObservable }) => {
36
+ const errorBuffer = Buffer.from("test error")
37
+ expectObservable(
38
+ throwError(() => errorBuffer).pipe(
39
+ logAndRethrowPipelineError("testFunction"),
40
+ ),
41
+ ).toBe("#", undefined, errorBuffer)
42
+ })
43
+
44
+ expect(logMessageSpy).toHaveBeenCalledOnce()
45
+
46
+ expect(
47
+ logMessageSpy.mock.calls[0].find((text) =>
48
+ text.includes("test error"),
49
+ ),
50
+ ).toBe("test error")
51
+ })
52
+ })
53
+ })
@@ -0,0 +1,26 @@
1
+ import {
2
+ catchError,
3
+ type OperatorFunction,
4
+ throwError,
5
+ } from "rxjs"
6
+ import { logError } from "./logMessage.js"
7
+
8
+ // Logs the error under `func`'s name (or the provided string label) and
9
+ // re-emits it as an observable error so the downstream catchError handlers
10
+ // in jobRunner / sequenceRunner can flip the job's status to "failed".
11
+ // Use this at the OUTER terminal pipe of an app-command. For INNER pipes
12
+ // that should skip a broken item and continue the batch, use
13
+ // `logAndSwallowPipelineError` instead.
14
+ export const logAndRethrowPipelineError = <PipelineValue>(
15
+ func: { name: string } | string,
16
+ ): OperatorFunction<PipelineValue, PipelineValue> =>
17
+ catchError((error) => {
18
+ logError(
19
+ typeof func === "string" ? func : func.name,
20
+ Buffer.isBuffer(error)
21
+ ? error.toString("utf8")
22
+ : error,
23
+ )
24
+
25
+ return throwError(() => error)
26
+ })
@@ -0,0 +1,60 @@
1
+ import { throwError } from "rxjs"
2
+ import { describe, expect, test } from "vitest"
3
+
4
+ import { captureLogMessage } from "./captureLogMessage.js"
5
+ import { logAndSwallowPipelineError } from "./logAndSwallowPipelineError.js"
6
+ import { runTestScheduler } from "./test-runners.js"
7
+
8
+ describe(logAndSwallowPipelineError.name, () => {
9
+ test("catches a pipeline error and completes cleanly", async () => {
10
+ captureLogMessage("error", (logMessageSpy) => {
11
+ runTestScheduler(({ expectObservable }) => {
12
+ expectObservable(
13
+ throwError(() => "test error").pipe(
14
+ logAndSwallowPipelineError("testFunction"),
15
+ ),
16
+ ).toBe("|")
17
+ })
18
+
19
+ expect(logMessageSpy).toHaveBeenCalledOnce()
20
+
21
+ expect(
22
+ logMessageSpy.mock.calls[0].find((text) =>
23
+ text.includes("testFunction"),
24
+ ),
25
+ ).toContain("testFunction")
26
+
27
+ expect(
28
+ logMessageSpy.mock.calls[0].find((text) =>
29
+ text.includes("test error"),
30
+ ),
31
+ ).toContain("test error")
32
+ })
33
+ })
34
+
35
+ test("logs an error buffer", async () => {
36
+ captureLogMessage("error", (logMessageSpy) => {
37
+ runTestScheduler(({ expectObservable }) => {
38
+ expectObservable(
39
+ throwError(() => Buffer.from("test error")).pipe(
40
+ logAndSwallowPipelineError("testFunction"),
41
+ ),
42
+ ).toBe("|")
43
+ })
44
+
45
+ expect(logMessageSpy).toHaveBeenCalledOnce()
46
+
47
+ expect(
48
+ logMessageSpy.mock.calls[0].find((text) =>
49
+ text.includes("testFunction"),
50
+ ),
51
+ ).toContain("testFunction")
52
+
53
+ expect(
54
+ logMessageSpy.mock.calls[0].find((text) =>
55
+ text.includes("test error"),
56
+ ),
57
+ ).toBe("test error")
58
+ })
59
+ })
60
+ })
@@ -0,0 +1,26 @@
1
+ import {
2
+ catchError,
3
+ EMPTY,
4
+ type OperatorFunction,
5
+ } from "rxjs"
6
+ import { logError } from "./logMessage.js"
7
+
8
+ // Logs the error under `func`'s name (or the provided string label) and
9
+ // completes the observable with no emissions. Use this in INNER pipes —
10
+ // per-file `mergeMap`/`concatMap` callbacks where one bad item shouldn't
11
+ // abort the rest of the batch (e.g. spawn-op wrappers, per-file fetch
12
+ // helpers). For OUTER terminal pipes that need the error to reach the
13
+ // runner, use `logAndRethrowPipelineError` instead.
14
+ export const logAndSwallowPipelineError = <PipelineValue>(
15
+ func: { name: string } | string,
16
+ ): OperatorFunction<PipelineValue, PipelineValue> =>
17
+ catchError((error) => {
18
+ logError(
19
+ typeof func === "string" ? func : func.name,
20
+ Buffer.isBuffer(error)
21
+ ? error.toString("utf8")
22
+ : error,
23
+ )
24
+
25
+ return EMPTY
26
+ })
@@ -0,0 +1,207 @@
1
+ import { Chalk } from "chalk"
2
+ import { describe, expect, test } from "vitest"
3
+ import { captureConsoleMessage } from "./captureConsoleMessage.js"
4
+ import {
5
+ createAddColorToChalk,
6
+ createLogMessage,
7
+ logError,
8
+ logInfo,
9
+ logWarning,
10
+ messageTemplate,
11
+ } from "./logMessage.js"
12
+
13
+ describe(createAddColorToChalk.name, () => {
14
+ test("adds no colors when none passed", async () => {
15
+ const chalk = new Chalk()
16
+
17
+ const modifiedChalk = createAddColorToChalk()(chalk)
18
+
19
+ expect(modifiedChalk("Hello World!")).toBe(
20
+ "Hello World!",
21
+ )
22
+ })
23
+
24
+ test("adds text color", async () => {
25
+ const chalk = new Chalk()
26
+
27
+ const modifiedChalk =
28
+ createAddColorToChalk("white")(chalk)
29
+
30
+ expect(modifiedChalk("Hello World!")).toBe(
31
+ chalk.white("Hello World!"),
32
+ )
33
+ })
34
+
35
+ test("adds a background color", async () => {
36
+ const chalk = new Chalk()
37
+
38
+ const modifiedChalk =
39
+ createAddColorToChalk("bgWhite")(chalk)
40
+
41
+ expect(modifiedChalk("Hello World!")).toBe(
42
+ chalk.bgWhite("Hello World!"),
43
+ )
44
+ })
45
+
46
+ test("adds both text and background colors", async () => {
47
+ const chalk = new Chalk()
48
+
49
+ const modifiedChalk = createAddColorToChalk("bgWhite")(
50
+ createAddColorToChalk("black")(chalk),
51
+ )
52
+
53
+ expect(modifiedChalk("Hello World!")).toBe(
54
+ chalk.black.bgWhite("Hello World!"),
55
+ )
56
+ })
57
+ })
58
+
59
+ describe("messageTemplate", () => {
60
+ test(messageTemplate.comparison.name, () => {
61
+ expect(
62
+ messageTemplate.comparison("old.mkv", "new.mkv"),
63
+ ).toEqual(["old.mkv", "\n", "new.mkv"])
64
+ })
65
+
66
+ test(messageTemplate.descriptiveComparison.name, () => {
67
+ expect(
68
+ messageTemplate.descriptiveComparison(
69
+ 12345,
70
+ "old.mkv",
71
+ "new.mkv",
72
+ ),
73
+ ).toEqual([
74
+ 12345,
75
+ "\n",
76
+ "\n",
77
+ "old.mkv",
78
+ "\n",
79
+ "new.mkv",
80
+ ])
81
+ })
82
+
83
+ test(messageTemplate.noItems.name, () => {
84
+ expect(messageTemplate.noItems()).toEqual([])
85
+ })
86
+
87
+ test(messageTemplate.singleItem.name, () => {
88
+ expect(messageTemplate.singleItem("new.mkv")).toEqual([
89
+ "new.mkv",
90
+ ])
91
+ })
92
+
93
+ test(messageTemplate.multipleItems.name, () => {
94
+ expect(
95
+ messageTemplate.multipleItems("DOWNLOADED", [
96
+ "a.mkv",
97
+ "b.mkv",
98
+ "c.mkv",
99
+ ]),
100
+ ).toEqual([
101
+ "DOWNLOADED",
102
+ "\n",
103
+ "a.mkv",
104
+ "\n",
105
+ "b.mkv",
106
+ "\n",
107
+ "c.mkv",
108
+ "\n",
109
+ ])
110
+ })
111
+ })
112
+
113
+ describe(createLogMessage.name, () => {
114
+ test("logs only once", async () => {
115
+ captureConsoleMessage("info", (consoleSpy) => {
116
+ createLogMessage({
117
+ logType: "info",
118
+ })("HELLO WORLD")
119
+
120
+ expect(consoleSpy).toHaveBeenCalledOnce()
121
+
122
+ expect(consoleSpy.mock.calls.at(0)?.at(0)).toContain(
123
+ "HELLO WORLD",
124
+ )
125
+ })
126
+ })
127
+
128
+ test("logs an informational message", async () => {
129
+ captureConsoleMessage("info", (consoleSpy) => {
130
+ createLogMessage({
131
+ logType: "info",
132
+ })("RENAMED", "old.mkv", "new.mkv")
133
+
134
+ expect(consoleSpy).toHaveBeenCalledWith(
135
+ "[RENAMED]",
136
+ "\n",
137
+ "old.mkv",
138
+ "\n",
139
+ "new.mkv",
140
+ "\n",
141
+ "\n",
142
+ )
143
+ })
144
+
145
+ // TODO: TEST COLORS
146
+ })
147
+
148
+ test("dispatches to the multipleItems template when content arg 1 is an array", async () => {
149
+ captureConsoleMessage("info", (consoleSpy) => {
150
+ createLogMessage({
151
+ logType: "info",
152
+ })("DOWNLOADED", "Files downloaded:", [
153
+ "a.mkv",
154
+ "b.mkv",
155
+ ])
156
+
157
+ expect(consoleSpy).toHaveBeenCalledWith(
158
+ "[DOWNLOADED]",
159
+ "\n",
160
+ "Files downloaded:",
161
+ "\n",
162
+ "a.mkv",
163
+ "\n",
164
+ "b.mkv",
165
+ "\n",
166
+ "\n",
167
+ "\n",
168
+ )
169
+ })
170
+ })
171
+ })
172
+
173
+ describe(logError.name, () => {
174
+ test("logs an error message", async () => {
175
+ captureConsoleMessage("error", (consoleSpy) => {
176
+ logError("ERROR")
177
+
178
+ expect(consoleSpy.mock.calls.at(0)?.at(0)).toContain(
179
+ "ERROR",
180
+ )
181
+ })
182
+ })
183
+ })
184
+
185
+ describe(logInfo.name, () => {
186
+ test("logs an info message", async () => {
187
+ captureConsoleMessage("info", (consoleSpy) => {
188
+ logInfo("INFO")
189
+
190
+ expect(consoleSpy.mock.calls.at(0)?.at(0)).toContain(
191
+ "INFO",
192
+ )
193
+ })
194
+ })
195
+ })
196
+
197
+ describe(logWarning.name, () => {
198
+ test("logs a warning message", async () => {
199
+ captureConsoleMessage("warn", (consoleSpy) => {
200
+ logWarning("WARNING")
201
+
202
+ expect(consoleSpy.mock.calls.at(0)?.at(0)).toContain(
203
+ "WARNING",
204
+ )
205
+ })
206
+ })
207
+ })