mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix: allow omitted optional rpc schema fields with zod 4.4 (#1541)
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@
|
||||
"react-native-worklets": "^0.7.4",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "~4.3.6",
|
||||
"zod": "~4.4.3",
|
||||
"zustand": "^5.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+5
-5
@@ -87,8 +87,8 @@ importers:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
zod:
|
||||
specifier: ~4.3.6
|
||||
version: 4.3.6
|
||||
specifier: ~4.4.3
|
||||
version: 4.4.3
|
||||
zustand:
|
||||
specifier: ^5.0.13
|
||||
version: 5.0.13(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6))
|
||||
@@ -5637,8 +5637,8 @@ packages:
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
zod@4.4.3:
|
||||
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||
|
||||
zustand@5.0.13:
|
||||
resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==}
|
||||
@@ -12492,7 +12492,7 @@ snapshots:
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
zod@4.4.3: {}
|
||||
|
||||
zustand@5.0.13(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)):
|
||||
optionalDependencies:
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "~4.3.6",
|
||||
"zod": "~4.4.3",
|
||||
"zustand": "^5.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+13
-13
@@ -227,8 +227,8 @@ importers:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
zod:
|
||||
specifier: ~4.3.6
|
||||
version: 4.3.6
|
||||
specifier: ~4.4.3
|
||||
version: 4.4.3
|
||||
zustand:
|
||||
specifier: ^5.0.13
|
||||
version: 5.0.13(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
|
||||
@@ -244,7 +244,7 @@ importers:
|
||||
version: 1.59.1
|
||||
'@stablyai/playwright-test':
|
||||
specifier: ^2.1.14
|
||||
version: 2.1.14(@playwright/test@1.59.1)(zod@4.3.6)
|
||||
version: 2.1.14(@playwright/test@1.59.1)(zod@4.4.3)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.4
|
||||
version: 4.2.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.8.4))
|
||||
@@ -6376,8 +6376,8 @@ packages:
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
zod@4.4.3:
|
||||
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||
|
||||
zustand@5.0.13:
|
||||
resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==}
|
||||
@@ -8224,27 +8224,27 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@stablyai/playwright-base@2.1.14(@playwright/test@1.59.1)(zod@4.3.6)':
|
||||
'@stablyai/playwright-base@2.1.14(@playwright/test@1.59.1)(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@playwright/test': 1.59.1
|
||||
jpeg-js: 0.4.4
|
||||
p-retry: 4.6.2
|
||||
pngjs: 7.0.0
|
||||
optionalDependencies:
|
||||
zod: 4.3.6
|
||||
zod: 4.4.3
|
||||
|
||||
'@stablyai/playwright-test@2.1.14(@playwright/test@1.59.1)(zod@4.3.6)':
|
||||
'@stablyai/playwright-test@2.1.14(@playwright/test@1.59.1)(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@playwright/test': 1.59.1
|
||||
'@stablyai/playwright': 2.1.14(@playwright/test@1.59.1)(zod@4.3.6)
|
||||
'@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.3.6)
|
||||
'@stablyai/playwright': 2.1.14(@playwright/test@1.59.1)(zod@4.4.3)
|
||||
'@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.4.3)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@stablyai/playwright@2.1.14(@playwright/test@1.59.1)(zod@4.3.6)':
|
||||
'@stablyai/playwright@2.1.14(@playwright/test@1.59.1)(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@playwright/test': 1.59.1
|
||||
'@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.3.6)
|
||||
'@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.4.3)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
@@ -12882,7 +12882,7 @@ snapshots:
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
zod@4.4.3: {}
|
||||
|
||||
zustand@5.0.13(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
|
||||
optionalDependencies:
|
||||
|
||||
@@ -45,7 +45,7 @@ export const Scroll = BrowserTarget.extend({
|
||||
amount: z
|
||||
.unknown()
|
||||
.transform((v) => (typeof v === 'number' && v > 0 ? v : undefined))
|
||||
.pipe(z.number().optional())
|
||||
.pipe(z.union([z.number(), z.undefined()]))
|
||||
.optional()
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Screenshot = BrowserTarget.extend({
|
||||
format: z
|
||||
.unknown()
|
||||
.transform((v) => (v === 'png' || v === 'jpeg' ? v : undefined))
|
||||
.pipe(z.enum(['png', 'jpeg']).optional())
|
||||
.pipe(z.union([z.enum(['png', 'jpeg']), z.undefined()]))
|
||||
.optional()
|
||||
})
|
||||
|
||||
@@ -86,7 +86,7 @@ export const TabSwitch = BrowserTarget.extend({
|
||||
index: z
|
||||
.unknown()
|
||||
.transform((v) => (typeof v === 'number' ? v : undefined))
|
||||
.pipe(z.number().optional())
|
||||
.pipe(z.union([z.number(), z.undefined()]))
|
||||
.optional(),
|
||||
focus: z.boolean().optional()
|
||||
}).refine(
|
||||
@@ -118,7 +118,7 @@ export const TabClose = z.object({
|
||||
index: z
|
||||
.unknown()
|
||||
.transform((v) => (typeof v === 'number' ? v : undefined))
|
||||
.pipe(z.number().optional())
|
||||
.pipe(z.union([z.number(), z.undefined()]))
|
||||
.optional(),
|
||||
page: OptionalString,
|
||||
worktree: OptionalString
|
||||
@@ -161,7 +161,7 @@ export const Wait = BrowserTarget.extend({
|
||||
timeout: z
|
||||
.unknown()
|
||||
.transform((v) => (typeof v === 'number' && v > 0 ? v : undefined))
|
||||
.pipe(z.number().optional())
|
||||
.pipe(z.union([z.number(), z.undefined()]))
|
||||
.optional(),
|
||||
text: OptionalPlainString,
|
||||
url: OptionalPlainString,
|
||||
@@ -275,7 +275,7 @@ export const InterceptEnable = BrowserTarget.extend({
|
||||
patterns: z
|
||||
.unknown()
|
||||
.transform((v) => (Array.isArray(v) ? (v as string[]) : undefined))
|
||||
.pipe(z.array(z.string()).optional())
|
||||
.pipe(z.union([z.array(z.string()), z.undefined()]))
|
||||
.optional()
|
||||
})
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ const TerminalSplit = TerminalHandle.extend({
|
||||
direction: z
|
||||
.unknown()
|
||||
.transform((v) => (v === 'vertical' || v === 'horizontal' ? v : undefined))
|
||||
.pipe(z.enum(['vertical', 'horizontal']).optional())
|
||||
.pipe(z.union([z.enum(['vertical', 'horizontal']), z.undefined()]))
|
||||
.optional(),
|
||||
command: OptionalString
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@ const WorktreeCreate = z.object({
|
||||
.transform((v) =>
|
||||
typeof v === 'string' && (v === 'run' || v === 'skip' || v === 'inherit') ? v : undefined
|
||||
)
|
||||
.pipe(z.enum(['run', 'skip', 'inherit']).optional())
|
||||
.pipe(z.union([z.enum(['run', 'skip', 'inherit']), z.undefined()]))
|
||||
.optional(),
|
||||
// Why: mobile clients pass a startup command (e.g. 'claude') so the first
|
||||
// terminal pane launches the selected agent instead of an idle shell.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { z, type ZodType } from 'zod'
|
||||
import {
|
||||
BrowserTarget,
|
||||
OptionalBoolean,
|
||||
OptionalFiniteNumber,
|
||||
OptionalPlainString,
|
||||
OptionalPositiveInt,
|
||||
OptionalString
|
||||
} from './schemas'
|
||||
import {
|
||||
InterceptEnable,
|
||||
Screenshot,
|
||||
Scroll,
|
||||
TabClose,
|
||||
TabSwitch,
|
||||
Wait
|
||||
} from './methods/browser-schemas'
|
||||
import { TERMINAL_METHODS } from './methods/terminal'
|
||||
import { WORKTREE_METHODS } from './methods/worktree'
|
||||
|
||||
function expectParses(schema: ZodType, value: unknown): void {
|
||||
const result = schema.safeParse(value)
|
||||
expect(result.success, result.success ? undefined : JSON.stringify(result.error.issues)).toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
function methodParams(
|
||||
methods: readonly { name: string; params: ZodType | null }[],
|
||||
name: string
|
||||
): ZodType {
|
||||
const method = methods.find((candidate) => candidate.name === name)
|
||||
if (!method?.params) {
|
||||
throw new Error(`missing test method schema: ${name}`)
|
||||
}
|
||||
return method.params
|
||||
}
|
||||
|
||||
describe('RPC optional pipe schemas', () => {
|
||||
it('accepts omitted shared optional helper fields', () => {
|
||||
const schema = z.object({
|
||||
finite: OptionalFiniteNumber,
|
||||
positive: OptionalPositiveInt,
|
||||
string: OptionalString,
|
||||
plain: OptionalPlainString,
|
||||
boolean: OptionalBoolean
|
||||
})
|
||||
|
||||
expectParses(schema, {})
|
||||
})
|
||||
|
||||
it('accepts omitted browser optional fields while required fields are present', () => {
|
||||
expectParses(Scroll, { direction: 'down' })
|
||||
expectParses(Screenshot, {})
|
||||
expectParses(TabSwitch, { page: 'page-1' })
|
||||
expectParses(TabClose, {})
|
||||
expectParses(Wait, {})
|
||||
expectParses(InterceptEnable, {})
|
||||
expectParses(BrowserTarget, {})
|
||||
})
|
||||
|
||||
it('accepts omitted terminal and worktree optional fields while required fields are present', () => {
|
||||
expectParses(methodParams(TERMINAL_METHODS, 'terminal.split'), { terminal: 'terminal-1' })
|
||||
expectParses(methodParams(WORKTREE_METHODS, 'worktree.create'), { repo: 'repo-1' })
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,7 @@ import { z } from 'zod'
|
||||
export const OptionalFiniteNumber = z
|
||||
.unknown()
|
||||
.transform((value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined))
|
||||
.pipe(z.number().optional())
|
||||
.pipe(z.union([z.number(), z.undefined()]))
|
||||
.optional()
|
||||
|
||||
export const OptionalPositiveInt = z
|
||||
@@ -21,25 +21,25 @@ export const OptionalPositiveInt = z
|
||||
.transform((value) =>
|
||||
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined
|
||||
)
|
||||
.pipe(z.number().optional())
|
||||
.pipe(z.union([z.number(), z.undefined()]))
|
||||
.optional()
|
||||
|
||||
export const OptionalString = z
|
||||
.unknown()
|
||||
.transform((value) => (typeof value === 'string' && value.length > 0 ? value : undefined))
|
||||
.pipe(z.string().optional())
|
||||
.pipe(z.union([z.string(), z.undefined()]))
|
||||
.optional()
|
||||
|
||||
export const OptionalPlainString = z
|
||||
.unknown()
|
||||
.transform((value) => (typeof value === 'string' ? value : undefined))
|
||||
.pipe(z.string().optional())
|
||||
.pipe(z.union([z.string(), z.undefined()]))
|
||||
.optional()
|
||||
|
||||
export const OptionalBoolean = z
|
||||
.unknown()
|
||||
.transform((value) => (typeof value === 'boolean' ? value : undefined))
|
||||
.pipe(z.boolean().optional())
|
||||
.pipe(z.union([z.boolean(), z.undefined()]))
|
||||
.optional()
|
||||
|
||||
// Why: runtime handlers accept `linkedIssue: number | null | undefined` with
|
||||
|
||||
Reference in New Issue
Block a user