@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest'
|
|
2
|
+
import { coerceBySchema } from '../coerce.js'
|
|
3
|
+
|
|
4
|
+
describe('coerceBySchema', () => {
|
|
5
|
+
describe('string type', () => {
|
|
6
|
+
test('string stays as string', () => {
|
|
7
|
+
expect(coerceBySchema('hello', { type: 'string' }, 'name')).toBe('hello')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('preserves leading zeros', () => {
|
|
11
|
+
expect(coerceBySchema('00123', { type: 'string' }, 'id')).toBe('00123')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('numeric string stays as string', () => {
|
|
15
|
+
expect(coerceBySchema('3000', { type: 'string' }, 'port')).toBe('3000')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('boolean coerced to string', () => {
|
|
19
|
+
expect(coerceBySchema(true, { type: 'string' }, 'flag')).toBe('true')
|
|
20
|
+
expect(coerceBySchema(false, { type: 'string' }, 'flag')).toBe('false')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('number type', () => {
|
|
25
|
+
test('numeric string to number', () => {
|
|
26
|
+
expect(coerceBySchema('3000', { type: 'number' }, 'port')).toBe(3000)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('decimal string to number', () => {
|
|
30
|
+
expect(coerceBySchema('3.14', { type: 'number' }, 'ratio')).toBe(3.14)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('negative string to number', () => {
|
|
34
|
+
expect(coerceBySchema('-42', { type: 'number' }, 'offset')).toBe(-42)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('zero string to number', () => {
|
|
38
|
+
expect(coerceBySchema('0', { type: 'number' }, 'count')).toBe(0)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('boolean to number', () => {
|
|
42
|
+
expect(coerceBySchema(true, { type: 'number' }, 'flag')).toBe(1)
|
|
43
|
+
expect(coerceBySchema(false, { type: 'number' }, 'flag')).toBe(0)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('non-numeric string throws', () => {
|
|
47
|
+
expect(() => coerceBySchema('abc', { type: 'number' }, 'port'))
|
|
48
|
+
.toThrow('expected number, got "abc"')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('empty string throws', () => {
|
|
52
|
+
expect(() => coerceBySchema('', { type: 'number' }, 'port'))
|
|
53
|
+
.toThrow('expected number, got empty string')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('Infinity string throws', () => {
|
|
57
|
+
expect(() => coerceBySchema('Infinity', { type: 'number' }, 'val'))
|
|
58
|
+
.toThrow('expected number')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('integer type', () => {
|
|
63
|
+
test('integer string to integer', () => {
|
|
64
|
+
expect(coerceBySchema('42', { type: 'integer' }, 'count')).toBe(42)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('decimal string throws for integer', () => {
|
|
68
|
+
expect(() => coerceBySchema('3.14', { type: 'integer' }, 'count'))
|
|
69
|
+
.toThrow('expected integer, got "3.14"')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('non-numeric string throws', () => {
|
|
73
|
+
expect(() => coerceBySchema('abc', { type: 'integer' }, 'count'))
|
|
74
|
+
.toThrow('expected number, got "abc"')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('boolean type', () => {
|
|
79
|
+
test('"true" to true', () => {
|
|
80
|
+
expect(coerceBySchema('true', { type: 'boolean' }, 'debug')).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('"false" to false', () => {
|
|
84
|
+
expect(coerceBySchema('false', { type: 'boolean' }, 'debug')).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('boolean passthrough', () => {
|
|
88
|
+
expect(coerceBySchema(true, { type: 'boolean' }, 'debug')).toBe(true)
|
|
89
|
+
expect(coerceBySchema(false, { type: 'boolean' }, 'debug')).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('other strings throw', () => {
|
|
93
|
+
expect(() => coerceBySchema('yes', { type: 'boolean' }, 'debug'))
|
|
94
|
+
.toThrow('expected true or false, got "yes"')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('null type', () => {
|
|
99
|
+
test('empty string to null', () => {
|
|
100
|
+
expect(coerceBySchema('', { type: 'null' }, 'val')).toBe(null)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('non-empty string throws', () => {
|
|
104
|
+
expect(() => coerceBySchema('hello', { type: 'null' }, 'val'))
|
|
105
|
+
.toThrow('expected empty string for null')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('object type', () => {
|
|
110
|
+
test('valid JSON object', () => {
|
|
111
|
+
expect(coerceBySchema('{"a":1}', { type: 'object' }, 'config'))
|
|
112
|
+
.toEqual({ a: 1 })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('invalid JSON throws', () => {
|
|
116
|
+
expect(() => coerceBySchema('not json', { type: 'object' }, 'config'))
|
|
117
|
+
.toThrow('expected valid JSON object')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('JSON array throws for object type', () => {
|
|
121
|
+
expect(() => coerceBySchema('[1,2]', { type: 'object' }, 'config'))
|
|
122
|
+
.toThrow('expected valid JSON object')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('array type', () => {
|
|
127
|
+
test('valid JSON array string', () => {
|
|
128
|
+
expect(coerceBySchema('[1,2,3]', { type: 'array' }, 'items'))
|
|
129
|
+
.toEqual([1, 2, 3])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('JSON array string with items schema coerces elements', () => {
|
|
133
|
+
expect(coerceBySchema('["1","2","3"]', { type: 'array', items: { type: 'number' } }, 'ids'))
|
|
134
|
+
.toEqual([1, 2, 3])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('single value wraps into array', () => {
|
|
138
|
+
expect(coerceBySchema('hello', { type: 'array' }, 'tags'))
|
|
139
|
+
.toEqual(['hello'])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('single value wraps and coerces via items schema', () => {
|
|
143
|
+
expect(coerceBySchema('42', { type: 'array', items: { type: 'number' } }, 'ids'))
|
|
144
|
+
.toEqual([42])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('repeated flags (array input) collected into array', () => {
|
|
148
|
+
expect(coerceBySchema(['foo', 'bar'], { type: 'array' }, 'tags'))
|
|
149
|
+
.toEqual(['foo', 'bar'])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('repeated flags coerced via items schema', () => {
|
|
153
|
+
expect(coerceBySchema(['1', '2', '3'], { type: 'array', items: { type: 'number' } }, 'ids'))
|
|
154
|
+
.toEqual([1, 2, 3])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('repeated flags with invalid item throws', () => {
|
|
158
|
+
expect(() => coerceBySchema(['1', 'abc'], { type: 'array', items: { type: 'number' } }, 'ids'))
|
|
159
|
+
.toThrow('expected number, got "abc"')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('JSON object string wraps into array', () => {
|
|
163
|
+
// Not a JSON array, so the string is wrapped into a single-element array
|
|
164
|
+
expect(coerceBySchema('{"a":1}', { type: 'array' }, 'items'))
|
|
165
|
+
.toEqual(['{"a":1}'])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('JSON object string with items: object schema parses correctly', () => {
|
|
169
|
+
expect(coerceBySchema('{"a":1}', { type: 'array', items: { type: 'object' } }, 'items'))
|
|
170
|
+
.toEqual([{ a: 1 }])
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('repeated flags with non-array schema', () => {
|
|
175
|
+
test('repeated flags with string schema throws', () => {
|
|
176
|
+
expect(() => coerceBySchema(['foo', 'bar'], { type: 'string' }, 'name'))
|
|
177
|
+
.toThrow('does not accept multiple values')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('repeated flags with number schema throws', () => {
|
|
181
|
+
expect(() => coerceBySchema(['1', '2'], { type: 'number' }, 'port'))
|
|
182
|
+
.toThrow('does not accept multiple values')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('repeated flags with object schema throws', () => {
|
|
186
|
+
expect(() => coerceBySchema(['{}', '{}'], { type: 'object' }, 'config'))
|
|
187
|
+
.toThrow('does not accept multiple values')
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('union types', () => {
|
|
192
|
+
test('["number", "string"] — numeric string becomes number', () => {
|
|
193
|
+
expect(coerceBySchema('123', { type: ['number', 'string'] }, 'val')).toBe(123)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('["number", "string"] — non-numeric stays string', () => {
|
|
197
|
+
expect(coerceBySchema('abc', { type: ['number', 'string'] }, 'val')).toBe('abc')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('["string", "null"] — empty string becomes null', () => {
|
|
201
|
+
expect(coerceBySchema('', { type: ['string', 'null'] }, 'val')).toBe('')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('["null", "string"] — empty string becomes null (null tried first)', () => {
|
|
205
|
+
expect(coerceBySchema('', { type: ['null', 'string'] }, 'val')).toBe(null)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('["number", "null"] — non-numeric non-empty throws', () => {
|
|
209
|
+
expect(() => coerceBySchema('abc', { type: ['number', 'null'] }, 'val'))
|
|
210
|
+
.toThrow('expected number or null')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('enum', () => {
|
|
215
|
+
test('valid enum value', () => {
|
|
216
|
+
expect(coerceBySchema('red', { enum: ['red', 'blue', 'green'] }, 'color')).toBe('red')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('invalid enum value throws', () => {
|
|
220
|
+
expect(() => coerceBySchema('yellow', { enum: ['red', 'blue'] }, 'color'))
|
|
221
|
+
.toThrow('expected one of "red", "blue"')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('numeric enum values', () => {
|
|
225
|
+
expect(coerceBySchema('1', { enum: [1, 2, 3] }, 'level')).toBe(1)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('anyOf / oneOf', () => {
|
|
230
|
+
test('anyOf tries variants in order', () => {
|
|
231
|
+
const schema = { anyOf: [{ type: 'number' as const }, { type: 'string' as const }] }
|
|
232
|
+
expect(coerceBySchema('123', schema, 'val')).toBe(123)
|
|
233
|
+
expect(coerceBySchema('abc', schema, 'val')).toBe('abc')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('oneOf tries variants in order', () => {
|
|
237
|
+
const schema = { oneOf: [{ type: 'boolean' as const }, { type: 'string' as const }] }
|
|
238
|
+
expect(coerceBySchema('true', schema, 'val')).toBe(true)
|
|
239
|
+
expect(coerceBySchema('hello', schema, 'val')).toBe('hello')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('const', () => {
|
|
244
|
+
test('matching const value', () => {
|
|
245
|
+
expect(coerceBySchema('hello', { const: 'hello' }, 'val')).toBe('hello')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('non-matching const throws', () => {
|
|
249
|
+
expect(() => coerceBySchema('world', { const: 'hello' }, 'val'))
|
|
250
|
+
.toThrow('expected "hello"')
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('edge cases', () => {
|
|
255
|
+
test('negative zero', () => {
|
|
256
|
+
expect(coerceBySchema('-0', { type: 'number' }, 'val')).toBe(-0)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('very large number', () => {
|
|
260
|
+
expect(coerceBySchema('9007199254740991', { type: 'number' }, 'val')).toBe(9007199254740991)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('NaN string throws', () => {
|
|
264
|
+
expect(() => coerceBySchema('NaN', { type: 'number' }, 'val'))
|
|
265
|
+
.toThrow('expected number')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('whitespace-only string stays as string', () => {
|
|
269
|
+
expect(coerceBySchema(' ', { type: 'string' }, 'val')).toBe(' ')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('empty JSON object', () => {
|
|
273
|
+
expect(coerceBySchema('{}', { type: 'object' }, 'val')).toEqual({})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('empty JSON array', () => {
|
|
277
|
+
expect(coerceBySchema('[]', { type: 'array' }, 'val')).toEqual([])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('nested JSON object', () => {
|
|
281
|
+
expect(coerceBySchema('{"a":{"b":[1,2]}}', { type: 'object' }, 'val'))
|
|
282
|
+
.toEqual({ a: { b: [1, 2] } })
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('boolean passed to object type throws', () => {
|
|
286
|
+
expect(() => coerceBySchema(true, { type: 'object' }, 'val'))
|
|
287
|
+
.toThrow('expected JSON object, got boolean')
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('boolean passed to array type wraps in array', () => {
|
|
291
|
+
expect(coerceBySchema(true, { type: 'array' }, 'val')).toEqual([true])
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('boolean passed to array type with items schema coerces', () => {
|
|
295
|
+
expect(coerceBySchema(true, { type: 'array', items: { type: 'string' } }, 'val'))
|
|
296
|
+
.toEqual(['true'])
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('"0" as number returns 0', () => {
|
|
300
|
+
expect(coerceBySchema('0', { type: 'number' }, 'val')).toBe(0)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('"0" as integer returns 0', () => {
|
|
304
|
+
expect(coerceBySchema('0', { type: 'integer' }, 'val')).toBe(0)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('"0" as string returns "0"', () => {
|
|
308
|
+
expect(coerceBySchema('0', { type: 'string' }, 'val')).toBe('0')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('phone number as string', () => {
|
|
312
|
+
expect(coerceBySchema('+1234567890', { type: 'string' }, 'phone')).toBe('+1234567890')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('phone number with + prefix is valid number', () => {
|
|
316
|
+
// "+1234567890" is a valid number in JS (+1234567890 === 1234567890)
|
|
317
|
+
// Use string type for phone numbers to preserve the "+"
|
|
318
|
+
expect(coerceBySchema('+1234567890', { type: 'number' }, 'phone')).toBe(1234567890)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('phone number with letters throws for number type', () => {
|
|
322
|
+
expect(() => coerceBySchema('+1-800-FLOWERS', { type: 'number' }, 'phone'))
|
|
323
|
+
.toThrow('expected number')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('leading zeros preserved as string', () => {
|
|
327
|
+
expect(coerceBySchema('007', { type: 'string' }, 'id')).toBe('007')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('hex string as number', () => {
|
|
331
|
+
expect(coerceBySchema('0xff', { type: 'number' }, 'val')).toBe(255)
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('implicit types', () => {
|
|
336
|
+
test('schema with properties implies object', () => {
|
|
337
|
+
const schema = { properties: { a: { type: 'number' as const } } }
|
|
338
|
+
expect(coerceBySchema('{"a":1}', schema, 'config')).toEqual({ a: 1 })
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('schema with items implies array — JSON string parsed', () => {
|
|
342
|
+
const schema = { items: { type: 'string' as const } }
|
|
343
|
+
expect(coerceBySchema('["a","b"]', schema, 'list')).toEqual(['a', 'b'])
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('schema with items implies array — single value wrapped', () => {
|
|
347
|
+
const schema = { items: { type: 'number' as const } }
|
|
348
|
+
expect(coerceBySchema('42', schema, 'list')).toEqual([42])
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('schema with items implies array — repeated flags coerced', () => {
|
|
352
|
+
const schema = { items: { type: 'number' as const } }
|
|
353
|
+
expect(coerceBySchema(['1', '2'], schema, 'list')).toEqual([1, 2])
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('no type info returns value as-is', () => {
|
|
357
|
+
expect(coerceBySchema('hello', {}, 'val')).toBe('hello')
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('union types with array', () => {
|
|
362
|
+
test('["array", "null"] — JSON array string parsed as array', () => {
|
|
363
|
+
expect(coerceBySchema('[1,2]', { type: ['array', 'null'] }, 'val'))
|
|
364
|
+
.toEqual([1, 2])
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('["array", "null"] — empty string becomes null', () => {
|
|
368
|
+
expect(coerceBySchema('', { type: ['null', 'array'] }, 'val'))
|
|
369
|
+
.toBe(null)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('["string", "array"] — string value stays string (tried first)', () => {
|
|
373
|
+
expect(coerceBySchema('hello', { type: ['string', 'array'] }, 'val'))
|
|
374
|
+
.toBe('hello')
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
test('["array", "string"] — non-JSON string wraps into array', () => {
|
|
378
|
+
// When array is tried first: JSON.parse fails, coerceToArray throws,
|
|
379
|
+
// then string is tried and succeeds
|
|
380
|
+
expect(coerceBySchema('hello', { type: ['array', 'string'] }, 'val'))
|
|
381
|
+
.toEqual('hello')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('["array", "null"] — repeated flags accepted', () => {
|
|
385
|
+
expect(coerceBySchema(['a', 'b'], { type: ['array', 'null'] }, 'val'))
|
|
386
|
+
.toEqual(['a', 'b'])
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('["array", "null"] — repeated flags with items coerced', () => {
|
|
390
|
+
expect(coerceBySchema(['1', '2'], { type: ['array', 'null'], items: { type: 'number' } }, 'val'))
|
|
391
|
+
.toEqual([1, 2])
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
describe('null coercion edge cases', () => {
|
|
396
|
+
test('false does NOT coerce to null', () => {
|
|
397
|
+
expect(() => coerceBySchema(false, { type: 'null' }, 'val'))
|
|
398
|
+
.toThrow('expected empty string for null')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('["null", "boolean"] — false stays as false', () => {
|
|
402
|
+
expect(coerceBySchema(false, { type: ['null', 'boolean'] }, 'val'))
|
|
403
|
+
.toBe(false)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('["boolean", "null"] — false stays as false (tried first)', () => {
|
|
407
|
+
expect(coerceBySchema(false, { type: ['boolean', 'null'] }, 'val'))
|
|
408
|
+
.toBe(false)
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
})
|