Skip to content

Introduce EffectJS and Zod - #26

Open
widoz wants to merge 5 commits into
mainfrom
feat/introduce-effectjs-and-zod
Open

Introduce EffectJS and Zod#26
widoz wants to merge 5 commits into
mainfrom
feat/introduce-effectjs-and-zod

Conversation

@widoz

@widoz widoz commented Jan 29, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Schema-based validation and sanitization added for block state, context, and configuration; blocks now validate and sanitize on init.
    • Reaction block updates include preference syncing and safer toggle behavior.
  • Bug Fixes

    • Removed an obsolete public attribute from the UserProfile block.
  • Tests

    • Added unit tests covering sanitization utilities and block init sanitization.
  • Chores

    • Added runtime dependencies and build configuration to support new modules and testing.

✏️ Tip: You can customize this high-level summary in your review settings.

widoz added 3 commits January 27, 2026 19:07
Add Zod and Effect.js libraries to sanitize server-provided Block
Context data for Bookmark, Reaction, Konomi, and UserProfile blocks.
This ensures type-safe runtime validation of data before assigning it
to the Interactivity Store.
This update prevents ESLint from checking the build-modules
directory, streamlining the linting process.
Introduce Effect.js for effect handling and Zod for schema validation.
Update tests to ensure proper sanitization and add new test cases.
@coderabbitai

coderabbitai Bot commented Jan 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds schema sanitization utilities, Zod/effect module wiring, tests, build module manifests, PHP module registration, and updates multiple block stores to validate/merge sanitized context and state during initialization.

Changes

Cohort / File(s) Summary
Schema & Effect runtime
resources/modules/schema/index.ts, resources/modules/effect-js/index.ts, resources/modules/zod/index.ts, __mocks__/@external/effect-js.ts
Adds sanitize/sanitizeState/sanitizeContext/sanitizeConfiguration functions using Zod + Effect, re-exports external libs, and provides a test mock for Effect.
Build output & manifests
webpack.modules.config.js, tsconfig.json, build-modules/.../*.asset.php, build-modules/konomi-schema.js
Adds extraction entries for external modules, TS path aliases, and generated module asset manifests for build-modules output.
WordPress PHP module registration
sources/Artifacts/Module.php, konomi.php
Implements Artifacts\Module to read build-modules/*.asset.php and register script modules; wires module into plugin bootstrap chain.
Blocks — schemas & stores
sources/Blocks/*/view/schema.ts, sources/Blocks/*/view/store.ts (Bookmark, Konomi, Reaction, UserProfile)
Adds Zod schemas for block context/state, introduces STORE_NAME constants, and updates stores to run sanitization on init (and related action updates).
UserProfile manifest change
sources/Blocks/blocks-manifest.php
Removes the public dummy attribute from the UserProfile block manifest.
Tooling, tests & config
package.json, .gitignore, .eslintignore, jest.config.ts, tests/setup.ts, tests/unit/js/modules/schema/*.spec.ts, tests/unit/js/blocks/bookmarks/view/store.spec.ts
Adds effect/zod deps, ignores build-modules, maps @external/effect-js to a mock in Jest, suppresses TS checks in test setup, and adds unit tests for sanitizers and bookmark callbacks.

Sequence Diagram(s)

sequenceDiagram
    participant Store as Block Store
    participant Inter as `@wordpress/interactivity`
    participant Effect as Effect runtime
    participant Zod as zod
    participant AppCtx as In-memory Context

    Store->>Inter: getServerContext(storeName)
    Inter-->>Store: rawContext
    Store->>Effect: sanitizeContext(schema, storeName)
    Effect->>Zod: parse/validate rawContext
    Zod-->>Effect: parsed or ZodError
    alt parsed
        Effect->>AppCtx: merge sanitized context (Object.assign)
        Effect-->>Store: succeed(null)
    else error
        Effect->>Store: fail(Cause/Error)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hopped in the code with a twitch of my nose,
Zod checks the carrots and Effect ties the rows,
Context and state all tidy and bright,
Build modules hum as they step into light,
I nibble a bug—now everything's right. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Introduce EffectJS and Zod' directly and clearly summarizes the main change: adding support for two new libraries (EffectJS and Zod) throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/introduce-effectjs-and-zod

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jan 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.33981% with 46 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
sources/Artifacts/Module.php 6.81% 41 Missing ⚠️
sources/Blocks/Konomi/view/store.ts 71.42% 2 Missing ⚠️
sources/Blocks/Reaction/view/store.ts 71.42% 2 Missing ⚠️
resources/modules/schema/index.ts 96.87% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sources/Blocks/blocks-manifest.php (1)

1-3: This file is auto-generated and violates naming/structure guidelines—requires a build or architecture decision, not manual edits.

This PHP file is generated by wp-scripts build-blocks-manifest and should not be manually modified. However, it violates the coding guidelines by missing declare(strict_types=1); and the SpaghettiDojo\Konomi namespace required for all PHP files in sources/.

Since the file is auto-generated and will be overwritten on rebuild, manual edits are not viable. Choose one:

  1. Document an exception for auto-generated manifests in sources/ (least preferred—creates inconsistency)
  2. Relocate the manifest outside sources/ with a documented reason
  3. Configure the generator (if wp-scripts build-blocks-manifest supports options) or create a wrapper script that regenerates with proper headers

Per coding guidelines: **/*.php requires strict types and sources/**/*.php requires namespace SpaghettiDojo\Konomi.

🤖 Fix all issues with AI agents
In `@build-modules/konomi-schema.asset.php`:
- Line 1: Add the PHP strict types declaration to the top of the file by
inserting declare(strict_types=1); as the first non-blank, non-comment line
before the existing "<?php return array(...)" so the file begins with "<?php
declare(strict_types=1);" and then returns the array as currently defined (the
returned array with 'dependencies', 'version', 'type' remains unchanged).

In `@sources/Blocks/Bookmark/view/store.ts`:
- Around line 77-82: callbacks.init currently calls
Effect.runPromise(sanitizeContext(contextSchema, STORE_NAME)) without handling
rejections; update this to handle the returned Promise from Effect.runPromise by
attaching a .catch(...) to log or surface errors (or alternatively pass an
onError callback into sanitizeContext/Effect.runPromise if supported),
referencing callbacks.init, Effect.runPromise, sanitizeContext, contextSchema
and STORE_NAME so that any failure from sanitizeContext is caught and does not
produce an unhandled rejection.

In `@sources/Blocks/Konomi/view/schema.ts`:
- Around line 3-12: Keep the error field required on contextSchema (do not make
error optional) and create a single source of truth by exporting the inferred
Context type from this file: export type Context = z.infer<typeof
contextSchema>; then remove or replace the separately-declared Context type in
store.ts so store code imports the Context type from this schema file (follow
the UserProfile pattern of exporting inferred types from state/schema files and
ensure all usages still access error.code and error.message without null
checks).

In `@sources/Blocks/Konomi/view/store.ts`:
- Around line 58-61: The init method calls
Effect.runPromise(sanitizeContext(contextSchema, STORE_NAME)) but does not
handle its returned Promise; update init (in the init() function) to await or
attach a .catch handler to the Effect.runPromise call so rejections from
sanitizeContext(contextSchema, STORE_NAME) are handled—e.g., await the call
inside an async init or add .catch to log or handle the error via the store's
error path (referencing init, sanitizeContext, contextSchema, STORE_NAME, and
Effect.runPromise).

In `@sources/Blocks/UserProfile/view/store.ts`:
- Around line 43-48: The init function calls
Effect.runSync(sanitizeState(stateSchema, STORE_NAME)) which can throw if schema
validation fails because sanitizeState is invoked without an onError handler;
modify init to handle that failure by either passing an onError callback (e.g.,
use catchInitializationIssues as the onError argument to sanitizeState) so
sanitizeState returns a safe fallback instead of throwing, or wrap the
Effect.runSync call in a try/catch that logs the error (via your logger) and
returns/initializes a default safe state for STORE_NAME; reference init,
STORE_NAME, sanitizeState, stateSchema and Effect.runSync when applying the
change.

In `@tests/setup.ts`:
- Around line 6-11: The file uses `@ts-ignore` to silence TypeScript for
globalThis.wp in ensureGlobalWp (and the similar block at lines 15-19); replace
those ignores by declaring a proper global type and casting: add a minimal
declaration for the global interface (or a local typed helper) so you can safely
write (globalThis as unknown as { wp?: Record<string, any> }).wp or use a helper
function to set globalThis.wp = {} with an explicit type, and remove the
`@ts-ignore` comments; update both ensureGlobalWp and the other global assignment
to use the same typed approach so strict mode is satisfied.

In `@tests/unit/js/modules/schema/routines.spec.ts`:
- Around line 17-23: Add a beforeEach hook to clear jest mocks between tests by
inserting beforeEach(() => { jest.clearAllMocks() }) near the top of the
routines spec (after the jest.mock(...) block) so mocks like
getServerState/getServerContext/getContext/getConfig are reset between test
cases; this ensures test isolation for the sanitizer suites and uses the global
jest.clearAllMocks API.
- Around line 28-41: Replace the unsafe `as any` test fixtures with correctly
typed values: for valid fixtures returned from getServerState (used in the test
invoking sanitizeState with schema and storeName) use TypeScript's `satisfies
z.infer<typeof schema>` to assert they match the schema type, and for invalid
fixtures use the `unknown` type (or a typed `unknown` variable) to represent
values that should fail validation; update the mocked getServerState calls
(jest.mocked(getServerState).mockReturnValue(...)) to return these properly
typed fixtures so the tests compile under strict mode while still exercising
sanitizeState and the onError behavior.

In `@tests/unit/js/modules/schema/schema.spec.ts`:
- Around line 6-12: Add a test-level beforeEach that clears Jest mocks so mock
state from the jest.mock of '@wordpress/interactivity' (getConfig, getContext,
getServerContext, getServerState) is reset between tests; place this beforeEach
in the same spec file (tests/unit/js/modules/schema/schema.spec.ts) after the
jest.mock block and have it call Jest's clearAllMocks to ensure each test starts
with clean mock state.
🧹 Nitpick comments (10)
sources/Blocks/UserProfile/view/schema.ts (1)

10-10: Consider wrapping the inferred type with Readonly<> for immutability.

As per coding guidelines, Readonly<> should be used for immutable data structures in this directory.

♻️ Suggested change
-export type UserProfileState = z.infer< typeof stateSchema >;
+export type UserProfileState = Readonly< z.infer< typeof stateSchema > >;
sources/Blocks/Reaction/view/schema.ts (1)

3-6: Consider exporting inferred types for consistency.

The UserProfile/view/schema.ts exports type aliases (UserProfileState, UserProfileStateSchema), while this file does not. Consider adding similar exports if consumers need the inferred type.

♻️ Optional type exports
 export const contextSchema = z.object( {
 	isActive: z.boolean(),
 	count: z.number(),
 } );
+
+export type ReactionContextSchema = typeof contextSchema;
+export type ReactionContext = Readonly< z.infer< typeof contextSchema > >;
resources/modules/zod/index.ts (1)

1-2: Redundant re-export of z.

export * from 'zod' already includes the named export z, making line 2 redundant. This doesn't cause issues but can be simplified.

♻️ Suggested simplification
 export * from 'zod';
-export { z } from 'zod';
sources/Artifacts/Module.php (1)

60-62: Silent skip on unreadable asset files may hinder debugging.

When an asset file is unreadable, the code silently continues. Consider logging a warning to aid troubleshooting deployment issues.

♻️ Optional: Add debug logging
             if (!is_readable($assetPath)) {
+                // Consider adding: error_log("Module asset not readable: {$assetPath}");
                 continue;
             }
__mocks__/@external/effect-js.ts (2)

122-136: Type and value both named Cause may cause confusion.

While TypeScript allows the same name for types and values (separate namespaces), having both export type Cause = unknown and export const Cause = {} with the same name can be confusing for consumers.

♻️ Optional: Clarify naming
-// Convenience re-exports to align with Effect style API
-export type Cause = unknown;
+// Convenience re-exports to align with Effect style API
+export type CauseType = unknown;
+export type Cause = CauseType;
 
 export const Effect = {
 	succeed,
 	fail,
 	try: try_,
 	runPromise,
 	runSync,
 	flatMap,
 	tap,
 	andThen,
 	catchAllCause,
 };
 
-export const Cause = {};
+export const Cause: Record<string, never> = {};

Alternatively, this naming mirrors the real Effect library's API where Cause exists as both a type and a namespace, so keeping it as-is is also acceptable for API compatibility.


118-119: Consider stricter typing for pipe.

The any type in the function array reduces type safety. For a test mock this is acceptable, but stricter typing would catch more issues.

♻️ Optional: Stricter typing
-export const pipe = <T>(value: T, ...fns: Array<(input: any) => any>) =>
+export const pipe = <T, R = T>(value: T, ...fns: Array<(input: unknown) => unknown>): R =>
-	fns.reduce((current, fn) => fn(current), value);
+	fns.reduce((current, fn) => fn(current), value as unknown) as R;

This is a minor nit for a mock file where perfect typing isn't critical.

resources/modules/schema/index.ts (1)

54-55: Replace any with unknown to preserve strict typing.

any bypasses type safety; unknown keeps the pipeline strict while still allowing schema validation.

♻️ Suggested change
-		Effect.flatMap( ( context: any ) => sanitize( context, schema ) ),
+		Effect.flatMap( ( context: unknown ) => sanitize( context, schema ) ),
@@
-		Effect.flatMap( ( config: any ) => sanitize( config, schema ) ),
+		Effect.flatMap( ( config: unknown ) => sanitize( config, schema ) ),

As per coding guidelines: **/*.{ts,tsx}: Use TypeScript with strict typing

Also applies to: 74-75

tests/unit/js/modules/schema/routines.spec.ts (1)

46-48: Track the TODOs for pub/sub assertions to avoid lingering gaps.

Consider opening a follow-up issue so these assertions don’t get lost as tests evolve.

Also applies to: 81-82, 122-123

tests/unit/js/blocks/bookmarks/view/store.spec.ts (2)

41-45: Tighten callback typing to keep strict TS.
any[] weakens type-safety; consider unknown[] (or a tighter signature) for callbacks/actions.

♻️ Suggested typing tweak
-		actions: Record<string, ( ...args: any[] ) => void>;
-		callbacks: Record<string, ( ...args: any[] ) => void>;
+		actions: Record<string, ( ...args: ReadonlyArray<unknown> ) => void>;
+		callbacks: Record<string, ( ...args: ReadonlyArray<unknown> ) => void>;

As per coding guidelines, Use TypeScript with strict typing.


84-86: Avoid as any in the sanitizeContext mock.
This should usually type-check without a cast; if it doesn’t, prefer a typed helper over any.

♻️ Suggested cleanup
-		jest.mocked( sanitizeContext ).mockReturnValue(
-			Effect.succeed( null ) as any,
-		);
+		jest.mocked( sanitizeContext ).mockReturnValue(
+			Effect.succeed( null ),
+		);

As per coding guidelines, Use TypeScript with strict typing.

Comment thread build-modules/konomi-schema.asset.php
Comment on lines +77 to +82
callbacks: {
init(): void {
Effect.runPromise(
sanitizeContext( contextSchema, STORE_NAME )
);
},

@coderabbitai coderabbitai Bot Jan 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "store.ts" -path "*/Blocks/Bookmark/view/*" -type f

Repository: Spaghetti-Dojo/konomi

Length of output: 104


🏁 Script executed:

# Read the specific file to see lines 77-82 and context
wc -l sources/Blocks/Bookmark/view/store.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 105


🏁 Script executed:

# Get the actual code at lines 77-82 with context
sed -n '70,90p' sources/Blocks/Bookmark/view/store.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 340


🏁 Script executed:

# Find sanitizeContext implementation
rg -n "export.*sanitizeContext|function sanitizeContext|const sanitizeContext" --type ts --type tsx

Repository: Spaghetti-Dojo/konomi

Length of output: 92


🏁 Script executed:

# Check how Effect.runPromise is defined and whether it handles errors
rg -n "runPromise|Effect" sources/Blocks/Bookmark/view/store.ts -B 2 -A 2

Repository: Spaghetti-Dojo/konomi

Length of output: 331


🏁 Script executed:

# Find sanitizeContext implementation
rg -n "export.*sanitizeContext" sources/ -A 10 --type ts

Repository: Spaghetti-Dojo/konomi

Length of output: 47


🏁 Script executed:

# Also check in konomi schema module
find . -path "*schema*" -name "*.ts" -o -name "*.tsx" | head -20

Repository: Spaghetti-Dojo/konomi

Length of output: 1003


🏁 Script executed:

# Check Effect.runPromise usage patterns in codebase
rg -n "Effect\.runPromise|runPromise\(" sources/ --type ts -B 2 -A 3

Repository: Spaghetti-Dojo/konomi

Length of output: 1153


🏁 Script executed:

# Read the store.ts file completely to check for Konomi namespace usage
cat -n sources/Blocks/Bookmark/view/store.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 2599


🏁 Script executed:

# Read sanitizeContext implementation
cat -n resources/modules/schema/index.ts | head -100

Repository: Spaghetti-Dojo/konomi

Length of output: 3699


🏁 Script executed:

# Check for sanitizeContext specifically
rg -n "sanitizeContext" resources/modules/schema/index.ts -A 15

Repository: Spaghetti-Dojo/konomi

Length of output: 770


🏁 Script executed:

# Check Effect.runPromise in node_modules or external definition
find . -name "*.d.ts" -o -name "*.ts" | xargs grep -l "runPromise" 2>/dev/null | head -5

Repository: Spaghetti-Dojo/konomi

Length of output: 265


🏁 Script executed:

# Look for Effect type definitions
rg -n "runPromise|Effect" sources/ --type ts -B 1 -A 1 | grep -A 3 -B 3 "runPromise"

Repository: Spaghetti-Dojo/konomi

Length of output: 1088


🏁 Script executed:

# Check Konomi namespace usage in the codebase
rg -n "namespace Konomi|declare namespace Konomi" sources/ --type ts --type tsx

Repository: Spaghetti-Dojo/konomi

Length of output: 92


🏁 Script executed:

# Check how Effect.runPromise behaves with failing Effects
cat -n ./__mocks__/@external/effect-js.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 4588


🏁 Script executed:

# Verify if there are other usages of sanitizeContext with onError callback
rg -n "sanitizeContext.*onError|onError.*:.*=>" sources/ --type ts

Repository: Spaghetti-Dojo/konomi

Length of output: 47


🏁 Script executed:

# Check if the review comment's concern about Konomi namespace is also valid
rg -n "namespace Konomi|type Context.*Konomi" sources/Blocks/Bookmark/view/ --type ts

Repository: Spaghetti-Dojo/konomi

Length of output: 47


Handle the Promise from Effect.runPromise to avoid unhandled rejections.

callbacks.init is sync, and sanitizeContext can reject via Effect.catchAllCauseEffect.fail. Without error handling, rejection goes unhandled. Add .catch() handler or pass onError callback to sanitizeContext.

🔧 Suggested patterns

Option 1 - Using .catch():

 init(): void {
-	Effect.runPromise(
-		sanitizeContext( contextSchema, STORE_NAME )
-	);
+	void Effect.runPromise(
+		sanitizeContext( contextSchema, STORE_NAME )
+	).catch( () => {
+		// optionally log or report initialization failure
+	} );
 },

Option 2 - Using onError callback:

 init(): void {
-	Effect.runPromise(
-		sanitizeContext( contextSchema, STORE_NAME )
-	);
+	void Effect.runPromise(
+		sanitizeContext( contextSchema, STORE_NAME, ( error ) => {
+			// handle initialization error
+		} )
+	);
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
callbacks: {
init(): void {
Effect.runPromise(
sanitizeContext( contextSchema, STORE_NAME )
);
},
callbacks: {
init(): void {
void Effect.runPromise(
sanitizeContext( contextSchema, STORE_NAME )
).catch( () => {
// optionally log or report initialization failure
} );
},
🤖 Prompt for AI Agents
In `@sources/Blocks/Bookmark/view/store.ts` around lines 77 - 82, callbacks.init
currently calls Effect.runPromise(sanitizeContext(contextSchema, STORE_NAME))
without handling rejections; update this to handle the returned Promise from
Effect.runPromise by attaching a .catch(...) to log or surface errors (or
alternatively pass an onError callback into sanitizeContext/Effect.runPromise if
supported), referencing callbacks.init, Effect.runPromise, sanitizeContext,
contextSchema and STORE_NAME so that any failure from sanitizeContext is caught
and does not produce an unhandled rejection.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Would you be so kind to work on this? Consider

  1. Keep the pipe flow
  2. We don't need to differentiate between expected and unexpected errors, just prevent to log in the console
  3. For now we can simply use @wordpress/hooks as a publish system, so that later on we can hook a subscriber
  4. The error should be a standard object, I would start fresh with a easy ErrorMessage with two properties, message, type, severity. The message is human readable text and shall not come from the underline procedure, it shall be meaningfull since it will be printed to the user, so no technical terms no data information related to logic or code errors or stack. type is to distinguish between expected and unexpected, if it's easier to catch those two errors separately, than I would avoid the type property. severity, this is more like info, error, warning. I think make sense to align with PHP PSR Logging specification so that, if necessary we can send this ErrorMessage to the server for further processing and there since we run PHP we expect to have something PSR compatible.
  5. Double check if there are other occurrencies of the same procedure, other store files using the same strategy to sanitize the content and apply the same changes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +3 to +12
export const contextSchema = z.object( {
id: z.number(),
type: z.string(),
isUserLoggedIn: z.boolean(),
loginRequired: z.boolean(),
error: z.object( {
code: z.string(),
message: z.string(),
} ),
} );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find usage of contextSchema in the Konomi block
rg -n "contextSchema" sources/Blocks/Konomi/ -A 3 -B 3

# Search for context initialization/creation patterns
rg -n "getServerContext|getContext|createContext" sources/Blocks/Konomi/ -A 5

Repository: Spaghetti-Dojo/konomi

Length of output: 2936


🏁 Script executed:

# Check how the error field is used in the block
rg -n "error" sources/Blocks/Konomi/ -B 2 -A 2

# Look for similar schema patterns in other blocks
find sources/Blocks -name "schema.ts" -o -name "schema.js" | head -10

Repository: Spaghetti-Dojo/konomi

Length of output: 3480


🏁 Script executed:

# Check if other schema files export inferred types
rg -n "export type.*=.*z.infer" sources/Blocks/ --type ts --type tsx

Repository: Spaghetti-Dojo/konomi

Length of output: 92


🏁 Script executed:

# Check the Context type definition in store.ts more closely
sed -n '30,45p' sources/Blocks/Konomi/view/store.ts

# Check if Context is exported from schema.ts or defined elsewhere
rg -n "type Context" sources/Blocks/Konomi/ -B 2 -A 5

# Check if other blocks' schema files export inferred types
for f in sources/Blocks/*/view/schema.ts; do echo "=== $f ==="; head -20 "$f"; done

Repository: Spaghetti-Dojo/konomi

Length of output: 1710


Remove suggestion to make error optional; consider establishing a single source of truth for the Context type.

The error field must remain required. Server initialization (Context.php) always provides { code: '', message: '' }, and all client code accesses error.code and error.message without null checks.

However, the Context type is currently maintained separately in store.ts while contextSchema is defined in schema.ts—creating a potential source of drift. The UserProfile block shows a better pattern: exporting inferred types directly from the schema file (e.g., export type UserProfileState = z.infer< typeof stateSchema >;). Consider following this approach to keep schema and types in sync.

🤖 Prompt for AI Agents
In `@sources/Blocks/Konomi/view/schema.ts` around lines 3 - 12, Keep the error
field required on contextSchema (do not make error optional) and create a single
source of truth by exporting the inferred Context type from this file: export
type Context = z.infer<typeof contextSchema>; then remove or replace the
separately-declared Context type in store.ts so store code imports the Context
type from this schema file (follow the UserProfile pattern of exporting inferred
types from state/schema files and ensure all usages still access error.code and
error.message without null checks).

Comment on lines +58 to +61
init(): void {
Effect.runPromise(
sanitizeContext( contextSchema, STORE_NAME )
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle the promise from Effect.runPromise to avoid unhandled rejections.

If sanitization fails, the promise rejects; currently it’s not caught.

🛠️ Suggested fix
-				Effect.runPromise(
-					sanitizeContext( contextSchema, STORE_NAME )
-				);
+				void Effect.runPromise(
+					sanitizeContext( contextSchema, STORE_NAME )
+				).catch( () => undefined );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
init(): void {
Effect.runPromise(
sanitizeContext( contextSchema, STORE_NAME )
);
init(): void {
void Effect.runPromise(
sanitizeContext( contextSchema, STORE_NAME )
).catch( () => undefined );
}
🤖 Prompt for AI Agents
In `@sources/Blocks/Konomi/view/store.ts` around lines 58 - 61, The init method
calls Effect.runPromise(sanitizeContext(contextSchema, STORE_NAME)) but does not
handle its returned Promise; update init (in the init() function) to await or
attach a .catch handler to the Effect.runPromise call so rejections from
sanitizeContext(contextSchema, STORE_NAME) are handled—e.g., await the call
inside an async init or add .catch to log or handle the error via the store's
error path (referencing init, sanitizeContext, contextSchema, STORE_NAME, and
Effect.runPromise).

Comment on lines +43 to +48
const STORE_NAME = 'konomiProfilePagination';

// eslint-disable-next-line max-lines-per-function
export function init(): void {
const { state } = store< Store >( 'konomiProfilePagination', {
state: getServerState(),
const { state } = store< Store >( STORE_NAME, {
state: Effect.runSync( sanitizeState( stateSchema, STORE_NAME ) ),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "catchInitializationIssues" resources/modules/schema/index.ts -A 10 -B 2

Repository: Spaghetti-Dojo/konomi

Length of output: 1493


🏁 Script executed:

rg -n "sanitizeState" sources/Blocks/UserProfile/view/store.ts -A 5 -B 5

Repository: Spaghetti-Dojo/konomi

Length of output: 843


🏁 Script executed:

rg -n "export.*sanitizeState" --type ts --type tsx -A 10

Repository: Spaghetti-Dojo/konomi

Length of output: 92


🏁 Script executed:

cat -n sources/Blocks/UserProfile/view/store.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 4858


Add error handling to prevent uncaught exceptions from Effect.runSync in store initialization.

catchInitializationIssues does not prevent the Effect from failing—it invokes the optional callback, then returns Effect.fail( error ). Since sanitizeState is called without an onError handler on line 48, any schema validation failure will cause Effect.runSync to throw an uncaught exception, breaking store initialization. Either pass an onError callback to sanitizeState, wrap the runSync call in try-catch, or use an alternative Effect method that provides a safe fallback.

🤖 Prompt for AI Agents
In `@sources/Blocks/UserProfile/view/store.ts` around lines 43 - 48, The init
function calls Effect.runSync(sanitizeState(stateSchema, STORE_NAME)) which can
throw if schema validation fails because sanitizeState is invoked without an
onError handler; modify init to handle that failure by either passing an onError
callback (e.g., use catchInitializationIssues as the onError argument to
sanitizeState) so sanitizeState returns a safe fallback instead of throwing, or
wrap the Effect.runSync call in a try/catch that logs the error (via your
logger) and returns/initializes a default safe state for STORE_NAME; reference
init, STORE_NAME, sanitizeState, stateSchema and Effect.runSync when applying
the change.

Comment thread tests/setup.ts
Comment on lines 6 to 11
function ensureGlobalWp() {
// @ts-ignore
if ( typeof globalThis.wp === 'undefined' ) {
// @ts-ignore
globalThis.wp = {};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n tests/setup.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 578


🏁 Script executed:

fd -e "d.ts" | head -20

Repository: Spaghetti-Dojo/konomi

Length of output: 119


🏁 Script executed:

rg -n "declare global|interface.*Wp|globalThis\.wp" --type ts --type tsx

Repository: Spaghetti-Dojo/konomi

Length of output: 92


🏁 Script executed:

rg -n "wp\s*:" -g "*.d.ts"

Repository: Spaghetti-Dojo/konomi

Length of output: 93


🏁 Script executed:

cat -n `@types/index.d.ts`

Repository: Spaghetti-Dojo/konomi

Length of output: 618


🏁 Script executed:

cat -n `@types/wp/index.d.ts` | head -50

Repository: Spaghetti-Dojo/konomi

Length of output: 147


🏁 Script executed:

rg "globalThis\.wp" --type ts

Repository: Spaghetti-Dojo/konomi

Length of output: 217


🏁 Script executed:

cat tsconfig.json | head -40

Repository: Spaghetti-Dojo/konomi

Length of output: 1352


Replace @ts-ignore with explicit global typing to enforce strict TypeScript checks.

The @ts-ignore comments suppress type errors despite strict: true being enabled in tsconfig.json. Since globalThis.wp is used only in this file and does not have a type definition for the Node.js test environment, use a typed helper cast instead.

🔧 Suggested refactor (typed helper)
+type GlobalWithWp = typeof globalThis & {
+	wp?: {
+		apiFetch?: ( ( ...args: unknown[] ) => unknown ) & { use?: ( ...args: unknown[] ) => void };
+	};
+};
+
 function ensureGlobalWp() {
-	// `@ts-ignore`
-	if ( typeof globalThis.wp === 'undefined' ) {
-		// `@ts-ignore`
-		globalThis.wp = {};
-	}
+	const globalWithWp = globalThis as GlobalWithWp;
+	if ( typeof globalWithWp.wp === 'undefined' ) {
+		globalWithWp.wp = {};
+	}
 }
 
 // TODO Use __mocks__
 function mockWpApiFetch() {
 	const apiFetch = () => void 0;
 	apiFetch.use = () => void 0;
-	// `@ts-ignore`
-	globalThis.wp['apiFetch'] = apiFetch;
+	const globalWithWp = globalThis as GlobalWithWp;
+	globalWithWp.wp = globalWithWp.wp ?? {};
+	globalWithWp.wp.apiFetch = apiFetch;
 }

Also applies to: lines 15-19

🤖 Prompt for AI Agents
In `@tests/setup.ts` around lines 6 - 11, The file uses `@ts-ignore` to silence
TypeScript for globalThis.wp in ensureGlobalWp (and the similar block at lines
15-19); replace those ignores by declaring a proper global type and casting: add
a minimal declaration for the global interface (or a local typed helper) so you
can safely write (globalThis as unknown as { wp?: Record<string, any> }).wp or
use a helper function to set globalThis.wp = {} with an explicit type, and
remove the `@ts-ignore` comments; update both ensureGlobalWp and the other global
assignment to use the same typed approach so strict mode is satisfied.

Comment on lines +17 to +23
jest.mock('@wordpress/interactivity', () => ({
getServerState: jest.fn(),
getServerContext: jest.fn(),
getContext: jest.fn(),
getConfig: jest.fn(),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add beforeEach to clear mocks between tests.

This keeps test isolation consistent across all sanitizer suites.

✅ Suggested addition
 jest.mock('@wordpress/interactivity', () => ({
 	getServerState: jest.fn(),
 	getServerContext: jest.fn(),
 	getContext: jest.fn(),
 	getConfig: jest.fn(),
 }));
+
+beforeEach(() => {
+	jest.clearAllMocks();
+});

As per coding guidelines, Clear mocks between tests with beforeEach(() => { jest.clearAllMocks() }) in TypeScript tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jest.mock('@wordpress/interactivity', () => ({
getServerState: jest.fn(),
getServerContext: jest.fn(),
getContext: jest.fn(),
getConfig: jest.fn(),
}));
jest.mock('@wordpress/interactivity', () => ({
getServerState: jest.fn(),
getServerContext: jest.fn(),
getContext: jest.fn(),
getConfig: jest.fn(),
}));
beforeEach(() => {
jest.clearAllMocks();
});
🤖 Prompt for AI Agents
In `@tests/unit/js/modules/schema/routines.spec.ts` around lines 17 - 23, Add a
beforeEach hook to clear jest mocks between tests by inserting beforeEach(() =>
{ jest.clearAllMocks() }) near the top of the routines spec (after the
jest.mock(...) block) so mocks like
getServerState/getServerContext/getContext/getConfig are reset between test
cases; this ensures test isolation for the sanitizer suites and uses the global
jest.clearAllMocks API.

Comment on lines +28 to +41
it('returns sanitized state', async () => {
const serverState = { page: 1, perPage: 10 };
jest.mocked(getServerState).mockReturnValue(serverState as any);

const result = await Effect.runPromise(sanitizeState(schema, storeName));

expect(result).toEqual(serverState);
expect(getServerState).toHaveBeenCalledWith(storeName);
});

it('fails on invalid state and calls onError', async () => {
const onError = jest.fn();
jest.mocked(getServerState).mockReturnValue({ page: 'invalid' } as any);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the test file
fd -t f "routines.spec.ts" | head -5

Repository: Spaghetti-Dojo/konomi

Length of output: 110


🏁 Script executed:

# Check the file size first
wc -l tests/unit/js/modules/schema/routines.spec.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 114


🏁 Script executed:

# Read the specific lines mentioned in the review (28-41)
sed -n '1,150p' tests/unit/js/modules/schema/routines.spec.ts | cat -n

Repository: Spaghetti-Dojo/konomi

Length of output: 4471


🏁 Script executed:

# Look for the getServerState type definition
fd -t f -e "d.ts" -e "ts" | xargs rg -l "getServerState" | head -10

Repository: Spaghetti-Dojo/konomi

Length of output: 241


🏁 Script executed:

# Check import statements and type definitions in the test file
head -30 tests/unit/js/modules/schema/routines.spec.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 858


🏁 Script executed:

# Check if there's a types directory or node_modules reference for `@wordpress/interactivity`
fd -t f "@wordpress" | head -20

Repository: Spaghetti-Dojo/konomi

Length of output: 47


🏁 Script executed:

# Look for any TypeScript configuration or type declaration files
fd -t f -e "d.ts" | xargs rg -l "@wordpress/interactivity" 2>/dev/null | head -10

Repository: Spaghetti-Dojo/konomi

Length of output: 47


🏁 Script executed:

# Check the project's package.json to understand dependencies
cat package.json | grep -A 20 -B 5 "@wordpress/interactivity"

Repository: Spaghetti-Dojo/konomi

Length of output: 1416


🏁 Script executed:

# Check the tsconfig to understand strict typing requirements
cat tsconfig.json | head -50

Repository: Spaghetti-Dojo/konomi

Length of output: 1494


🏁 Script executed:

# Also verify the actual behavior - check if there are any type errors currently
# Look for any error suppression comments near the as any usage
grep -n "as any" tests/unit/js/modules/schema/routines.spec.ts

Repository: Spaghetti-Dojo/konomi

Length of output: 574


🏁 Script executed:

# Check if there's a `@types` directory or type definitions
fd -t d "@types" | head -10

Repository: Spaghetti-Dojo/konomi

Length of output: 72


Use strict typing in test fixtures—remove as any casts.

The project enforces strict: true TypeScript compilation. Using as any bypasses type safety and can mask test setup errors. Replace with:

  • Valid fixtures: satisfies with the schema type (e.g., satisfies z.infer<typeof schema>)
  • Invalid fixtures: unknown type for values that should fail validation
🔧 Refactoring pattern
-const serverState = { page: 1, perPage: 10 };
-jest.mocked(getServerState).mockReturnValue(serverState as any);
+const serverState = { page: 1, perPage: 10 } satisfies z.infer<typeof schema>;
+jest.mocked(getServerState).mockReturnValue(serverState);

-jest.mocked(getServerState).mockReturnValue({ page: 'invalid' } as any);
+const invalidState: unknown = { page: 'invalid' };
+jest.mocked(getServerState).mockReturnValue(invalidState);

Applies to lines 30, 40, 59, 60, 73, 97, 100, 113.

🤖 Prompt for AI Agents
In `@tests/unit/js/modules/schema/routines.spec.ts` around lines 28 - 41, Replace
the unsafe `as any` test fixtures with correctly typed values: for valid
fixtures returned from getServerState (used in the test invoking sanitizeState
with schema and storeName) use TypeScript's `satisfies z.infer<typeof schema>`
to assert they match the schema type, and for invalid fixtures use the `unknown`
type (or a typed `unknown` variable) to represent values that should fail
validation; update the mocked getServerState calls
(jest.mocked(getServerState).mockReturnValue(...)) to return these properly
typed fixtures so the tests compile under strict mode while still exercising
sanitizeState and the onError behavior.

Comment on lines +6 to +12
jest.mock( '@wordpress/interactivity', () => ( {
getConfig: jest.fn(),
getContext: jest.fn(),
getServerContext: jest.fn(),
getServerState: jest.fn(),
} ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add beforeEach to clear mocks between tests.

This keeps mock state isolated across tests and aligns with the test guidelines.

✅ Suggested addition
 jest.mock( '@wordpress/interactivity', () => ( {
 	getConfig: jest.fn(),
 	getContext: jest.fn(),
 	getServerContext: jest.fn(),
 	getServerState: jest.fn(),
 } ) );
+
+beforeEach( () => {
+	jest.clearAllMocks();
+} );

As per coding guidelines, Clear mocks between tests with beforeEach(() => { jest.clearAllMocks() }) in TypeScript tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jest.mock( '@wordpress/interactivity', () => ( {
getConfig: jest.fn(),
getContext: jest.fn(),
getServerContext: jest.fn(),
getServerState: jest.fn(),
} ) );
jest.mock( '@wordpress/interactivity', () => ( {
getConfig: jest.fn(),
getContext: jest.fn(),
getServerContext: jest.fn(),
getServerState: jest.fn(),
} ) );
beforeEach( () => {
jest.clearAllMocks();
} );
🤖 Prompt for AI Agents
In `@tests/unit/js/modules/schema/schema.spec.ts` around lines 6 - 12, Add a
test-level beforeEach that clears Jest mocks so mock state from the jest.mock of
'@wordpress/interactivity' (getConfig, getContext, getServerContext,
getServerState) is reset between tests; place this beforeEach in the same spec
file (tests/unit/js/modules/schema/schema.spec.ts) after the jest.mock block and
have it call Jest's clearAllMocks to ensure each test starts with clean mock
state.

Copilot AI commented Jan 31, 2026

Copy link
Copy Markdown
Contributor

@widoz I've opened a new pull request, #28, to work on those changes. Once the pull request is ready, I'll request review from you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants