@planet-matrix/mobius-model 0.6.0 → 0.9.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.
Files changed (233) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/index.js +706 -36
  3. package/dist/index.js.map +855 -59
  4. package/package.json +28 -16
  5. package/src/ai/README.md +1 -0
  6. package/src/ai/ai.ts +107 -0
  7. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  8. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  9. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  10. package/src/ai/chat-completion-ai/index.ts +7 -0
  11. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  12. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  13. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  14. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  15. package/src/ai/embedding-ai/embedding.ts +50 -0
  16. package/src/ai/embedding-ai/index.ts +4 -0
  17. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  18. package/src/ai/index.ts +4 -0
  19. package/src/aio/README.md +100 -0
  20. package/src/aio/content.ts +141 -0
  21. package/src/aio/index.ts +3 -0
  22. package/src/aio/json.ts +127 -0
  23. package/src/aio/prompt.ts +246 -0
  24. package/src/basic/README.md +20 -15
  25. package/src/basic/error.ts +19 -5
  26. package/src/basic/function.ts +2 -2
  27. package/src/basic/index.ts +1 -0
  28. package/src/basic/schedule.ts +111 -0
  29. package/src/basic/stream.ts +135 -25
  30. package/src/credential/README.md +107 -0
  31. package/src/credential/api-key.ts +158 -0
  32. package/src/credential/bearer.ts +73 -0
  33. package/src/credential/index.ts +4 -0
  34. package/src/credential/json-web-token.ts +96 -0
  35. package/src/credential/password.ts +170 -0
  36. package/src/cron/README.md +86 -0
  37. package/src/cron/cron.ts +87 -0
  38. package/src/cron/index.ts +1 -0
  39. package/src/drizzle/README.md +1 -0
  40. package/src/drizzle/drizzle.ts +1 -0
  41. package/src/drizzle/helper.ts +47 -0
  42. package/src/drizzle/index.ts +5 -0
  43. package/src/drizzle/infer.ts +52 -0
  44. package/src/drizzle/kysely.ts +8 -0
  45. package/src/drizzle/pagination.ts +200 -0
  46. package/src/email/README.md +1 -0
  47. package/src/email/index.ts +1 -0
  48. package/src/email/resend.ts +25 -0
  49. package/src/event/class-event-proxy.ts +6 -5
  50. package/src/event/common.ts +13 -3
  51. package/src/event/event-manager.ts +3 -3
  52. package/src/event/instance-event-proxy.ts +6 -5
  53. package/src/event/internal.ts +4 -4
  54. package/src/form/README.md +25 -0
  55. package/src/form/index.ts +1 -0
  56. package/src/form/inputor-controller/base.ts +874 -0
  57. package/src/form/inputor-controller/boolean.ts +39 -0
  58. package/src/form/inputor-controller/file.ts +39 -0
  59. package/src/form/inputor-controller/form.ts +181 -0
  60. package/src/form/inputor-controller/helper.ts +117 -0
  61. package/src/form/inputor-controller/index.ts +17 -0
  62. package/src/form/inputor-controller/multi-select.ts +99 -0
  63. package/src/form/inputor-controller/number.ts +116 -0
  64. package/src/form/inputor-controller/select.ts +109 -0
  65. package/src/form/inputor-controller/text.ts +82 -0
  66. package/src/http/READMD.md +1 -0
  67. package/src/http/api/api-core.ts +84 -0
  68. package/src/http/api/api-handler.ts +79 -0
  69. package/src/http/api/api-host.ts +47 -0
  70. package/src/http/api/api-result.ts +56 -0
  71. package/src/http/api/api-schema.ts +154 -0
  72. package/src/http/api/api-server.ts +130 -0
  73. package/src/http/api/api-test.ts +142 -0
  74. package/src/http/api/api-type.ts +37 -0
  75. package/src/http/api/api.ts +81 -0
  76. package/src/http/api/index.ts +11 -0
  77. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  78. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  79. package/src/http/api-adapter/api-result-arktype.ts +297 -0
  80. package/src/http/api-adapter/api-result-zod.ts +286 -0
  81. package/src/http/api-adapter/index.ts +5 -0
  82. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  83. package/src/http/bin/gen-api-list/index.ts +1 -0
  84. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  85. package/src/http/bin/gen-api-test/index.ts +1 -0
  86. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  87. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  88. package/src/http/bin/gen-api-type/index.ts +2 -0
  89. package/src/http/bin/index.ts +2 -0
  90. package/src/http/index.ts +3 -0
  91. package/src/huawei/README.md +1 -0
  92. package/src/huawei/index.ts +2 -0
  93. package/src/huawei/moderation/index.ts +1 -0
  94. package/src/huawei/moderation/moderation.ts +355 -0
  95. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  96. package/src/huawei/obs/index.ts +1 -0
  97. package/src/huawei/obs/obs.ts +42 -0
  98. package/src/index.ts +19 -2
  99. package/src/json/README.md +92 -0
  100. package/src/json/index.ts +1 -0
  101. package/src/json/repair.ts +18 -0
  102. package/src/log/logger.ts +15 -4
  103. package/src/openai/README.md +1 -0
  104. package/src/openai/index.ts +1 -0
  105. package/src/openai/openai.ts +510 -0
  106. package/src/orchestration/README.md +9 -7
  107. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  108. package/src/orchestration/dispatching/index.ts +2 -0
  109. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  110. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  111. package/src/orchestration/dispatching/selector/index.ts +2 -0
  112. package/src/orchestration/index.ts +2 -0
  113. package/src/orchestration/scheduling/index.ts +2 -0
  114. package/src/orchestration/scheduling/scheduler.ts +103 -0
  115. package/src/orchestration/scheduling/task.ts +32 -0
  116. package/src/random/README.md +8 -7
  117. package/src/random/base.ts +66 -0
  118. package/src/random/index.ts +5 -1
  119. package/src/random/random-boolean.ts +40 -0
  120. package/src/random/random-integer.ts +60 -0
  121. package/src/random/random-number.ts +72 -0
  122. package/src/random/random-string.ts +66 -0
  123. package/src/request/README.md +108 -0
  124. package/src/request/fetch/base.ts +108 -0
  125. package/src/request/fetch/browser.ts +285 -0
  126. package/src/request/fetch/general.ts +20 -0
  127. package/src/request/fetch/index.ts +4 -0
  128. package/src/request/fetch/nodejs.ts +285 -0
  129. package/src/request/index.ts +2 -0
  130. package/src/request/request/base.ts +250 -0
  131. package/src/request/request/general.ts +64 -0
  132. package/src/request/request/index.ts +3 -0
  133. package/src/request/request/resource.ts +68 -0
  134. package/src/result/README.md +4 -0
  135. package/src/result/controller.ts +54 -0
  136. package/src/result/either.ts +193 -0
  137. package/src/result/index.ts +2 -0
  138. package/src/route/README.md +105 -0
  139. package/src/route/adapter/browser.ts +122 -0
  140. package/src/route/adapter/driver.ts +56 -0
  141. package/src/route/adapter/index.ts +2 -0
  142. package/src/route/index.ts +3 -0
  143. package/src/route/router/index.ts +2 -0
  144. package/src/route/router/route.ts +630 -0
  145. package/src/route/router/router.ts +1642 -0
  146. package/src/route/uri/hash.ts +308 -0
  147. package/src/route/uri/index.ts +7 -0
  148. package/src/route/uri/pathname.ts +376 -0
  149. package/src/route/uri/search.ts +413 -0
  150. package/src/socket/README.md +105 -0
  151. package/src/socket/client/index.ts +2 -0
  152. package/src/socket/client/socket-unit.ts +660 -0
  153. package/src/socket/client/socket.ts +203 -0
  154. package/src/socket/common/index.ts +2 -0
  155. package/src/socket/common/socket-unit-common.ts +23 -0
  156. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  157. package/src/socket/index.ts +3 -0
  158. package/src/socket/server/index.ts +3 -0
  159. package/src/socket/server/server.ts +183 -0
  160. package/src/socket/server/socket-unit.ts +449 -0
  161. package/src/socket/server/socket.ts +264 -0
  162. package/src/storage/table.ts +3 -3
  163. package/src/timer/expiration/expiration-manager.ts +3 -3
  164. package/src/timer/expiration/remaining-manager.ts +3 -3
  165. package/src/tube/README.md +99 -0
  166. package/src/tube/helper.ts +138 -0
  167. package/src/tube/index.ts +2 -0
  168. package/src/tube/tube.ts +880 -0
  169. package/src/weixin/README.md +1 -0
  170. package/src/weixin/index.ts +2 -0
  171. package/src/weixin/official-account/authorization.ts +159 -0
  172. package/src/weixin/official-account/index.ts +2 -0
  173. package/src/weixin/official-account/js-api.ts +134 -0
  174. package/src/weixin/open/index.ts +1 -0
  175. package/src/weixin/open/oauth2.ts +133 -0
  176. package/tests/unit/ai/ai.spec.ts +85 -0
  177. package/tests/unit/aio/content.spec.ts +105 -0
  178. package/tests/unit/aio/json.spec.ts +147 -0
  179. package/tests/unit/aio/prompt.spec.ts +111 -0
  180. package/tests/unit/basic/error.spec.ts +16 -4
  181. package/tests/unit/basic/schedule.spec.ts +74 -0
  182. package/tests/unit/basic/stream.spec.ts +90 -37
  183. package/tests/unit/credential/api-key.spec.ts +37 -0
  184. package/tests/unit/credential/bearer.spec.ts +23 -0
  185. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  186. package/tests/unit/credential/password.spec.ts +41 -0
  187. package/tests/unit/cron/cron.spec.ts +84 -0
  188. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  189. package/tests/unit/event/event-manager.spec.ts +3 -3
  190. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  191. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  192. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  193. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  194. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  195. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  196. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  197. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  198. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  199. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  200. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  201. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  202. package/tests/unit/http/api/api-server.spec.ts +363 -0
  203. package/tests/unit/http/api/api-test.spec.ts +117 -0
  204. package/tests/unit/http/api/api.spec.ts +121 -0
  205. package/tests/unit/http/api-adapter/node-http.spec.ts +191 -0
  206. package/tests/unit/json/repair.spec.ts +11 -0
  207. package/tests/unit/log/logger.spec.ts +19 -4
  208. package/tests/unit/openai/openai.spec.ts +64 -0
  209. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  210. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  211. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  212. package/tests/unit/random/base.spec.ts +58 -0
  213. package/tests/unit/random/random-boolean.spec.ts +25 -0
  214. package/tests/unit/random/random-integer.spec.ts +32 -0
  215. package/tests/unit/random/random-number.spec.ts +33 -0
  216. package/tests/unit/random/random-string.spec.ts +22 -0
  217. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  218. package/tests/unit/request/fetch/general.spec.ts +43 -0
  219. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  220. package/tests/unit/request/request/base.spec.ts +385 -0
  221. package/tests/unit/request/request/general.spec.ts +161 -0
  222. package/tests/unit/route/router/route.spec.ts +431 -0
  223. package/tests/unit/route/router/router.spec.ts +407 -0
  224. package/tests/unit/route/uri/hash.spec.ts +72 -0
  225. package/tests/unit/route/uri/pathname.spec.ts +147 -0
  226. package/tests/unit/route/uri/search.spec.ts +107 -0
  227. package/tests/unit/socket/client.spec.ts +208 -0
  228. package/tests/unit/socket/server.spec.ts +135 -0
  229. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  230. package/tests/unit/tube/helper.spec.ts +139 -0
  231. package/tests/unit/tube/tube.spec.ts +501 -0
  232. package/src/random/string.ts +0 -35
  233. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,147 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
4
+ import { Logger } from "#Source/log/index.ts"
5
+ import {
6
+ JsonModeResponseParser,
7
+ extractJsonBlock,
8
+ } from "#Source/aio/index.ts"
9
+
10
+ interface TestJsonOutput {
11
+ value: number
12
+ label?: string | undefined
13
+ }
14
+
15
+ const createSyncSchema = (
16
+ options: { validateCount?: { current: number } | undefined } = {}
17
+ ): StandardSchemaV1<unknown, TestJsonOutput> => {
18
+ const validateCount = options.validateCount
19
+
20
+ return {
21
+ "~standard": {
22
+ version: 1 as const,
23
+ vendor: "mobius-aio-json-sync-spec",
24
+ validate: (value: unknown): StandardSchemaV1.Result<TestJsonOutput> => {
25
+ if (validateCount !== undefined) {
26
+ validateCount.current = validateCount.current + 1
27
+ }
28
+
29
+ if (typeof value !== "object" || value === null) {
30
+ return {
31
+ issues: [{ message: "value must be an object" }],
32
+ }
33
+ }
34
+
35
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
36
+ const record = value as Record<string, unknown>
37
+ if (typeof record["value"] !== "number") {
38
+ return {
39
+ issues: [{ message: "value must be a number" }],
40
+ }
41
+ }
42
+
43
+ const output: TestJsonOutput = {
44
+ value: record["value"],
45
+ }
46
+
47
+ if (typeof record["label"] === "string") {
48
+ output.label = record["label"]
49
+ }
50
+
51
+ return {
52
+ value: output,
53
+ }
54
+ },
55
+ },
56
+ }
57
+ }
58
+
59
+ const createAsyncSchema = (): StandardSchemaV1<unknown, TestJsonOutput> => {
60
+ return {
61
+ "~standard": {
62
+ version: 1 as const,
63
+ vendor: "mobius-aio-json-async-spec",
64
+ validate: async (_value: unknown): Promise<StandardSchemaV1.Result<TestJsonOutput>> => {
65
+ await Promise.resolve()
66
+
67
+ return {
68
+ value: {
69
+ value: 1,
70
+ },
71
+ }
72
+ },
73
+ },
74
+ }
75
+ }
76
+
77
+ test("extractJsonBlock returns the first fenced json block and throws when none exists", () => {
78
+ const response = [
79
+ "before",
80
+ "```json",
81
+ "{\"value\":1}",
82
+ "```",
83
+ "```json",
84
+ "{\"value\":2}",
85
+ "```",
86
+ ].join("\n")
87
+
88
+ expect(extractJsonBlock(response)).toBe(["```json", "{\"value\":1}", "```"].join("\n"))
89
+ expect(() => extractJsonBlock("plain text only")).toThrow("Failed to extract json block from response")
90
+ })
91
+
92
+ test("JsonModeResponseParser.extractJsonContent strips fences, repairs json-like content and falls back to raw text", () => {
93
+ const parser = new JsonModeResponseParser({
94
+ outputSchema: createSyncSchema(),
95
+ })
96
+
97
+ const repairedFromFence = parser.extractJsonContent(["header", "```json", "{value:1,}", "```"].join("\n"))
98
+ const repairedFromRawText = parser.extractJsonContent("{label:'mobius',value:2,}")
99
+
100
+ expect(repairedFromFence).toBe('\n{"value":1}\n')
101
+ expect(repairedFromRawText).toBe('{"label":"mobius","value":2}')
102
+ })
103
+
104
+ test("JsonModeResponseParser.parse validates sync output, caches by source text and throws for invalid or async schemas", () => {
105
+ const validateCount = { current: 0 }
106
+ const parser = new JsonModeResponseParser({
107
+ outputSchema: createSyncSchema({ validateCount }),
108
+ })
109
+
110
+ const first = parser.parse('{"value":1,"label":"ok"}')
111
+ const second = parser.parse('{"value":1,"label":"ok"}')
112
+
113
+ expect(first).toEqual({ value: 1, label: "ok" })
114
+ expect(second).toBe(first)
115
+ expect(validateCount.current).toBe(1)
116
+
117
+ const invalidParser = new JsonModeResponseParser({
118
+ outputSchema: createSyncSchema(),
119
+ })
120
+ expect(() => invalidParser.parse('{"label":"missing-value"}')).toThrow("Variable validation failed")
121
+
122
+ const asyncParser = new JsonModeResponseParser({
123
+ outputSchema: createAsyncSchema(),
124
+ })
125
+ expect(() => asyncParser.parse('{"value":1}')).toThrow("Validation result is a promise")
126
+ })
127
+
128
+ test("JsonModeResponseParser.check returns false and logs the failure context when parsing fails", () => {
129
+ const logger = new Logger({
130
+ name: "AioJsonTest",
131
+ configs: {
132
+ enabled: false,
133
+ },
134
+ })
135
+ const logSpy = vi.spyOn(logger, "log")
136
+ const parser = new JsonModeResponseParser({
137
+ logger,
138
+ outputSchema: createSyncSchema(),
139
+ })
140
+
141
+ expect(parser.check('{"value":3}')).toBe(true)
142
+ expect(parser.check('{"label":"missing-value"}')).toBe(false)
143
+
144
+ expect(logSpy).toHaveBeenCalledTimes(2)
145
+ expect(logSpy.mock.calls[0]?.[0]).toBe("check error:")
146
+ expect(logSpy.mock.calls[1]).toEqual(["error response:", '{"label":"missing-value"}'])
147
+ })
@@ -0,0 +1,111 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ Prompt,
5
+ emptyLine,
6
+ orderedListItem,
7
+ scopeContent,
8
+ text,
9
+ unorderedListItem,
10
+ } from "#Source/aio/index.ts"
11
+
12
+ test("text creates a plain text prompt block", () => {
13
+ expect(text("hello")).toEqual({
14
+ type: "text",
15
+ content: "hello",
16
+ })
17
+ })
18
+
19
+ test("emptyLine creates an empty-line prompt block", () => {
20
+ expect(emptyLine()).toEqual({
21
+ type: "empty-line",
22
+ })
23
+ })
24
+
25
+ test("scopeContent wraps content with triple-quote boundaries", () => {
26
+ expect(scopeContent("context")).toEqual({
27
+ type: "scope-content",
28
+ content: '"""\ncontext\n"""',
29
+ })
30
+ })
31
+
32
+ test("orderedListItem creates an ordered-list prompt block", () => {
33
+ expect(orderedListItem("first")).toEqual({
34
+ type: "ordered-list-item",
35
+ content: "first",
36
+ })
37
+ })
38
+
39
+ test("unorderedListItem creates an unordered-list prompt block", () => {
40
+ expect(unorderedListItem("first")).toEqual({
41
+ type: "unordered-list-item",
42
+ content: "first",
43
+ })
44
+ })
45
+
46
+ test("Prompt.addBlock appends a block produced by helpers and returns the same instance", async () => {
47
+ const prompt = new Prompt({})
48
+
49
+ expect(prompt.addBlock(helpers => helpers.text("from-helper"))).toBe(prompt)
50
+ await expect(prompt.getPromptInString()).resolves.toBe("from-helper")
51
+ })
52
+
53
+ test("Prompt.addText appends a text block", async () => {
54
+ const prompt = new Prompt({})
55
+
56
+ expect(prompt.addText("alpha")).toBe(prompt)
57
+ await expect(prompt.getPromptInString()).resolves.toBe("alpha")
58
+ })
59
+
60
+ test("Prompt.addEmptyLine appends an empty line block", async () => {
61
+ const prompt = new Prompt({})
62
+
63
+ expect(prompt.addEmptyLine()).toBe(prompt)
64
+ await expect(prompt.getPromptInString()).resolves.toBe("\r\n")
65
+ })
66
+
67
+ test("Prompt.addScopeContent appends a scoped content block", async () => {
68
+ const prompt = new Prompt({})
69
+
70
+ expect(prompt.addScopeContent("body")).toBe(prompt)
71
+ await expect(prompt.getPromptInString()).resolves.toBe('"""\nbody\n"""')
72
+ })
73
+
74
+ test("Prompt.addOrderedListItem appends an ordered list block", async () => {
75
+ const prompt = new Prompt({})
76
+
77
+ expect(prompt.addOrderedListItem("step")).toBe(prompt)
78
+ await expect(prompt.getPromptInString()).resolves.toBe("1. step")
79
+ })
80
+
81
+ test("Prompt.addUnorderedListItem appends an unordered list block", async () => {
82
+ const prompt = new Prompt({})
83
+
84
+ expect(prompt.addUnorderedListItem("item")).toBe(prompt)
85
+ await expect(prompt.getPromptInString()).resolves.toBe("- item")
86
+ })
87
+
88
+ test("Prompt.getPromptInString renders prompt blocks with list numbering and resets between block groups", async () => {
89
+ const prompt = new Prompt({})
90
+
91
+ prompt
92
+ .addText("Intro")
93
+ .addOrderedListItem("First")
94
+ .addOrderedListItem("Second")
95
+ .addText("Break")
96
+ .addOrderedListItem("Reset")
97
+ .addUnorderedListItem("One")
98
+ .addUnorderedListItem("Two")
99
+ .addScopeContent("body")
100
+
101
+ await expect(prompt.getPromptInString()).resolves.toBe([
102
+ "Intro",
103
+ "1. First",
104
+ "2. Second",
105
+ "Break",
106
+ "1. Reset",
107
+ "- One",
108
+ "- Two",
109
+ '"""\nbody\n"""',
110
+ ].join("\r\n"))
111
+ })
@@ -2,7 +2,8 @@ import { expect, test } from "vitest"
2
2
 
3
3
  import {
4
4
  errorIsNetworkError,
5
- errorStringifyException,
5
+ errorStringify,
6
+ toError,
6
7
  } from "#Source/basic/index.ts"
7
8
 
8
9
  test("errorIsNetworkError returns expected values", () => {
@@ -26,7 +27,18 @@ test("errorIsNetworkError returns expected values", () => {
26
27
  expect(errorIsNetworkError(safariWithStack)).toBe(false)
27
28
  })
28
29
 
29
- test("errorStringifyException returns readable output", () => {
30
- expect(errorStringifyException(new Error("boom"))).toBe("Error: boom")
31
- expect(errorStringifyException(123)).toBe("123")
30
+ test("errorStringify returns readable output", () => {
31
+ expect(errorStringify(new Error("boom"))).toBe("Error: boom")
32
+ expect(errorStringify(123)).toBe("123")
33
+ })
34
+
35
+ test("toError returns expected values", () => {
36
+ const error = new Error("boom")
37
+
38
+ expect(toError(error)).toBe(error)
39
+
40
+ const wrappedError = toError(123)
41
+
42
+ expect(wrappedError).toBeInstanceOf(Error)
43
+ expect(wrappedError.message).toBe("123")
32
44
  })
@@ -0,0 +1,74 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import {
4
+ scheduleMacroTask,
5
+ scheduleMacroTaskSimple,
6
+ scheduleMicroTask,
7
+ scheduleMicroTaskSimple,
8
+ } from "#Source/basic/index.ts"
9
+
10
+ test("scheduleMicroTaskSimple schedules a task in the microtask queue", async () => {
11
+ const values: string[] = []
12
+
13
+ scheduleMicroTaskSimple(() => {
14
+ values.push("micro")
15
+ })
16
+
17
+ expect(values).toEqual([])
18
+
19
+ await Promise.resolve()
20
+
21
+ expect(values).toEqual(["micro"])
22
+ })
23
+
24
+ test("scheduleMacroTaskSimple schedules a task in the macrotask queue", async () => {
25
+ vi.useFakeTimers()
26
+ const values: string[] = []
27
+
28
+ scheduleMacroTaskSimple(() => {
29
+ values.push("macro")
30
+ })
31
+
32
+ expect(values).toEqual([])
33
+
34
+ await vi.runAllTimersAsync()
35
+
36
+ expect(values).toEqual(["macro"])
37
+ vi.useRealTimers()
38
+ })
39
+
40
+ test("scheduleMicroTask schedules async-capable tasks in the microtask queue", async () => {
41
+ const values: string[] = []
42
+
43
+ scheduleMicroTask({
44
+ task: () => {
45
+ values.push("micro")
46
+ },
47
+ })
48
+
49
+ expect(values).toEqual([])
50
+
51
+ await Promise.resolve()
52
+ await Promise.resolve()
53
+
54
+ expect(values).toEqual(["micro"])
55
+ })
56
+
57
+ test("scheduleMacroTask schedules async-capable tasks with timeout", async () => {
58
+ vi.useFakeTimers()
59
+ const values: string[] = []
60
+
61
+ scheduleMacroTask({
62
+ timeout: 25,
63
+ task: () => {
64
+ values.push("macro")
65
+ },
66
+ })
67
+
68
+ await vi.advanceTimersByTimeAsync(24)
69
+ expect(values).toEqual([])
70
+
71
+ await vi.advanceTimersByTimeAsync(1)
72
+ expect(values).toEqual(["macro"])
73
+ vi.useRealTimers()
74
+ })
@@ -1,17 +1,18 @@
1
1
  import { expect, test } from "vitest"
2
2
 
3
3
  import {
4
- streamConsumeInAsyncMacroTask,
5
- streamConsumeInSyncMacroTask,
6
- streamFromArray,
7
- streamTransformInAsyncMacroTask,
4
+ streamConsumeInMacroTask,
5
+ streamConsumeInMicroTask,
6
+ streamFromArrayEager,
7
+ streamFromArrayLazy,
8
+ streamTransformInMacroTask,
8
9
  } from "#Source/basic/index.ts"
9
10
 
10
- test("streamFromArray creates a readable stream from array values", async () => {
11
+ test("streamFromArrayEager creates a readable stream from array values", async () => {
11
12
  const source = [1, 2, 3]
12
13
  const values: number[] = []
13
14
 
14
- const stream = streamFromArray(source)
15
+ const stream = streamFromArrayEager(source)
15
16
  for await (const value of stream) {
16
17
  values.push(value)
17
18
  }
@@ -19,18 +20,38 @@ test("streamFromArray creates a readable stream from array values", async () =>
19
20
  expect(values).toEqual(source)
20
21
  })
21
22
 
22
- test("streamConsumeInSyncMacroTask consumes stream and handles callback errors", async () => {
23
+ test("streamFromArrayLazy creates a lazily consumed readable stream", async () => {
24
+ const source = [1, 2, 3]
25
+ const stream = streamFromArrayLazy(source)
26
+ const reader = stream.getReader()
27
+
28
+ const firstChunk = await reader.read()
29
+ const secondChunk = await reader.read()
30
+ const thirdChunk = await reader.read()
31
+ const doneChunk = await reader.read()
32
+
33
+ expect(firstChunk).toEqual({ done: false, value: 1 })
34
+ expect(secondChunk).toEqual({ done: false, value: 2 })
35
+ expect(thirdChunk).toEqual({ done: false, value: 3 })
36
+ expect(doneChunk).toEqual({ done: true, value: undefined })
37
+ })
38
+
39
+ test("streamConsumeInMicroTask consumes stream and handles callback errors", async () => {
23
40
  const consumed: number[] = []
24
41
  let doneCalled = false
25
42
 
26
- await streamConsumeInSyncMacroTask<number>({
27
- readableStream: streamFromArray([1, 2, 3]),
28
- onValue: (chunk) => {
29
- consumed.push(chunk)
30
- },
31
- onDone: () => {
32
- doneCalled = true
33
- },
43
+ await new Promise<void>((resolve, reject) => {
44
+ streamConsumeInMicroTask<number>({
45
+ readableStream: streamFromArrayEager([1, 2, 3]),
46
+ onValue: (chunk) => {
47
+ consumed.push(chunk)
48
+ },
49
+ onDone: () => {
50
+ doneCalled = true
51
+ resolve()
52
+ },
53
+ onError: reject,
54
+ })
34
55
  })
35
56
 
36
57
  expect(consumed).toEqual([1, 2, 3])
@@ -38,30 +59,33 @@ test("streamConsumeInSyncMacroTask consumes stream and handles callback errors",
38
59
 
39
60
  let errorMessage = ""
40
61
  let doneCalledInErrorCase = false
41
- await streamConsumeInSyncMacroTask<number>({
42
- readableStream: streamFromArray([1, 2]),
43
- onValue: (chunk) => {
44
- if (chunk === 2) {
45
- throw new Error("boom")
46
- }
47
- },
48
- onDone: () => {
49
- doneCalledInErrorCase = true
50
- },
51
- onError: (error) => {
52
- errorMessage = error.message
53
- },
62
+ await new Promise<void>((resolve) => {
63
+ streamConsumeInMicroTask<number>({
64
+ readableStream: streamFromArrayEager([1, 2]),
65
+ onValue: (chunk) => {
66
+ if (chunk === 2) {
67
+ throw new Error("boom")
68
+ }
69
+ },
70
+ onDone: () => {
71
+ doneCalledInErrorCase = true
72
+ },
73
+ onError: (error) => {
74
+ errorMessage = error.message
75
+ resolve()
76
+ },
77
+ })
54
78
  })
55
79
 
56
80
  expect(errorMessage).toContain("boom")
57
81
  expect(doneCalledInErrorCase).toBe(false)
58
82
  })
59
83
 
60
- test("streamConsumeInAsyncMacroTask consumes stream and forwards callback failures", async () => {
84
+ test("streamConsumeInMacroTask consumes stream and forwards callback failures", async () => {
61
85
  const consumed: number[] = []
62
86
  await new Promise<void>((resolve, reject) => {
63
- streamConsumeInAsyncMacroTask<number>({
64
- readableStream: streamFromArray([1, 2, 3]),
87
+ streamConsumeInMacroTask<number>({
88
+ readableStream: streamFromArrayEager([1, 2, 3]),
65
89
  onValue: (chunk) => {
66
90
  consumed.push(chunk)
67
91
  },
@@ -78,8 +102,8 @@ test("streamConsumeInAsyncMacroTask consumes stream and forwards callback failur
78
102
 
79
103
  let errorMessage = ""
80
104
  await new Promise<void>((resolve, reject) => {
81
- streamConsumeInAsyncMacroTask<number>({
82
- readableStream: streamFromArray([1]),
105
+ streamConsumeInMacroTask<number>({
106
+ readableStream: streamFromArrayEager([1]),
83
107
  onValue: () => {
84
108
  throw new Error("async-boom")
85
109
  },
@@ -96,13 +120,13 @@ test("streamConsumeInAsyncMacroTask consumes stream and forwards callback failur
96
120
  expect(errorMessage).toContain("async-boom")
97
121
  })
98
122
 
99
- test("streamTransformInAsyncMacroTask transforms values and handles invalid inputs/errors", async () => {
123
+ test("streamTransformInMacroTask transforms values and handles invalid inputs/errors", async () => {
100
124
  expect(() => {
101
- streamTransformInAsyncMacroTask<number, number>({})
125
+ streamTransformInMacroTask<number, number>({})
102
126
  }).toThrow("Either readableStream or reader must be provided")
103
127
 
104
- const transformed = streamTransformInAsyncMacroTask<number, number>({
105
- readableStream: streamFromArray([1, 2, 3]),
128
+ const transformed = streamTransformInMacroTask<number, number>({
129
+ readableStream: streamFromArrayEager([1, 2, 3]),
106
130
  onChunk: (chunk, controller) => {
107
131
  if (chunk.done === true) {
108
132
  controller.close()
@@ -117,4 +141,33 @@ test("streamTransformInAsyncMacroTask transforms values and handles invalid inpu
117
141
  values.push(value)
118
142
  }
119
143
  expect(values).toEqual([10, 20, 30])
144
+
145
+ const reader = streamFromArrayEager([4]).getReader()
146
+ const transformedFromReader = streamTransformInMacroTask<number, number>({
147
+ reader,
148
+ onChunk: (chunk, controller) => {
149
+ if (chunk.done === true) {
150
+ controller.close()
151
+ return
152
+ }
153
+ controller.enqueue(chunk.value + 1)
154
+ },
155
+ })
156
+
157
+ expect(await Array.fromAsync(transformedFromReader)).toEqual([5])
158
+
159
+ let refinedError: Error | undefined
160
+ const errored = streamTransformInMacroTask<number, number>({
161
+ readableStream: streamFromArrayEager([1]),
162
+ onChunk: () => {
163
+ throw new Error("transform-boom")
164
+ },
165
+ onError: (error) => {
166
+ refinedError = new Error(`wrapped:${error.message}`)
167
+ return refinedError
168
+ },
169
+ })
170
+
171
+ await expect(Array.fromAsync(errored)).rejects.toThrow("wrapped:Error: transform-boom")
172
+ expect(refinedError?.message).toBe("wrapped:Error: transform-boom")
120
173
  })
@@ -0,0 +1,37 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import { generateApiKey, maskApiKey } from "#Source/credential/index.ts"
4
+
5
+ test("generateApiKey creates a stable sk-prefixed key shape", () => {
6
+ vi.spyOn(crypto, "getRandomValues").mockImplementation(buffer => {
7
+ if (buffer === null) {
8
+ return buffer
9
+ }
10
+
11
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
12
+ const target = buffer as Uint8Array
13
+ target.forEach((_, index) => {
14
+ target[index] = index
15
+ })
16
+ return buffer
17
+ })
18
+
19
+ const apiKey = generateApiKey({})
20
+ const customApiKey = generateApiKey({ prefix: "pk_", alphabet: "ABC123", bodyLength: 6 })
21
+
22
+ expect(apiKey.startsWith("sk-")).toBe(true)
23
+ expect(apiKey).toHaveLength(51)
24
+ expect(apiKey).toMatch(/^sk-[0-9A-Za-z]{48}$/)
25
+ expect(customApiKey).toBe("pk_ABC123")
26
+ expect(() => generateApiKey({ alphabet: "", bodyLength: 6 })).toThrow("Expected alphabet to contain at least one character")
27
+ })
28
+
29
+ test("maskApiKey hides the middle of a valid key and rejects malformed input", () => {
30
+ const apiKey = `sk-${"ab"}${"0".repeat(44)}yz`
31
+ const customOptions = { prefix: "pk_", alphabet: "ABC123", bodyLength: 6 } as const
32
+
33
+ expect(maskApiKey(apiKey)).toBe("sk-ab********************************************yz")
34
+ expect(maskApiKey("pk_ABC123", customOptions)).toBe("pk_AB**23")
35
+ expect(() => maskApiKey("not-an-api-key")).toThrow("Invalid API key format")
36
+ expect(() => maskApiKey(`sk-${"#".repeat(48)}`)).toThrow("Invalid API key format")
37
+ })
@@ -0,0 +1,23 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { formatBearerCredential, isBearerCredential, parseBearerCredential } from "#Source/credential/index.ts"
4
+
5
+ test("isBearerCredential recognizes bearer credential strings", () => {
6
+ expect(isBearerCredential("Bearer token-value")).toBe(true)
7
+ expect(isBearerCredential("bearer token-value")).toBe(true)
8
+ expect(isBearerCredential("Basic token-value")).toBe(false)
9
+ expect(isBearerCredential("token-value")).toBe(false)
10
+ })
11
+
12
+ test("formatBearerCredential prefixes a raw token with the Bearer scheme", () => {
13
+ expect(formatBearerCredential("token-value")).toBe("Bearer token-value")
14
+ expect(formatBearerCredential(" token-value ")).toBe("Bearer token-value")
15
+ expect(() => formatBearerCredential(" ")).toThrow("Expected token to contain at least one non-whitespace character")
16
+ })
17
+
18
+ test("parseBearerCredential extracts the raw token only from bearer credentials", () => {
19
+ expect(parseBearerCredential("Bearer token-value")).toBe("token-value")
20
+ expect(parseBearerCredential("bearer token-value")).toBe("token-value")
21
+ expect(parseBearerCredential("Basic token-value")).toBeUndefined()
22
+ expect(parseBearerCredential(undefined)).toBeUndefined()
23
+ })
@@ -0,0 +1,23 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { formatBearerCredential, JsonWebToken } from "#Source/credential/index.ts"
4
+
5
+ test("JsonWebToken.signToken creates a compact JWT string", async () => {
6
+ const jwt = new JsonWebToken({ secret: "credential-secret", expiresIn: 3_600 })
7
+
8
+ const token = await jwt.signToken("user-1")
9
+
10
+ expect(token.split(".")).toHaveLength(3)
11
+ expect(typeof token).toBe("string")
12
+ })
13
+
14
+ test("JsonWebToken.parseToken returns userId for valid tokens and undefined for invalid input", async () => {
15
+ const jwt = new JsonWebToken({ secret: "credential-secret", expiresIn: 3_600 })
16
+ const token = await jwt.signToken("user-1")
17
+
18
+ await expect(jwt.parseToken(token)).resolves.toBe("user-1")
19
+ await expect(jwt.parseToken(formatBearerCredential(token))).resolves.toBeUndefined()
20
+ await expect(jwt.parseToken(undefined)).resolves.toBeUndefined()
21
+ await expect(jwt.parseToken("broken.token.value")).resolves.toBeUndefined()
22
+ await expect(new JsonWebToken({ secret: "other-secret", expiresIn: 3_600 }).parseToken(token)).resolves.toBeUndefined()
23
+ })
@@ -0,0 +1,41 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import { Password } from "#Source/credential/index.ts"
4
+
5
+ test("Password.generateSalt returns a hexadecimal salt string of the requested byte length", () => {
6
+ vi.spyOn(crypto, "getRandomValues").mockImplementation(buffer => {
7
+ if (buffer === null) {
8
+ return buffer
9
+ }
10
+
11
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
12
+ const target = buffer as Uint8Array
13
+ target.fill(0xAB)
14
+ return buffer
15
+ })
16
+
17
+ const salt = Password.generateSalt(8)
18
+
19
+ expect(salt).toBe("abababababababab")
20
+ expect(salt).toHaveLength(16)
21
+ })
22
+
23
+ test("Password.hashPasswordWithSalt derives a stable PBKDF2-SHA-512 hash for the same input", async () => {
24
+ const salt = "00112233445566778899AABBCCDDEEFF"
25
+ const hash1 = await Password.hashPasswordWithSalt("secret", salt)
26
+ const hash2 = await Password.hashPasswordWithSalt("secret", salt)
27
+ const hash3 = await Password.hashPasswordWithSalt("other", salt)
28
+
29
+ expect(hash1).toHaveLength(128)
30
+ expect(hash1).toMatch(/^[0-9A-F]+$/i)
31
+ expect(hash1).toBe(hash2)
32
+ expect(hash1).not.toBe(hash3)
33
+ })
34
+
35
+ test("Password.comparePassword distinguishes matching and mismatching passwords", async () => {
36
+ const salt = "00112233445566778899AABBCCDDEEFF"
37
+ const hash = await Password.hashPasswordWithSalt("secret", salt)
38
+
39
+ await expect(Password.comparePassword("secret", hash, salt)).resolves.toBe(true)
40
+ await expect(Password.comparePassword("other", hash, salt)).resolves.toBe(false)
41
+ })