From e396c6aba519e708b10f46444d4420580eaa5adb Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 09:56:26 +0200 Subject: [PATCH 01/16] feat(config): task runner schemas and types --- packages/config/src/cli/task.context.ts | 82 ++++++++++++++++++++ packages/config/src/cli/task.env.ts | 19 +++++ packages/config/src/cli/task.ts | 21 +++++ packages/config/src/index.ts | 3 + packages/config/src/utils/identity.utils.ts | 22 ++++++ packages/config/src/utils/principal.utils.ts | 11 +++ packages/config/src/utils/zod.utils.ts | 7 ++ 7 files changed, 165 insertions(+) create mode 100644 packages/config/src/cli/task.context.ts create mode 100644 packages/config/src/cli/task.env.ts create mode 100644 packages/config/src/cli/task.ts create mode 100644 packages/config/src/utils/identity.utils.ts create mode 100644 packages/config/src/utils/zod.utils.ts diff --git a/packages/config/src/cli/task.context.ts b/packages/config/src/cli/task.context.ts new file mode 100644 index 000000000..1fb8fc765 --- /dev/null +++ b/packages/config/src/cli/task.context.ts @@ -0,0 +1,82 @@ +import type {Identity} from '@dfinity/agent'; +import type {Principal} from '@dfinity/principal'; +import type {PrincipalText} from '@dfinity/zod-schemas'; +import {PrincipalTextSchema} from '@dfinity/zod-schemas/dist/types/principal'; +import * as z from 'zod/v4'; +import {StrictIdentitySchema} from '../utils/identity.utils'; +import {StrictPrincipalSchema} from '../utils/principal.utils'; + +/** + * @see SatelliteContext + */ +const SatelliteContextSchema = z.strictObject({ + satelliteId: z.union([PrincipalTextSchema, StrictPrincipalSchema]), + identity: StrictIdentitySchema +}); + +/** + * Parameters required to call a Satellite from a task. + */ +export interface SatelliteContext { + /** + * The Satellite ID as defined in the `juno.config` file. + * + * This can be either a textual representation + * or a {@link Principal} instance. + */ + satelliteId: PrincipalText | Principal; + + /** + * The {@link Identity} used by the CLI for this execution, + * resolved according to the selected mode and profile. + */ + identity: Identity; +} + +/** + * @see OnTaskRunContext + */ +export const OnTaskRunContextSchema = z.strictObject({ + satellite: SatelliteContextSchema +}); + +/** + * The context for running a task. + */ +export interface OnTaskRunContext { + /** + * Context of the current Satellite, used to perform signed API calls + * and feature executions. + */ + satellite: SatelliteContext; +} + +/** + * @see TaskRunFunction + */ +const TaskRunFunctionSchema = z.function({ + input: z.tuple([OnTaskRunContextSchema]), + output: z.promise(z.void()).or(z.void()) +}); + +/** + * The function executed by a task. + */ +export type TaskRunFunction = (context: OnTaskRunContext) => void | Promise; + +/** + * @see OnTask + */ +export const OnTaskSchema = z.strictObject({ + run: TaskRunFunctionSchema +}); + +/** + * A task executed with `juno run`. + */ +export interface OnTask { + /** + * The function executed by the task. + */ + run: TaskRunFunction; +} diff --git a/packages/config/src/cli/task.env.ts b/packages/config/src/cli/task.env.ts new file mode 100644 index 000000000..139830b4c --- /dev/null +++ b/packages/config/src/cli/task.env.ts @@ -0,0 +1,19 @@ +import * as z from 'zod/v4'; +import {type JunoConfigEnv, JunoConfigEnvSchema} from '../types/juno.env'; + +/** + * @see OnTaskEnv + */ +export const OnTaskEnvSchema = JunoConfigEnvSchema.extend({ + profile: z.string().optional() +}); + +/** + * The environment available when running a task with `juno run`. + */ +export type OnTaskEnv = JunoConfigEnv & { + /** + * Optional profile (e.g. `personal`, `team`) used for execution. + */ + profile?: string; +}; diff --git a/packages/config/src/cli/task.ts b/packages/config/src/cli/task.ts new file mode 100644 index 000000000..ce27efb84 --- /dev/null +++ b/packages/config/src/cli/task.ts @@ -0,0 +1,21 @@ +import * as z from 'zod/v4'; +import {createFunctionSchema} from '../utils/zod.utils'; +import {type OnTask, OnTaskRunContextSchema, OnTaskSchema} from './task.context'; +import type {OnTaskEnv} from './task.env'; + +export const TaskFnSchema = z.function({ + input: z.tuple([OnTaskRunContextSchema]), + output: OnTaskSchema +}); +export type TaskFn = (context: OnTaskEnv) => OnTask; + +export const TaskFnOrObjectSchema = () => + z.union([OnTaskSchema, createFunctionSchema(TaskFnSchema)]); +export type TaskFnOrObject = OnTask | TaskFn; + +export function defineTask(task: OnTask): OnTask; +export function defineTask(task: TaskFn): TaskFn; +export function defineTask(task: TaskFnOrObject): TaskFnOrObject; +export function defineTask(task: TaskFnOrObject): TaskFnOrObject { + return task; +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 8d6ac275e..245814270 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,3 +1,6 @@ +export * from './cli/task'; +export * from './cli/task.context'; +export * from './cli/task.env'; export * from './console/config'; export * from './console/console.config'; export * from './pkg/juno.package'; diff --git a/packages/config/src/utils/identity.utils.ts b/packages/config/src/utils/identity.utils.ts new file mode 100644 index 000000000..508422ed2 --- /dev/null +++ b/packages/config/src/utils/identity.utils.ts @@ -0,0 +1,22 @@ +import {nonNullish} from '@junobuild/analytics/src/utils/dfinity/nullish.utils'; +import * as z from 'zod/v4/index'; +import {StrictPrincipalSchema} from './principal.utils'; + +/** + * Ensures an unknown object is an identity. + */ +export const StrictIdentitySchema = z + .unknown() + .refine( + (val) => + nonNullish(val) && + typeof val === 'object' && + 'transformRequest' in val && + typeof val.transformRequest === 'function' && + 'getPrincipal' in val && + typeof val.getPrincipal === 'function' && + StrictPrincipalSchema.safeParse(val.getPrincipal()).success, + { + message: 'Invalid Identity' + } + ); diff --git a/packages/config/src/utils/principal.utils.ts b/packages/config/src/utils/principal.utils.ts index 797be473e..1caa8bfe1 100644 --- a/packages/config/src/utils/principal.utils.ts +++ b/packages/config/src/utils/principal.utils.ts @@ -1,3 +1,4 @@ +import {Principal} from '@dfinity/principal'; import {PrincipalTextSchema} from '@dfinity/zod-schemas'; import * as z from 'zod/v4'; @@ -9,3 +10,13 @@ export const StrictPrincipalTextSchema = z .refine((val) => PrincipalTextSchema.safeParse(val).success, { message: 'Invalid textual representation of a Principal.' }); + +/** + * Ensures an unknown type is a Principal. + */ +export const StrictPrincipalSchema = z + .unknown() + .refine((val) => Principal.isPrincipal(val), { + message: 'Invalid Principal' + }) + .transform((val) => Principal.from(val)); diff --git a/packages/config/src/utils/zod.utils.ts b/packages/config/src/utils/zod.utils.ts new file mode 100644 index 000000000..7521267de --- /dev/null +++ b/packages/config/src/utils/zod.utils.ts @@ -0,0 +1,7 @@ +import * as z from 'zod/v4'; + +// TODO: Workaround source: https://github.com/colinhacks/zod/issues/4143#issuecomment-2845134912 +export const createFunctionSchema = (schema: T) => + z.custom[0]>((fn) => + schema.implement(fn as Parameters[0]) + ); From 71a3c730058ba980842865c2b1c82bee10fbe08d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 10:02:00 +0200 Subject: [PATCH 02/16] fix: no utils --- packages/config/src/utils/identity.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/config/src/utils/identity.utils.ts b/packages/config/src/utils/identity.utils.ts index 508422ed2..98afd9b51 100644 --- a/packages/config/src/utils/identity.utils.ts +++ b/packages/config/src/utils/identity.utils.ts @@ -1,4 +1,3 @@ -import {nonNullish} from '@junobuild/analytics/src/utils/dfinity/nullish.utils'; import * as z from 'zod/v4/index'; import {StrictPrincipalSchema} from './principal.utils'; @@ -9,7 +8,8 @@ export const StrictIdentitySchema = z .unknown() .refine( (val) => - nonNullish(val) && + val !== undefined && + val !== null && typeof val === 'object' && 'transformRequest' in val && typeof val.transformRequest === 'function' && From 54bf0451b749519b5e56513f1f8848167aff8fe6 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 12:18:15 +0200 Subject: [PATCH 03/16] feat: flatten config --- packages/config/src/cli/task.context.ts | 47 +++++++------------------ packages/config/src/cli/task.ts | 4 +-- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/packages/config/src/cli/task.context.ts b/packages/config/src/cli/task.context.ts index 1fb8fc765..dedc7955a 100644 --- a/packages/config/src/cli/task.context.ts +++ b/packages/config/src/cli/task.context.ts @@ -1,30 +1,27 @@ import type {Identity} from '@dfinity/agent'; import type {Principal} from '@dfinity/principal'; -import type {PrincipalText} from '@dfinity/zod-schemas'; -import {PrincipalTextSchema} from '@dfinity/zod-schemas/dist/types/principal'; import * as z from 'zod/v4'; import {StrictIdentitySchema} from '../utils/identity.utils'; import {StrictPrincipalSchema} from '../utils/principal.utils'; /** - * @see SatelliteContext + * @see OnRunContext */ -const SatelliteContextSchema = z.strictObject({ - satelliteId: z.union([PrincipalTextSchema, StrictPrincipalSchema]), +export const OnRunContextSchema = z.strictObject({ + satelliteId: StrictPrincipalSchema, identity: StrictIdentitySchema }); /** - * Parameters required to call a Satellite from a task. + * The context for running a task. */ -export interface SatelliteContext { +export interface OnRunContext { /** * The Satellite ID as defined in the `juno.config` file. * - * This can be either a textual representation - * or a {@link Principal} instance. + * A {@link Principal} instance. */ - satelliteId: PrincipalText | Principal; + satelliteId: Principal; /** * The {@link Identity} used by the CLI for this execution, @@ -34,41 +31,23 @@ export interface SatelliteContext { } /** - * @see OnTaskRunContext - */ -export const OnTaskRunContextSchema = z.strictObject({ - satellite: SatelliteContextSchema -}); - -/** - * The context for running a task. - */ -export interface OnTaskRunContext { - /** - * Context of the current Satellite, used to perform signed API calls - * and feature executions. - */ - satellite: SatelliteContext; -} - -/** - * @see TaskRunFunction + * @see RunFunction */ -const TaskRunFunctionSchema = z.function({ - input: z.tuple([OnTaskRunContextSchema]), +const RunFunctionSchema = z.function({ + input: z.tuple([OnRunContextSchema]), output: z.promise(z.void()).or(z.void()) }); /** * The function executed by a task. */ -export type TaskRunFunction = (context: OnTaskRunContext) => void | Promise; +export type RunFunction = (context: OnRunContext) => void | Promise; /** * @see OnTask */ export const OnTaskSchema = z.strictObject({ - run: TaskRunFunctionSchema + run: RunFunctionSchema }); /** @@ -78,5 +57,5 @@ export interface OnTask { /** * The function executed by the task. */ - run: TaskRunFunction; + run: RunFunction; } diff --git a/packages/config/src/cli/task.ts b/packages/config/src/cli/task.ts index ce27efb84..300db4aca 100644 --- a/packages/config/src/cli/task.ts +++ b/packages/config/src/cli/task.ts @@ -1,10 +1,10 @@ import * as z from 'zod/v4'; import {createFunctionSchema} from '../utils/zod.utils'; -import {type OnTask, OnTaskRunContextSchema, OnTaskSchema} from './task.context'; +import {type OnTask, OnRunContextSchema, OnTaskSchema} from './task.context'; import type {OnTaskEnv} from './task.env'; export const TaskFnSchema = z.function({ - input: z.tuple([OnTaskRunContextSchema]), + input: z.tuple([OnRunContextSchema]), output: OnTaskSchema }); export type TaskFn = (context: OnTaskEnv) => OnTask; From 347063cc1b741019cdb858504674a3bfa4dc48db Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 12:30:52 +0200 Subject: [PATCH 04/16] feat: no fn --- packages/config/src/cli/task.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/config/src/cli/task.ts b/packages/config/src/cli/task.ts index 300db4aca..2e9b6ecd2 100644 --- a/packages/config/src/cli/task.ts +++ b/packages/config/src/cli/task.ts @@ -9,8 +9,7 @@ export const TaskFnSchema = z.function({ }); export type TaskFn = (context: OnTaskEnv) => OnTask; -export const TaskFnOrObjectSchema = () => - z.union([OnTaskSchema, createFunctionSchema(TaskFnSchema)]); +export const TaskFnOrObjectSchema = z.union([OnTaskSchema, createFunctionSchema(TaskFnSchema)]); export type TaskFnOrObject = OnTask | TaskFn; export function defineTask(task: OnTask): OnTask; From f236c92fa94331bc06839233d94acc31657742e7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 13:21:19 +0200 Subject: [PATCH 05/16] fix: zod import mess --- packages/config/src/utils/identity.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/src/utils/identity.utils.ts b/packages/config/src/utils/identity.utils.ts index 98afd9b51..53d9f2cc5 100644 --- a/packages/config/src/utils/identity.utils.ts +++ b/packages/config/src/utils/identity.utils.ts @@ -1,4 +1,4 @@ -import * as z from 'zod/v4/index'; +import * as z from 'zod/v4'; import {StrictPrincipalSchema} from './principal.utils'; /** From a1fee589adaaef165bb415d3116799390c277f6d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 13:42:08 +0200 Subject: [PATCH 06/16] fix: zod pain --- scripts/update-version.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/update-version.mjs b/scripts/update-version.mjs index 30de5b62a..3cac839bd 100644 --- a/scripts/update-version.mjs +++ b/scripts/update-version.mjs @@ -44,8 +44,8 @@ const updateVersion = async () => { // Peer dependencies need to point to wip references - e.g. @junobuild/utils@0.0.1-next const peerDependencies = Object.entries(packageJson.peerDependencies ?? {}).reduce( - (acc, [key, _value]) => { - acc[key] = `*`; + (acc, [key, value]) => { + acc[key] = key === 'zod' ? value : `*`; return acc; }, {} From 29729f7eabd530ced59a5844fbad585016a8024c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 13:50:56 +0200 Subject: [PATCH 07/16] chore: fck it --- scripts/update-version.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/update-version.mjs b/scripts/update-version.mjs index 3cac839bd..30de5b62a 100644 --- a/scripts/update-version.mjs +++ b/scripts/update-version.mjs @@ -44,8 +44,8 @@ const updateVersion = async () => { // Peer dependencies need to point to wip references - e.g. @junobuild/utils@0.0.1-next const peerDependencies = Object.entries(packageJson.peerDependencies ?? {}).reduce( - (acc, [key, value]) => { - acc[key] = key === 'zod' ? value : `*`; + (acc, [key, _value]) => { + acc[key] = `*`; return acc; }, {} From 47d31f0cd7800854fd8fec1db0fddf041e9c7808 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 14:13:33 +0200 Subject: [PATCH 08/16] fix: gosh --- packages/config/src/cli/task.context.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/config/src/cli/task.context.ts b/packages/config/src/cli/task.context.ts index dedc7955a..560930aa2 100644 --- a/packages/config/src/cli/task.context.ts +++ b/packages/config/src/cli/task.context.ts @@ -3,6 +3,7 @@ import type {Principal} from '@dfinity/principal'; import * as z from 'zod/v4'; import {StrictIdentitySchema} from '../utils/identity.utils'; import {StrictPrincipalSchema} from '../utils/principal.utils'; +import {createFunctionSchema} from '../utils/zod.utils'; /** * @see OnRunContext @@ -37,7 +38,6 @@ const RunFunctionSchema = z.function({ input: z.tuple([OnRunContextSchema]), output: z.promise(z.void()).or(z.void()) }); - /** * The function executed by a task. */ @@ -47,7 +47,7 @@ export type RunFunction = (context: OnRunContext) => void | Promise; * @see OnTask */ export const OnTaskSchema = z.strictObject({ - run: RunFunctionSchema + run: createFunctionSchema(RunFunctionSchema) }); /** From f96e2d687c615486f786396b3a5ef9d4c687b066 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 19:45:38 +0200 Subject: [PATCH 09/16] feat: rename task to run --- .../cli/{task.context.ts => run.context.ts} | 11 +++++----- .../src/cli/{task.env.ts => run.env.ts} | 8 ++++---- packages/config/src/cli/run.ts | 20 +++++++++++++++++++ packages/config/src/cli/task.ts | 20 ------------------- packages/config/src/index.ts | 6 +++--- 5 files changed, 33 insertions(+), 32 deletions(-) rename packages/config/src/cli/{task.context.ts => run.context.ts} (83%) rename packages/config/src/cli/{task.env.ts => run.env.ts} (58%) create mode 100644 packages/config/src/cli/run.ts delete mode 100644 packages/config/src/cli/task.ts diff --git a/packages/config/src/cli/task.context.ts b/packages/config/src/cli/run.context.ts similarity index 83% rename from packages/config/src/cli/task.context.ts rename to packages/config/src/cli/run.context.ts index 560930aa2..ee1b4fa17 100644 --- a/packages/config/src/cli/task.context.ts +++ b/packages/config/src/cli/run.context.ts @@ -44,18 +44,19 @@ const RunFunctionSchema = z.function({ export type RunFunction = (context: OnRunContext) => void | Promise; /** - * @see OnTask + * @see OnRun */ -export const OnTaskSchema = z.strictObject({ +export const OnRunSchema = z.strictObject({ run: createFunctionSchema(RunFunctionSchema) }); /** - * A task executed with `juno run`. + * A runner (job) executed with `juno run`. */ -export interface OnTask { +export interface OnRun { /** - * The function executed by the task. + * The function that will be executed and called with parameters + * inherited from your configuration and CLI. */ run: RunFunction; } diff --git a/packages/config/src/cli/task.env.ts b/packages/config/src/cli/run.env.ts similarity index 58% rename from packages/config/src/cli/task.env.ts rename to packages/config/src/cli/run.env.ts index 139830b4c..812e89784 100644 --- a/packages/config/src/cli/task.env.ts +++ b/packages/config/src/cli/run.env.ts @@ -2,16 +2,16 @@ import * as z from 'zod/v4'; import {type JunoConfigEnv, JunoConfigEnvSchema} from '../types/juno.env'; /** - * @see OnTaskEnv + * @see OnRunEnv */ -export const OnTaskEnvSchema = JunoConfigEnvSchema.extend({ +export const OnRunEnvSchema = JunoConfigEnvSchema.extend({ profile: z.string().optional() }); /** - * The environment available when running a task with `juno run`. + * The environment available when executing `juno run`. */ -export type OnTaskEnv = JunoConfigEnv & { +export type OnRunEnv = JunoConfigEnv & { /** * Optional profile (e.g. `personal`, `team`) used for execution. */ diff --git a/packages/config/src/cli/run.ts b/packages/config/src/cli/run.ts new file mode 100644 index 000000000..1d37c7072 --- /dev/null +++ b/packages/config/src/cli/run.ts @@ -0,0 +1,20 @@ +import * as z from 'zod/v4'; +import {createFunctionSchema} from '../utils/zod.utils'; +import {type OnRun, OnRunContextSchema, OnRunSchema} from './run.context'; +import type {OnRunEnv} from './run.env'; + +export const RunFnSchema = z.function({ + input: z.tuple([OnRunContextSchema]), + output: OnRunSchema +}); +export type RunFn = (context: OnRunEnv) => OnRun; + +export const RunFnOrObjectSchema = z.union([OnRunSchema, createFunctionSchema(RunFnSchema)]); +export type RunFnOrObject = OnRun | RunFn; + +export function defineRun(run: OnRun): OnRun; +export function defineRun(run: RunFn): RunFn; +export function defineRun(run: RunFnOrObject): RunFnOrObject; +export function defineRun(run: RunFnOrObject): RunFnOrObject { + return run; +} diff --git a/packages/config/src/cli/task.ts b/packages/config/src/cli/task.ts deleted file mode 100644 index 2e9b6ecd2..000000000 --- a/packages/config/src/cli/task.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as z from 'zod/v4'; -import {createFunctionSchema} from '../utils/zod.utils'; -import {type OnTask, OnRunContextSchema, OnTaskSchema} from './task.context'; -import type {OnTaskEnv} from './task.env'; - -export const TaskFnSchema = z.function({ - input: z.tuple([OnRunContextSchema]), - output: OnTaskSchema -}); -export type TaskFn = (context: OnTaskEnv) => OnTask; - -export const TaskFnOrObjectSchema = z.union([OnTaskSchema, createFunctionSchema(TaskFnSchema)]); -export type TaskFnOrObject = OnTask | TaskFn; - -export function defineTask(task: OnTask): OnTask; -export function defineTask(task: TaskFn): TaskFn; -export function defineTask(task: TaskFnOrObject): TaskFnOrObject; -export function defineTask(task: TaskFnOrObject): TaskFnOrObject { - return task; -} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 245814270..8022ccd3f 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,6 +1,6 @@ -export * from './cli/task'; -export * from './cli/task.context'; -export * from './cli/task.env'; +export * from './cli/run'; +export * from './cli/run.context'; +export * from './cli/run.env'; export * from './console/config'; export * from './console/console.config'; export * from './pkg/juno.package'; From c9399442ebba3d34af80c2e80ab4b16a02f9cc8f Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 19:48:09 +0200 Subject: [PATCH 10/16] feat: container --- packages/config/src/cli/run.context.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/config/src/cli/run.context.ts b/packages/config/src/cli/run.context.ts index ee1b4fa17..cbc5de687 100644 --- a/packages/config/src/cli/run.context.ts +++ b/packages/config/src/cli/run.context.ts @@ -10,7 +10,8 @@ import {createFunctionSchema} from '../utils/zod.utils'; */ export const OnRunContextSchema = z.strictObject({ satelliteId: StrictPrincipalSchema, - identity: StrictIdentitySchema + identity: StrictIdentitySchema, + container: z.union([z.string(), z.boolean()]).optional() }); /** @@ -29,6 +30,13 @@ export interface OnRunContext { * resolved according to the selected mode and profile. */ identity: Identity; + + /** + * Specifies whether the actor is calling the local development Docker container or provides the container URL. + * @type {boolean | string} + * @optional + */ + container?: boolean | string; } /** From 47621677e4acc09bde848ee44033904bbf367397 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 23 Sep 2025 22:11:10 +0200 Subject: [PATCH 11/16] feat: improvements and tests --- .../config/src/tests/cli/run.context.spec.ts | 138 ++++++++++++++++++ packages/config/src/tests/cli/run.env.spec.ts | 38 +++++ .../config/src/tests/mocks/identity.mock.ts | 15 ++ .../config/src/tests/mocks/principal.mock.ts | 4 + packages/config/src/utils/principal.utils.ts | 19 ++- 5 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 packages/config/src/tests/cli/run.context.spec.ts create mode 100644 packages/config/src/tests/cli/run.env.spec.ts create mode 100644 packages/config/src/tests/mocks/identity.mock.ts diff --git a/packages/config/src/tests/cli/run.context.spec.ts b/packages/config/src/tests/cli/run.context.spec.ts new file mode 100644 index 000000000..ae6cfe311 --- /dev/null +++ b/packages/config/src/tests/cli/run.context.spec.ts @@ -0,0 +1,138 @@ +import {Principal} from '@dfinity/principal'; +import {OnRun, OnRunContextSchema, OnRunSchema} from '../../cli/run.context'; +import {mockIdentity} from '../mocks/identity.mock'; +import {mockUserIdPrincipal} from '../mocks/principal.mock'; + +describe('run.context', () => { + describe('OnRunContextSchema', () => { + it('accepts a valid context with required fields only', () => { + const result = OnRunContextSchema.safeParse({ + satelliteId: mockUserIdPrincipal, + identity: mockIdentity + }); + expect(result.success).toBe(true); + }); + + it('accepts with container as boolean', () => { + const result = OnRunContextSchema.safeParse({ + satelliteId: mockUserIdPrincipal, + identity: mockIdentity, + container: true + }); + expect(result.success).toBe(true); + }); + + it('accepts with container as string (URL)', () => { + const result = OnRunContextSchema.safeParse({ + satelliteId: mockUserIdPrincipal, + identity: mockIdentity, + container: 'http://localhost:4943' + }); + expect(result.success).toBe(true); + }); + + it('rejects if satelliteId is missing', () => { + const result = OnRunContextSchema.safeParse({ + identity: mockIdentity + }); + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0].path).toEqual(['satelliteId']); + } + }); + + it('rejects if identity is missing', () => { + const result = OnRunContextSchema.safeParse({ + satelliteId: mockUserIdPrincipal + }); + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0].path).toEqual(['identity']); + } + }); + + it('rejects if container is not boolean or string', () => { + const result = OnRunContextSchema.safeParse({ + satelliteId: mockUserIdPrincipal, + identity: mockIdentity, + container: 123 + }); + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0].path).toEqual(['container']); + } + }); + + it('is strict: rejects unknown keys', () => { + const result = OnRunContextSchema.safeParse({ + satelliteId: mockUserIdPrincipal, + identity: mockIdentity, + extra: 'nope' + }); + expect(result.success).toBe(false); + }); + }); + + describe('OnRunSchema', () => { + it('accepts a sync run function', () => { + const onRun: OnRun = { + run: (ctx) => { + const parsed = OnRunContextSchema.parse(ctx); + expect(parsed.satelliteId.toText()).toBe('2vxsx-fae'); + } + }; + const res = OnRunSchema.safeParse(onRun); + expect(res.success).toBe(true); + }); + + it('accepts an async run function', async () => { + const onRun: OnRun = { + run: async (ctx) => { + const parsed = OnRunContextSchema.parse(ctx); + expect(parsed.identity).toBeTruthy(); + } + }; + const res = OnRunSchema.safeParse(onRun); + expect(res.success).toBe(true); + }); + + it('rejects when run is not a function', () => { + expect(() => + OnRunSchema.parse({ + run: 'not-a-function' + }) + ).toThrow(); + }); + + it('rejects when run is missing', () => { + expect(() => OnRunSchema.parse({})).toThrow(); + }); + + it('executes a validated run function at runtime', async () => { + const onRun: OnRun = { + run: async (ctx: unknown) => { + const parsed = OnRunContextSchema.parse(ctx); + expect(parsed.satelliteId).toBeInstanceOf(Principal); + } + }; + + const parsed = OnRunSchema.parse(onRun); + await parsed.run({ + satelliteId: Principal.fromText('jx5yt-yyaaa-aaaal-abzbq-cai'), + identity: mockIdentity + }); + }); + + it('is strict: rejects unknown keys at top-level OnRun object', () => { + const onRun: any = { + run: () => {}, + extra: 'nope' + }; + const res = OnRunSchema.safeParse(onRun); + expect(res.success).toBe(false); + }); + }); +}); diff --git a/packages/config/src/tests/cli/run.env.spec.ts b/packages/config/src/tests/cli/run.env.spec.ts new file mode 100644 index 000000000..3a5b67ba8 --- /dev/null +++ b/packages/config/src/tests/cli/run.env.spec.ts @@ -0,0 +1,38 @@ +import {OnRunEnvSchema} from '../../cli/run.env'; + +describe('run.env', () => { + describe('OnRunEnvSchema', () => { + it('accepts valid env with mode only', () => { + const result = OnRunEnvSchema.safeParse({mode: 'production'}); + expect(result.success).toBe(true); + }); + + it('accepts valid env with mode and profile', () => { + const result = OnRunEnvSchema.safeParse({mode: 'staging', profile: 'team'}); + expect(result.success).toBe(true); + }); + + it('rejects if profile is not a string', () => { + const result = OnRunEnvSchema.safeParse({mode: 'production', profile: 123}); + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0].path).toEqual(['profile']); + } + }); + + it('rejects if mode is missing', () => { + const result = OnRunEnvSchema.safeParse({profile: 'personal'}); + expect(result.success).toBe(false); + + if (!result.success) { + expect(result.error.issues[0].path).toEqual(['mode']); + } + }); + + it('rejects null/undefined mode', () => { + expect(OnRunEnvSchema.safeParse({mode: null}).success).toBe(false); + expect(OnRunEnvSchema.safeParse({mode: undefined}).success).toBe(false); + }); + }); +}); diff --git a/packages/config/src/tests/mocks/identity.mock.ts b/packages/config/src/tests/mocks/identity.mock.ts new file mode 100644 index 000000000..b05261692 --- /dev/null +++ b/packages/config/src/tests/mocks/identity.mock.ts @@ -0,0 +1,15 @@ +import type {Identity} from '@dfinity/agent'; +import {mockUserIdPrincipal} from './principal.mock'; + +const transformRequest = () => { + console.error( + 'It looks like the agent is trying to make a request that should have been mocked at', + new Error().stack + ); + throw new Error('Not implemented'); +}; + +export const mockIdentity = { + getPrincipal: () => mockUserIdPrincipal, + transformRequest +} as unknown as Identity; diff --git a/packages/config/src/tests/mocks/principal.mock.ts b/packages/config/src/tests/mocks/principal.mock.ts index cfd913ed3..d2f99d137 100644 --- a/packages/config/src/tests/mocks/principal.mock.ts +++ b/packages/config/src/tests/mocks/principal.mock.ts @@ -1,2 +1,6 @@ +import {Principal} from '@dfinity/principal'; + export const mockModuleIdText = 'ucnx3-aqaaa-aaaal-ab3ea-cai'; export const mockUserIdText = 'xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe'; + +export const mockUserIdPrincipal = Principal.fromText(mockModuleIdText); diff --git a/packages/config/src/utils/principal.utils.ts b/packages/config/src/utils/principal.utils.ts index 1caa8bfe1..3a139e5b1 100644 --- a/packages/config/src/utils/principal.utils.ts +++ b/packages/config/src/utils/principal.utils.ts @@ -14,9 +14,16 @@ export const StrictPrincipalTextSchema = z /** * Ensures an unknown type is a Principal. */ -export const StrictPrincipalSchema = z - .unknown() - .refine((val) => Principal.isPrincipal(val), { - message: 'Invalid Principal' - }) - .transform((val) => Principal.from(val)); +export const StrictPrincipalSchema = z.unknown().transform((val, ctx): Principal => { + if (Principal.isPrincipal(val)) { + return Principal.from(val); + } + + ctx.issues.push({ + code: 'custom', + message: 'Invalid Principal', + input: val + }); + + return z.NEVER; +}); From 2dee8a3b628719d1ddcf739983cf0c2a8a044b24 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 24 Sep 2025 06:20:19 +0200 Subject: [PATCH 12/16] test: defineRUn --- packages/config/src/tests/cli/run.spec.ts | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 packages/config/src/tests/cli/run.spec.ts diff --git a/packages/config/src/tests/cli/run.spec.ts b/packages/config/src/tests/cli/run.spec.ts new file mode 100644 index 000000000..dcfce7157 --- /dev/null +++ b/packages/config/src/tests/cli/run.spec.ts @@ -0,0 +1,65 @@ +// A minimal valid OnRun object +import {defineRun, RunFnOrObjectSchema} from '../../cli/run'; +import {OnRunContextSchema, OnRunSchema} from '../../cli/run.context'; +import {mockIdentity} from '../mocks/identity.mock'; +import {mockUserIdPrincipal} from '../mocks/principal.mock'; + +describe('run', () => { + const validOnRunObj = { + run: (_ctx: unknown) => {} + }; + + describe('RunFnOrObjectSchema', () => { + it('accepts an OnRun object', () => { + const res = RunFnOrObjectSchema.safeParse(validOnRunObj); + expect(res.success).toBe(true); + + if (res.success) { + expect(OnRunSchema.safeParse(res.data).success).toBe(true); + } + }); + + it('accepts a function returning a valid OnRun object', () => { + const fn = (_env: unknown) => validOnRunObj; + const res = RunFnOrObjectSchema.safeParse(fn); + expect(res.success).toBe(true); + }); + + it('throws when input is not a function nor a valid OnRun object', () => { + expect(() => RunFnOrObjectSchema.parse('nope')).toThrow(); + }); + + it('runtime: call the function form and validate the returned OnRun', async () => { + const fn = (_env: unknown) => ({ + run: async (ctx: unknown) => { + const parsed = OnRunContextSchema.parse(ctx); + expect(parsed.satelliteId.toText()).toBe(mockUserIdPrincipal.toText()); + } + }); + + const parsedUnion = RunFnOrObjectSchema.parse(fn); + const onRun = (parsedUnion as typeof fn)({}); + + const valid = OnRunSchema.parse(onRun); + + await valid.run({ + satelliteId: mockUserIdPrincipal, + identity: mockIdentity + }); + }); + }); + + describe('defineRun', () => { + it('returns the same OnRun object reference', () => { + const res = defineRun(validOnRunObj); + expect(res).toBe(validOnRunObj); + expect(OnRunSchema.safeParse(res).success).toBe(true); + }); + + it('returns the same function reference', () => { + const fn = (_env: unknown) => validOnRunObj; + const res = defineRun(fn); + expect(res).toBe(fn); + }); + }); +}); From 1d5e9751693347b0312d9a4af416ef6a6cd72387 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 24 Sep 2025 06:20:33 +0200 Subject: [PATCH 13/16] chore: remove cmt --- packages/config/src/tests/cli/run.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/config/src/tests/cli/run.spec.ts b/packages/config/src/tests/cli/run.spec.ts index dcfce7157..838601138 100644 --- a/packages/config/src/tests/cli/run.spec.ts +++ b/packages/config/src/tests/cli/run.spec.ts @@ -1,4 +1,3 @@ -// A minimal valid OnRun object import {defineRun, RunFnOrObjectSchema} from '../../cli/run'; import {OnRunContextSchema, OnRunSchema} from '../../cli/run.context'; import {mockIdentity} from '../mocks/identity.mock'; From 8dc1eafe379d1a6860e5b93086a577800837e667 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 24 Sep 2025 06:26:47 +0200 Subject: [PATCH 14/16] feat: test and todo --- .../config/src/tests/mocks/principal.mock.ts | 2 +- .../src/tests/utils/identity.utils.spec.ts | 46 +++++++++++++++++++ .../src/tests/utils/principal.utils.spec.ts | 36 ++++++++++++++- packages/config/src/utils/zod.utils.ts | 1 + 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 packages/config/src/tests/utils/identity.utils.spec.ts diff --git a/packages/config/src/tests/mocks/principal.mock.ts b/packages/config/src/tests/mocks/principal.mock.ts index d2f99d137..2dddcc05b 100644 --- a/packages/config/src/tests/mocks/principal.mock.ts +++ b/packages/config/src/tests/mocks/principal.mock.ts @@ -3,4 +3,4 @@ import {Principal} from '@dfinity/principal'; export const mockModuleIdText = 'ucnx3-aqaaa-aaaal-ab3ea-cai'; export const mockUserIdText = 'xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe'; -export const mockUserIdPrincipal = Principal.fromText(mockModuleIdText); +export const mockUserIdPrincipal = Principal.fromText(mockUserIdText); diff --git a/packages/config/src/tests/utils/identity.utils.spec.ts b/packages/config/src/tests/utils/identity.utils.spec.ts new file mode 100644 index 000000000..e5e0e9043 --- /dev/null +++ b/packages/config/src/tests/utils/identity.utils.spec.ts @@ -0,0 +1,46 @@ +import {StrictIdentitySchema} from '../../utils/identity.utils'; +import {mockIdentity} from '../mocks/identity.mock'; +import {mockUserIdPrincipal} from '../mocks/principal.mock'; + +describe('StrictIdentitySchema', () => { + it('accepts a valid identity object', () => { + const res = StrictIdentitySchema.safeParse(mockIdentity); + expect(res.success).toBe(true); + }); + + it('rejects if getPrincipal is missing', () => { + const res = StrictIdentitySchema.safeParse({ + transformRequest: () => {} + }); + expect(res.success).toBe(false); + if (!res.success) { + expect(res.error.issues[0].message).toBe('Invalid Identity'); + } + }); + + it('rejects if transformRequest is missing', () => { + const res = StrictIdentitySchema.safeParse({ + getPrincipal: () => mockUserIdPrincipal + }); + expect(res.success).toBe(false); + }); + + it('rejects if getPrincipal does not return a Principal', () => { + const res = StrictIdentitySchema.safeParse({ + transformRequest: () => {}, + getPrincipal: () => 'not-a-principal' + }); + expect(res.success).toBe(false); + }); + + it('rejects null and undefined', () => { + expect(StrictIdentitySchema.safeParse(null).success).toBe(false); + expect(StrictIdentitySchema.safeParse(undefined).success).toBe(false); + }); + + it('rejects primitives', () => { + expect(StrictIdentitySchema.safeParse(123).success).toBe(false); + expect(StrictIdentitySchema.safeParse('foo').success).toBe(false); + expect(StrictIdentitySchema.safeParse(true).success).toBe(false); + }); +}); diff --git a/packages/config/src/tests/utils/principal.utils.spec.ts b/packages/config/src/tests/utils/principal.utils.spec.ts index 3878fb908..6da8d2682 100644 --- a/packages/config/src/tests/utils/principal.utils.spec.ts +++ b/packages/config/src/tests/utils/principal.utils.spec.ts @@ -1,5 +1,6 @@ -import {StrictPrincipalTextSchema} from '../../utils/principal.utils'; -import {mockModuleIdText} from '../mocks/principal.mock'; +import {Principal} from '@dfinity/principal'; +import {StrictPrincipalSchema, StrictPrincipalTextSchema} from '../../utils/principal.utils'; +import {mockModuleIdText, mockUserIdPrincipal, mockUserIdText} from '../mocks/principal.mock'; describe('principal.utils', () => { describe('StrictPrincipalTextSchema', () => { @@ -18,4 +19,35 @@ describe('principal.utils', () => { } }); }); + + describe('StrictPrincipalSchema', () => { + it('accepts a Principal instance and returns a Principal', () => { + const res = StrictPrincipalSchema.safeParse(mockUserIdPrincipal); + expect(res.success).toBe(true); + if (res.success) { + expect(Principal.isPrincipal(res.data)).toBeTruthy(); + expect(res.data.toText()).toBe(mockUserIdText); + } + }); + + it('rejects a principal as string text', () => { + const res = StrictPrincipalSchema.safeParse(mockUserIdText); + expect(res.success).toBe(false); + if (!res.success) { + expect(res.error.issues[0].message).toBe('Invalid Principal'); + expect(res.error.issues[0].path).toEqual([]); + } + }); + + it('rejects null and undefined', () => { + expect(StrictPrincipalSchema.safeParse(null).success).toBe(false); + expect(StrictPrincipalSchema.safeParse(undefined).success).toBe(false); + }); + + it('rejects non-principal objects', () => { + expect(StrictPrincipalSchema.safeParse({} as any).success).toBe(false); + expect(StrictPrincipalSchema.safeParse(123 as any).success).toBe(false); + expect(StrictPrincipalSchema.safeParse(new Uint8Array([1, 2, 3]) as any).success).toBe(false); + }); + }); }); diff --git a/packages/config/src/utils/zod.utils.ts b/packages/config/src/utils/zod.utils.ts index 7521267de..4871adca7 100644 --- a/packages/config/src/utils/zod.utils.ts +++ b/packages/config/src/utils/zod.utils.ts @@ -1,6 +1,7 @@ import * as z from 'zod/v4'; // TODO: Workaround source: https://github.com/colinhacks/zod/issues/4143#issuecomment-2845134912 +// TODO: Duplicates the helper in @junobuild/functions. Both should be removed once we migrate to latest v4 version of Zod export const createFunctionSchema = (schema: T) => z.custom[0]>((fn) => schema.implement(fn as Parameters[0]) From 154c14bf721d72f9a1fea6e1b4ec05add00bcd39 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 24 Sep 2025 06:27:16 +0200 Subject: [PATCH 15/16] chore: lint --- packages/config/src/utils/principal.utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/config/src/utils/principal.utils.ts b/packages/config/src/utils/principal.utils.ts index 3a139e5b1..a808af4a0 100644 --- a/packages/config/src/utils/principal.utils.ts +++ b/packages/config/src/utils/principal.utils.ts @@ -14,6 +14,7 @@ export const StrictPrincipalTextSchema = z /** * Ensures an unknown type is a Principal. */ +// eslint-disable-next-line local-rules/prefer-object-params export const StrictPrincipalSchema = z.unknown().transform((val, ctx): Principal => { if (Principal.isPrincipal(val)) { return Principal.from(val); From d9ada67a7468e53f6777db8c54f1c4df3b9b746d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 24 Sep 2025 07:29:55 +0200 Subject: [PATCH 16/16] feat: container url --- packages/config/src/cli/run.context.ts | 8 ++++---- packages/config/src/tests/cli/run.context.spec.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/config/src/cli/run.context.ts b/packages/config/src/cli/run.context.ts index cbc5de687..9b92c1094 100644 --- a/packages/config/src/cli/run.context.ts +++ b/packages/config/src/cli/run.context.ts @@ -11,7 +11,7 @@ import {createFunctionSchema} from '../utils/zod.utils'; export const OnRunContextSchema = z.strictObject({ satelliteId: StrictPrincipalSchema, identity: StrictIdentitySchema, - container: z.union([z.string(), z.boolean()]).optional() + container: z.string().optional() }); /** @@ -32,11 +32,11 @@ export interface OnRunContext { identity: Identity; /** - * Specifies whether the actor is calling the local development Docker container or provides the container URL. - * @type {boolean | string} + * A custom container URL. Useful when your local emulator runs on a non-default URL or port. + * @type {string} * @optional */ - container?: boolean | string; + container?: string; } /** diff --git a/packages/config/src/tests/cli/run.context.spec.ts b/packages/config/src/tests/cli/run.context.spec.ts index ae6cfe311..04aa70f65 100644 --- a/packages/config/src/tests/cli/run.context.spec.ts +++ b/packages/config/src/tests/cli/run.context.spec.ts @@ -13,13 +13,13 @@ describe('run.context', () => { expect(result.success).toBe(true); }); - it('accepts with container as boolean', () => { + it('should not accept a container as boolean', () => { const result = OnRunContextSchema.safeParse({ satelliteId: mockUserIdPrincipal, identity: mockIdentity, container: true }); - expect(result.success).toBe(true); + expect(result.success).toBe(false); }); it('accepts with container as string (URL)', () => {