From 293c06839da0cc9bb8f2684aa90938608a60753b Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Tue, 21 Jul 2026 15:54:25 -0600 Subject: [PATCH] fix(cloud-exe-dev): reject non-decimal numeric options --- packages/cloud/exe-dev/src/index.test.ts | 48 ++++++++++++++++++++++++ packages/cloud/exe-dev/src/index.ts | 27 ++++++++----- 2 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 packages/cloud/exe-dev/src/index.test.ts diff --git a/packages/cloud/exe-dev/src/index.test.ts b/packages/cloud/exe-dev/src/index.test.ts new file mode 100644 index 00000000..64264b25 --- /dev/null +++ b/packages/cloud/exe-dev/src/index.test.ts @@ -0,0 +1,48 @@ +import { contractTestCloud } from '@profullstack/sh1pt-core/testing'; +import { describe, expect, it, vi } from 'vitest'; +import adapter from './index.js'; + +const ctx = { + secret: (key: string) => key === 'EXE_DEV_TOKEN' ? 'test-token' : undefined, + log: vi.fn(), + dryRun: true, +}; + +describe('exe.dev numeric options', () => { + it('quotes positive decimal CPU, memory, and disk values', async () => { + const quote = await adapter.quote(ctx as any, { + kind: 'cpu-vps', + cpu: '2', + memory: '4.5', + storage: '20', + } as any, {}); + + expect(quote.sku).toBe('2cpu-4.5gb-20gb'); + }); + + it('rejects non-decimal CPU, memory, and disk strings instead of falling back', async () => { + await expect(adapter.quote(ctx as any, { kind: 'cpu-vps', cpu: '1e2' } as any, {})) + .rejects.toThrow('exe.dev cpu must be a positive decimal number'); + await expect(adapter.quote(ctx as any, { kind: 'cpu-vps', memory: '0x10' } as any, {})) + .rejects.toThrow('exe.dev memory must be a positive decimal number'); + await expect(adapter.quote(ctx as any, { kind: 'cpu-vps', storage: '-1' } as any, {})) + .rejects.toThrow('exe.dev disk must be a positive decimal number'); + }); + + it('keeps dry-run provision args decimal and deterministic', async () => { + const instance = await adapter.provision(ctx as any, { + kind: 'cpu-vps', + cpu: '2', + memory: '4', + storage: '20', + } as any, {}); + + expect(instance.sku).toBe('2cpu-4gb-20gb'); + }); +}); + +contractTestCloud(adapter, { + sampleConfig: {}, + sampleSpec: { kind: 'cpu-vps', cpu: 2, memory: 4, storage: 20 }, + requiredSecrets: ['EXE_DEV_TOKEN'], +}); diff --git a/packages/cloud/exe-dev/src/index.ts b/packages/cloud/exe-dev/src/index.ts index 0088e6fc..20df8419 100644 --- a/packages/cloud/exe-dev/src/index.ts +++ b/packages/cloud/exe-dev/src/index.ts @@ -52,9 +52,9 @@ export default defineCloud({ }, async quote(ctx, spec, config) { - const cpu = positiveNumber(spec.cpu ?? config.defaultCpu, 2); - const memory = positiveNumber(spec.memory ?? config.defaultMemoryGb, 4); - const disk = positiveNumber(spec.storage ?? config.defaultDiskGb, 20); + const cpu = positiveNumber(spec.cpu ?? config.defaultCpu, 2, 'cpu'); + const memory = positiveNumber(spec.memory ?? config.defaultMemoryGb, 4, 'memory'); + const disk = positiveNumber(spec.storage ?? config.defaultDiskGb, 20, 'disk'); ctx.log(`exe.dev quote - cpu=${cpu} memory=${memory}GB disk=${disk}GB`); return { @@ -73,9 +73,9 @@ export default defineCloud({ } const name = safeName(`sh1pt-${Date.now()}`); - const cpu = positiveNumber(spec.cpu ?? config.defaultCpu, 2); - const memory = positiveNumber(spec.memory ?? config.defaultMemoryGb, 4); - const disk = positiveNumber(spec.storage ?? config.defaultDiskGb, 20); + const cpu = positiveNumber(spec.cpu ?? config.defaultCpu, 2, 'cpu'); + const memory = positiveNumber(spec.memory ?? config.defaultMemoryGb, 4, 'memory'); + const disk = positiveNumber(spec.storage ?? config.defaultDiskGb, 20, 'disk'); if (spec.region) { ctx.log(`exe.dev region is account-level; ignoring per-VM region ${spec.region}`, 'warn'); @@ -234,8 +234,17 @@ function quoteArg(value: string): string { return JSON.stringify(value); } -function positiveNumber(value: Numberish | undefined, fallback: number): number { +function positiveNumber(value: Numberish | undefined, fallback: number, label = 'value'): number { if (value === undefined || value === '') return fallback; - const n = typeof value === 'number' ? value : Number(value); - return Number.isFinite(n) && n > 0 ? n : fallback; + if (typeof value === 'number') { + if (Number.isFinite(value) && value > 0) return value; + throw new Error(`exe.dev ${label} must be a positive number`); + } + const text = value.trim(); + if (!/^(?:[1-9]\d*|0?\.\d+|[1-9]\d*\.\d+)$/.test(text)) { + throw new Error(`exe.dev ${label} must be a positive decimal number`); + } + const n = Number(text); + if (!Number.isFinite(n) || n <= 0) throw new Error(`exe.dev ${label} must be a positive number`); + return n; }