diff --git a/README.md b/README.md index 266146f..a5b3343 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ output: # Run from project root wp-bench run --config wp-bench.yaml # run with config file wp-bench run --model-name gpt-4o --limit 5 # quick single-model test +wp-bench run --test-type knowledge # run only knowledge tests (no WordPress env needed) +wp-bench run --test-type execution # run only execution tests wp-bench dry-run --config wp-bench.yaml # validate config without calling models ``` diff --git a/datasets/suites/wp-core-v1/knowledge/abilities-api.json b/datasets/suites/wp-core-v1/knowledge/abilities-api.json index f3a59ab..1f92dd1 100644 --- a/datasets/suites/wp-core-v1/knowledge/abilities-api.json +++ b/datasets/suites/wp-core-v1/knowledge/abilities-api.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-abilities_api", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Abilities API", "description": "Tests knowledge of WordPress Abilities API", @@ -11,51 +11,261 @@ { "id": "k-abilities-001", "category": "abilities-api", + "subcategory": "registration", + "difficulty": "basic", + "prompt": "What function registers a new ability in WordPress?", + "type": "short_answer", + "correct_answer": "wp_register_ability", + "answer_type": "contains", + "explanation": "wp_register_ability() accepts a name (string) and args (array). It must be called during the wp_abilities_api_init action hook.", + "references": [ + "https://developer.wordpress.org/reference/functions/wp_register_ability/" + ] + }, + { + "id": "k-abilities-002", + "category": "abilities-api", + "subcategory": "core-abilities", + "difficulty": "basic", + "prompt": "Which of these is a core ability that ships with WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "core/get-site-info" + }, + { + "key": "B", + "text": "core/create-post" + }, + { + "key": "C", + "text": "core/manage-users" + }, + { + "key": "D", + "text": "core/install-plugin" + } + ], + "correct_answer": "A", + "explanation": "WordPress 6.9 ships with three initial core abilities: core/get-site-info (returns site information), core/get-user-info (returns current user information), and core/get-environment-info (returns PHP/DB/WP version information). All three are read-only abilities.", + "references": [ + "https://make.wordpress.org/core/2025/11/25/wordpress-6-9-field-guide/" + ] + }, + { + "id": "k-abilities-003", + "category": "abilities-api", + "subcategory": "hooks", "difficulty": "intermediate", "prompt": "Which hook should plugins use to register abilities with the WordPress Abilities API?", "type": "multiple_choice", "choices": [ - { "key": "A", "text": "init" }, - { "key": "B", "text": "wp_loaded" }, - { "key": "C", "text": "wp_abilities_api_init" }, - { "key": "D", "text": "rest_api_init" } + { + "key": "A", + "text": "init" + }, + { + "key": "B", + "text": "wp_loaded" + }, + { + "key": "C", + "text": "wp_abilities_api_init" + }, + { + "key": "D", + "text": "rest_api_init" + } ], "correct_answer": "C", - "explanation": "Abilities are registered during the wp_abilities_api_init hook.", - "references": ["https://make.wordpress.org/core/2024/07/03/wordpress-6-8-field-guide/#abilities-api"] + "explanation": "Abilities must be registered during the wp_abilities_api_init action hook. A separate hook, wp_abilities_api_categories_init, is used for registering ability categories.", + "references": [ + "https://developer.wordpress.org/reference/hooks/wp_abilities_api_init/" + ] }, { - "id": "k-abilities-002", + "id": "k-abilities-004", "category": "abilities-api", - "difficulty": "basic", - "prompt": "How do you expose a custom ability over the REST API when calling wp_register_ability()?", + "subcategory": "categories", + "difficulty": "intermediate", + "prompt": "What are the two default ability categories registered by WordPress core?", "type": "multiple_choice", "choices": [ - { "key": "A", "text": "Set 'rest' => true in the args" }, - { "key": "B", "text": "Add a REST route manually" }, - { "key": "C", "text": "Pass 'show_in_rest' => true in the meta array" }, - { "key": "D", "text": "It is always exposed automatically" } + { + "key": "A", + "text": "site and user" + }, + { + "key": "B", + "text": "admin and public" + }, + { + "key": "C", + "text": "read and write" + }, + { + "key": "D", + "text": "content and settings" + } ], - "correct_answer": "C", - "explanation": "Abilities are opt-in to REST; set show_in_rest => true when registering.", - "references": ["https://developer.wordpress.org/news/2024/10/16/abilities-api-coming-to-wordpress-core-6-9/"] + "correct_answer": "A", + "explanation": "WordPress 6.9 registers two core ability categories: 'site' (abilities that retrieve or modify site information and settings) and 'user' (abilities that retrieve or modify user information and settings).", + "references": [ + "https://make.wordpress.org/core/2025/11/25/wordpress-6-9-field-guide/" + ] }, { - "id": "k-abilities-003", + "id": "k-abilities-005", + "category": "abilities-api", + "subcategory": "rest-api", + "difficulty": "intermediate", + "prompt": "What is the REST API namespace for the Abilities API?", + "type": "short_answer", + "correct_answer": "wp-abilities/v1", + "answer_type": "contains", + "explanation": "The Abilities API uses its own REST namespace wp-abilities/v1, separate from the standard wp/v2. It provides endpoints for listing abilities, running them, and listing categories.", + "references": [ + "https://developer.wordpress.org/rest-api/" + ] + }, + { + "id": "k-abilities-006", "category": "abilities-api", "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "Ability names must follow a specific format. What pattern is required?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "2 to 4 lowercase segments separated by forward slashes" + }, + { + "key": "B", + "text": "A single namespace/name pair like block types" + }, + { + "key": "C", + "text": "Any valid PHP function name" + }, + { + "key": "D", + "text": "A dot-separated path like JavaScript modules" + } + ], + "correct_answer": "A", + "explanation": "Ability names must match the regex /^[a-z0-9-]+(?:\\/[a-z0-9-]+){1,3}$/ -- 2 to 4 lowercase alphanumeric segments separated by forward slashes. For example: core/get-site-info.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_abilities_registry/" + ] + }, + { + "id": "k-abilities-007", + "category": "abilities-api", + "subcategory": "annotations", + "difficulty": "hard", + "prompt": "What are the three annotation properties on a WP_Ability?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "readonly, destructive, idempotent" + }, + { + "key": "B", + "text": "readable, writable, cacheable" + }, + { + "key": "C", + "text": "public, private, protected" + }, + { + "key": "D", + "text": "get, post, delete" + } + ], + "correct_answer": "A", + "explanation": "WP_Ability has three annotation properties: readonly (whether the ability only reads data), destructive (whether it destroys data), and idempotent (whether repeated calls produce the same result). All default to null.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_ability/" + ] + }, + { + "id": "k-abilities-008", + "category": "abilities-api", + "subcategory": "rest-api", "difficulty": "hard", - "prompt": "Which hook must you use to register ability categories so WordPress 6.9 persists them without a _doing_it_wrong notice?", + "prompt": "When a REST API request executes an ability annotated as readonly=true, what HTTP method is required?", + "type": "short_answer", + "correct_answer": "GET", + "answer_type": "exact", + "explanation": "The REST run endpoint enforces HTTP methods based on annotations: readonly=true requires GET; destructive=true AND idempotent=true requires DELETE; all other cases require POST.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_rest_abilities_v1_run_controller/" + ] + }, + { + "id": "k-abilities-009", + "category": "abilities-api", + "subcategory": "execution", + "difficulty": "hard", + "prompt": "What is the execution flow inside WP_Ability::execute()?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "normalize_input, validate_input, check_permissions, fire wp_before_execute_ability, do_execute, validate_output, fire wp_after_execute_ability" + }, + { + "key": "B", + "text": "check_permissions, validate_input, do_execute, validate_output" + }, + { + "key": "C", + "text": "do_execute, validate_output, fire wp_ability_complete" + }, + { + "key": "D", + "text": "authorize, sanitize, execute, cache_result" + } + ], + "correct_answer": "A", + "explanation": "WP_Ability::execute() follows a strict pipeline: normalize_input -> validate_input -> check_permissions -> fire wp_before_execute_ability action -> do_execute (the actual callback) -> validate_output -> fire wp_after_execute_ability action -> return result.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_ability/execute/" + ] + }, + { + "id": "k-abilities-010", + "category": "abilities-api", + "subcategory": "rest-api", + "difficulty": "intermediate", + "prompt": "How do you expose a custom ability over the REST API when calling wp_register_ability()?", "type": "multiple_choice", "choices": [ - { "key": "A", "text": "init" }, - { "key": "B", "text": "wp_abilities_api_init" }, - { "key": "C", "text": "wp_abilities_api_categories_init" }, - { "key": "D", "text": "rest_api_init" } + { + "key": "A", + "text": "Set 'rest' => true in the args" + }, + { + "key": "B", + "text": "Add a REST route manually" + }, + { + "key": "C", + "text": "Pass 'show_in_rest' => true in the meta array" + }, + { + "key": "D", + "text": "It is always exposed automatically" + } ], "correct_answer": "C", - "explanation": "Categories are validated separately from abilities; WordPress 6.9 only saves them when registered on wp_abilities_api_categories_init.", - "references": ["https://make.wordpress.org/core/2025/11/10/abilities-api-in-wordpress-6-9/"] + "explanation": "Abilities are opt-in to REST; set show_in_rest => true in the meta array when registering. The default is false (WP_Ability::DEFAULT_SHOW_IN_REST = false).", + "references": [ + "https://developer.wordpress.org/reference/functions/wp_register_ability/" + ] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/block-api.json b/datasets/suites/wp-core-v1/knowledge/block-api.json deleted file mode 100644 index 3371aa9..0000000 --- a/datasets/suites/wp-core-v1/knowledge/block-api.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "wp-core-knowledge-v1-block_api", - "version": "1.1.0", - "metadata": { - "name": "WordPress Core Knowledge - Block API", - "description": "Tests knowledge of WordPress Block API internals", - "wp_version": "6.9", - "created_at": "2025-12-16" - }, - "tests": [ - { - "id": "k-blockprocessor-001", - "category": "block-api", - "difficulty": "hard", - "prompt": "In WordPress 6.9, which WP_Block_Processor method replaced extract_block() to return a parsed block and advance the cursor?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "extract_block()" }, - { "key": "B", "text": "extract_current_block()" }, - { "key": "C", "text": "extract_full_block_and_advance()" }, - { "key": "D", "text": "get_block_and_seek()" } - ], - "correct_answer": "C", - "explanation": "The RC renamed extract_block() to extract_full_block_and_advance() before the 6.9 release; older names now fail.", - "references": ["https://make.wordpress.org/core/2025/11/19/introducing-the-streaming-block-parser-in-wordpress-6-9/"] - }, - { - "id": "k-blockprocessor-attrs-001", - "category": "block-api", - "difficulty": "hard", - "prompt": "When using WP_Block_Processor in 6.9, which method triggers JSON parsing of the current block's attributes (and is intentionally verbose about its cost)?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "get_block_attributes()" }, - { "key": "B", "text": "allocate_and_return_parsed_attributes()" }, - { "key": "C", "text": "parse_attributes()" }, - { "key": "D", "text": "get_attributes_json()" } - ], - "correct_answer": "B", - "explanation": "allocate_and_return_parsed_attributes() is the costly, explicitly named method that parses JSON attributes on demand in WP_Block_Processor.", - "references": ["https://make.wordpress.org/core/2025/11/19/introducing-the-streaming-block-parser-in-wordpress-6-9/"] - } - ] -} diff --git a/datasets/suites/wp-core-v1/knowledge/block-bindings.json b/datasets/suites/wp-core-v1/knowledge/block-bindings.json deleted file mode 100644 index 9012e94..0000000 --- a/datasets/suites/wp-core-v1/knowledge/block-bindings.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "wp-core-knowledge-v1-block_bindings", - "version": "1.1.0", - "metadata": { - "name": "WordPress Core Knowledge - Block Bindings", - "description": "Tests knowledge of WordPress Block Bindings API", - "wp_version": "6.9", - "created_at": "2025-12-16" - }, - "tests": [ - { - "id": "k-bindings-001", - "category": "block-bindings", - "difficulty": "basic", - "prompt": "Which function registers a custom block bindings source?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "register_block_source()" }, - { "key": "B", "text": "register_block_bindings_source()" }, - { "key": "C", "text": "add_block_binding()" }, - { "key": "D", "text": "wp_register_binding()" } - ], - "correct_answer": "B", - "explanation": "register_block_bindings_source() adds a new source to the Block Bindings Registry.", - "references": ["https://developer.wordpress.org/news/2024/04/16/block-bindings-api-in-core/"] - }, - { - "id": "k-bindings-002", - "category": "block-bindings", - "difficulty": "intermediate", - "prompt": "Which filter lets you declare which attributes of a specific block type can be bound via the Block Bindings API?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "block_bindings_attributes" }, - { "key": "B", "text": "block_bindings_supported_attributes_{block_type}" }, - { "key": "C", "text": "block_supported_attributes" }, - { "key": "D", "text": "block_attributes_whitelist" } - ], - "correct_answer": "B", - "explanation": "Use block_bindings_supported_attributes_{block_type} to opt specific attributes into bindings.", - "references": ["https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-bindings.md#supported-attributes"] - }, - { - "id": "k-bindings-editor-001", - "category": "block-bindings", - "difficulty": "hard", - "prompt": "How do you expose a custom Block Bindings source in the 6.9 editor UI so its fields appear in the binding dropdown?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Add supports.bindings UI flags to block.json" }, - { "key": "B", "text": "Provide a getFieldsList() method on the source registration" }, - { "key": "C", "text": "Register the source on enqueue_block_editor_assets" }, - { "key": "D", "text": "Set show_in_rest true when registering the source" } - ], - "correct_answer": "B", - "explanation": "6.9 surfaces custom sources in the editor when the registration function returns getFieldsList() with label/type definitions.", - "references": ["https://make.wordpress.org/core/2025/11/12/block-bindings-improvements-in-wordpress-6-9/"] - } - ] -} diff --git a/datasets/suites/wp-core-v1/knowledge/block-editor.json b/datasets/suites/wp-core-v1/knowledge/block-editor.json deleted file mode 100644 index bf6abef..0000000 --- a/datasets/suites/wp-core-v1/knowledge/block-editor.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "id": "wp-core-knowledge-v1-block_editor", - "version": "1.1.0", - "metadata": { - "name": "WordPress Core Knowledge - Block Editor", - "description": "Tests knowledge of WordPress Block Editor", - "wp_version": "6.9", - "created_at": "2025-12-16" - }, - "tests": [ - { - "id": "k-blocks-001", - "category": "block-editor", - "difficulty": "basic", - "prompt": "What function is used to register a block type in WordPress?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "wp_register_block()" }, - { "key": "B", "text": "add_block_type()" }, - { "key": "C", "text": "register_block_type()" }, - { "key": "D", "text": "create_block()" } - ], - "correct_answer": "C", - "explanation": "register_block_type() is the PHP function to register a block type.", - "references": ["https://developer.wordpress.org/reference/functions/register_block_type/"] - }, - { - "id": "k-visibility-001", - "category": "block-editor", - "difficulty": "basic", - "prompt": "Which block support flag enables the built-in \"Hide block\" toggle introduced in recent WordPress versions?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "supports.lock" }, - { "key": "B", "text": "supports.visibility" }, - { "key": "C", "text": "supports.hidden" }, - { "key": "D", "text": "supports.render" } - ], - "correct_answer": "B", - "explanation": "Setting supports.visibility to true enables the Hide/Show control for the block.", - "references": ["https://wptavern.com/wordpress-6-7-beta-1-brings-enhanced-block-visibility"] - }, - { - "id": "k-blockapi-001", - "category": "block-editor", - "difficulty": "intermediate", - "prompt": "For blocks that run inside the iframe-backed editor in WordPress 6.9+, which Block API version must block.json declare?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "apiVersion: 1" }, - { "key": "B", "text": "apiVersion: 2" }, - { "key": "C", "text": "apiVersion: 3" }, - { "key": "D", "text": "Any version is accepted" } - ], - "correct_answer": "C", - "explanation": "apiVersion 3 is required for iframe-compatible block.json validation.", - "references": ["https://make.wordpress.org/core/2024/12/05/whats-new-in-gutenberg-19-5-0/"] - }, - { - "id": "k-accordion-001", - "category": "block-editor", - "difficulty": "basic", - "prompt": "Which new core block introduced in WordPress 6.9 provides collapsible content sections?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Tabs" }, - { "key": "B", "text": "Details" }, - { "key": "C", "text": "Accordion" }, - { "key": "D", "text": "Summary" } - ], - "correct_answer": "C", - "explanation": "WordPress 6.9 ships with a native Accordion block for collapsible sections.", - "references": ["https://developer.wordpress.org/news/2024/12/wordpress-6-9-beta-1/"] - } - ] -} diff --git a/datasets/suites/wp-core-v1/knowledge/block-hooks.json b/datasets/suites/wp-core-v1/knowledge/block-hooks.json deleted file mode 100644 index ec6328e..0000000 --- a/datasets/suites/wp-core-v1/knowledge/block-hooks.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "id": "wp-core-knowledge-v1-block_hooks", - "version": "1.1.0", - "metadata": { - "name": "WordPress Core Knowledge - Block Hooks", - "description": "Tests knowledge of WordPress Block Hooks API", - "wp_version": "6.9", - "created_at": "2025-12-16" - }, - "tests": [ - { - "id": "k-blockhooks-context-001", - "category": "block-hooks", - "difficulty": "hard", - "prompt": "Why might a hooked block fail to appear when a user adds a new anchor block after activating the plugin in WordPress 6.9?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Hooked blocks only work on classic themes" }, - { "key": "B", "text": "Hooks only fire for dynamic blocks" }, - { "key": "C", "text": "Hooked blocks are applied to templates present at activation; anchors added later are not auto-filled" }, - { "key": "D", "text": "Block hooks require a REST request to refresh" } - ], - "correct_answer": "C", - "explanation": "Current block hooks implementation auto-inserts into existing templates at activation time; anchors created afterward are not backfilled, so the hooked block stays missing until manually added.", - "references": ["https://core.trac.wordpress.org/ticket/63608"] - }, - { - "id": "k-blockhooks-postcontent-001", - "category": "block-hooks", - "difficulty": "hard", - "prompt": "Why won't a block hook that targets core/post-content appear inside regular post content in WordPress 6.9?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Block hooks only work on classic themes" }, - { "key": "B", "text": "Hooks are limited to templates, template parts, and patterns—not per-post content" }, - { "key": "C", "text": "core/post-content is a static block so hooks are skipped" }, - { "key": "D", "text": "Hooks require REST context" } - ], - "correct_answer": "B", - "explanation": "Block hooks are applied to theme templates, template parts, and patterns when registered; user-authored post content isn't rewritten, so the hooked block never appears there.", - "references": ["https://make.wordpress.org/core/2023/10/15/introducing-block-hooks-for-dynamic-blocks/"] - }, - { - "id": "k-blockhooks-multiple-001", - "category": "block-hooks", - "difficulty": "hard", - "prompt": "What happens when a hooked block declares \"multiple\": false and an instance already exists in the current context?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Another instance is inserted after the anchor" }, - { "key": "B", "text": "The hook is skipped for that context" }, - { "key": "C", "text": "The hook throws a block validation error" }, - { "key": "D", "text": "The hook is deferred to render time" } - ], - "correct_answer": "B", - "explanation": "With multiple:false, block hooks insert at most one instance per context; if one already exists, no new instance is injected.", - "references": ["https://core.trac.wordpress.org/ticket/61902"] - }, - { - "id": "k-blockhooks-ignored-001", - "category": "block-hooks", - "difficulty": "hard", - "prompt": "How do user-modified templates prevent auto-insertion of a hooked block after removal?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Set multiple:false on the hooked block" }, - { "key": "B", "text": "Add the block to ignoredHookedBlocks on the anchor block" }, - { "key": "C", "text": "Disable block hooks in theme.json" }, - { "key": "D", "text": "Rename the anchor block" } - ], - "correct_answer": "B", - "explanation": "User edits persist by storing the removed block type in the anchor's ignoredHookedBlocks array, which stops re-insertion on render.", - "references": ["https://make.wordpress.org/core/2024/03/04/updates-to-block-hooks-in-6-5/"] - }, - { - "id": "k-blockhooks-context-002", - "category": "block-hooks", - "difficulty": "hard", - "prompt": "Where are block hooks applied by default as of WordPress 6.9?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Only classic template files" }, - { "key": "B", "text": "Templates, template parts, patterns, and navigation posts" }, - { "key": "C", "text": "Post content and reusable blocks only" }, - { "key": "D", "text": "Widgets and sidebars only" } - ], - "correct_answer": "B", - "explanation": "Block hooks run in layout contexts (templates, template parts, patterns, navigation posts), not inside post content.", - "references": ["https://make.wordpress.org/core/2023/10/15/introducing-block-hooks-for-dynamic-blocks/"] - } - ] -} diff --git a/datasets/suites/wp-core-v1/knowledge/caching.json b/datasets/suites/wp-core-v1/knowledge/caching.json index 10fb22b..3f77f2a 100644 --- a/datasets/suites/wp-core-v1/knowledge/caching.json +++ b/datasets/suites/wp-core-v1/knowledge/caching.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-caching", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Caching", "description": "Tests knowledge of WordPress caching APIs", @@ -11,6 +11,7 @@ { "id": "k-transients-001", "category": "caching", + "subcategory": "transients", "difficulty": "basic", "prompt": "What function retrieves a transient value in WordPress?", "type": "multiple_choice", @@ -27,6 +28,7 @@ { "id": "k-cache-salted-001", "category": "caching", + "subcategory": "salted_cache", "difficulty": "hard", "prompt": "Which helper should you use to reuse WP_Query cache keys while still invalidating when last_changed updates in WordPress 6.9?", "type": "multiple_choice", @@ -43,6 +45,7 @@ { "id": "k-object-cache-found-001", "category": "caching", + "subcategory": "object_cache", "difficulty": "hard", "prompt": "When storing falsey values in an object cache group, how do you distinguish a cached false from a cache miss with WordPress 6.9 APIs?", "type": "multiple_choice", @@ -55,6 +58,185 @@ "correct_answer": "B", "explanation": "WP_Object_Cache::get and wp_cache_get_multiple* report hits through the passed-by-reference $found flag/miss list, allowing false or 0 values to be distinguished from absent keys.", "references": ["https://developer.wordpress.org/reference/classes/wp_object_cache/get/"] + }, + { + "id": "k-caching-001", + "category": "caching", + "subcategory": "object_cache", + "difficulty": "basic", + "prompt": "When an empty string is passed as the cache group to wp_cache_set(), what group name is used internally?", + "type": "short_answer", + "correct_answer": "default", + "answer_type": "exact", + "explanation": "WP_Object_Cache methods check for empty group and replace it with 'default'. This is the implicit group for all cache operations that don't specify one.", + "references": ["https://developer.wordpress.org/reference/classes/wp_object_cache/"] + }, + { + "id": "k-caching-002", + "category": "caching", + "subcategory": "object_cache", + "difficulty": "basic", + "prompt": "What is the key difference between wp_cache_add() and wp_cache_set()?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "wp_cache_add() fails if the key already exists; wp_cache_set() always overwrites" }, + { "key": "B", "text": "wp_cache_add() is persistent; wp_cache_set() is runtime-only" }, + { "key": "C", "text": "wp_cache_add() supports groups; wp_cache_set() does not" }, + { "key": "D", "text": "wp_cache_add() increments an existing value; wp_cache_set() replaces it" } + ], + "correct_answer": "A", + "explanation": "wp_cache_add() returns false if the key already exists in the cache, making it safe for race conditions. wp_cache_set() unconditionally stores the value, overwriting any existing data.", + "references": ["https://developer.wordpress.org/reference/functions/wp_cache_add/"] + }, + { + "id": "k-caching-003", + "category": "caching", + "subcategory": "transients", + "difficulty": "intermediate", + "prompt": "Without an external object cache, how are transients with an expiration stored in the database?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "A single row in a dedicated wp_transients table" }, + { "key": "B", "text": "Two options: _transient_{name} for the value and _transient_timeout_{name} for the expiration timestamp" }, + { "key": "C", "text": "A single option _transient_{name} containing both value and expiration in a serialized array" }, + { "key": "D", "text": "An entry in the wp_postmeta table tied to a special transients post" } + ], + "correct_answer": "B", + "explanation": "Without an external cache, transients with a non-zero expiration are stored as two separate options in the wp_options table: one for the value and one for the timeout (time() + expiration). Both are stored as non-autoloaded options.", + "references": ["https://developer.wordpress.org/reference/functions/set_transient/"] + }, + { + "id": "k-caching-004", + "category": "caching", + "subcategory": "transients", + "difficulty": "intermediate", + "prompt": "What happens when you call set_transient() with an expiration of 0?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "The transient is not stored" }, + { "key": "B", "text": "The transient never expires and is stored as an autoloaded option (without external cache)" }, + { "key": "C", "text": "The transient expires immediately on the next request" }, + { "key": "D", "text": "It defaults to 1 hour" } + ], + "correct_answer": "B", + "explanation": "An expiration of 0 means the transient never expires. Without an external cache, it's stored as an autoloaded option with no timeout option created. It persists until explicitly deleted.", + "references": ["https://developer.wordpress.org/reference/functions/set_transient/"] + }, + { + "id": "k-caching-005", + "category": "caching", + "subcategory": "object_cache", + "difficulty": "intermediate", + "prompt": "What is the filename of the drop-in file that WordPress looks for to load an external persistent object cache?", + "type": "short_answer", + "correct_answer": "object-cache.php", + "answer_type": "exact", + "explanation": "WordPress checks for WP_CONTENT_DIR/object-cache.php. If this file exists and defines wp_cache_init(), WordPress sets wp_using_ext_object_cache(true) and uses it instead of the default WP_Object_Cache.", + "references": ["https://developer.wordpress.org/reference/functions/wp_using_ext_object_cache/"] + }, + { + "id": "k-caching-006", + "category": "caching", + "subcategory": "transients", + "difficulty": "intermediate", + "prompt": "When an external object cache is active, in which cache group are regular transients stored?", + "type": "short_answer", + "correct_answer": "transient", + "answer_type": "exact", + "explanation": "With an external object cache, set_transient() stores data via wp_cache_set($transient, $value, 'transient', $expiration). Site transients use the 'site-transient' group instead.", + "references": ["https://developer.wordpress.org/reference/functions/set_transient/"] + }, + { + "id": "k-caching-007", + "category": "caching", + "subcategory": "object_cache", + "difficulty": "hard", + "prompt": "Which of these is a non-persistent cache group that WordPress core registers by default?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "counts" }, + { "key": "B", "text": "posts" }, + { "key": "C", "text": "options" }, + { "key": "D", "text": "users" } + ], + "correct_answer": "A", + "explanation": "The 'counts' group is one of the non-persistent cache groups registered by WordPress core (along with 'plugins' and 'theme_json'). Non-persistent groups are never stored in an external persistent cache backend. Groups like 'posts', 'options', and 'users' are persistent by default.", + "references": ["https://developer.wordpress.org/reference/functions/wp_cache_add_non_persistent_groups/"] + }, + { + "id": "k-caching-008", + "category": "caching", + "subcategory": "invalidation", + "difficulty": "hard", + "prompt": "What value does wp_cache_get_last_changed() store as the 'last_changed' token?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "An auto-incrementing integer" }, + { "key": "B", "text": "The result of microtime() — a string with microseconds and Unix timestamp" }, + { "key": "C", "text": "A random UUID v4 string" }, + { "key": "D", "text": "The Unix timestamp from time()" } + ], + "correct_answer": "B", + "explanation": "wp_cache_set_last_changed() calls microtime() and stores the result. The microtime string provides sufficient precision to detect rapid successive changes.", + "references": ["https://developer.wordpress.org/reference/functions/wp_cache_get_last_changed/"] + }, + { + "id": "k-caching-009", + "category": "caching", + "subcategory": "invalidation", + "difficulty": "hard", + "prompt": "When clean_post_cache() is called, how does it invalidate all cached WP_Query results?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It calls wp_cache_flush_group('post-queries')" }, + { "key": "B", "text": "It deletes all cache keys matching a 'post-queries:*' pattern" }, + { "key": "C", "text": "It calls wp_cache_set_posts_last_changed() which updates the 'last_changed' token in the 'posts' group, causing salted cache reads to reject stale data" }, + { "key": "D", "text": "It iterates through all cached queries and deletes them individually" } + ], + "correct_answer": "C", + "explanation": "clean_post_cache() calls wp_cache_set_posts_last_changed(), which updates the 'last_changed' key in the 'posts' cache group. Since WP_Query uses salted caching, stored results with the old salt no longer match and are effectively invalidated.", + "references": ["https://developer.wordpress.org/reference/functions/clean_post_cache/"] + }, + { + "id": "k-caching-010", + "category": "caching", + "subcategory": "object_cache", + "difficulty": "intermediate", + "prompt": "In which WordPress version was wp_cache_flush_group() introduced?", + "type": "short_answer", + "correct_answer": "6.1", + "answer_type": "contains", + "explanation": "wp_cache_flush_group() was introduced in WordPress 6.1.0. The default WP_Object_Cache implementation simply unsets the entire group array from its internal $cache property.", + "references": ["https://developer.wordpress.org/reference/functions/wp_cache_flush_group/"] + }, + { + "id": "k-caching-011", + "category": "caching", + "subcategory": "transients", + "difficulty": "hard", + "prompt": "What is the maximum length of a transient name for set_transient()?", + "type": "short_answer", + "correct_answer": "172", + "answer_type": "exact", + "explanation": "Transient names must be 172 characters or fewer for set_transient(). Site transients have an even shorter limit of 167 characters. These limits account for the '_transient_' prefix within the options table's option_name column.", + "references": ["https://developer.wordpress.org/reference/functions/set_transient/"] + }, + { + "id": "k-caching-012", + "category": "caching", + "subcategory": "object_cache", + "difficulty": "hard", + "prompt": "The default WP_Object_Cache clones PHP objects when storing and retrieving them. Why?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "To ensure serialization compatibility with external backends" }, + { "key": "B", "text": "To prevent accidental mutation — modifications to a retrieved object won't affect the cached copy, and vice versa" }, + { "key": "C", "text": "To reduce memory usage via copy-on-write" }, + { "key": "D", "text": "To support multisite blog switching" } + ], + "correct_answer": "B", + "explanation": "WP_Object_Cache clones objects on both set() and get() to prevent accidental mutation. Without cloning, modifying a retrieved object would also modify the cached copy since PHP objects are passed by reference.", + "references": ["https://developer.wordpress.org/reference/classes/wp_object_cache/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/dataviews.json b/datasets/suites/wp-core-v1/knowledge/dataviews.json deleted file mode 100644 index 05844ff..0000000 --- a/datasets/suites/wp-core-v1/knowledge/dataviews.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "id": "wp-core-knowledge-v1-dataviews", - "version": "1.1.0", - "metadata": { - "name": "WordPress Core Knowledge - DataViews", - "description": "Tests knowledge of WordPress DataViews API", - "wp_version": "6.9", - "created_at": "2025-12-16" - }, - "tests": [ - { - "id": "k-dataviews-locked-001", - "category": "dataviews", - "difficulty": "hard", - "prompt": "In WordPress 6.9 DataViews, how do you make a filter visible but non-editable by users?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Set editable:false on the view" }, - { "key": "B", "text": "Use isLocked:true on the filter definition" }, - { "key": "C", "text": "Omit the operator field" }, - { "key": "D", "text": "Register the filter server-side only" } - ], - "correct_answer": "B", - "explanation": "Locked filters are defined with isLocked:true, keeping them applied while preventing user edits or removal.", - "references": ["https://make.wordpress.org/core/2025/11/11/dataviews-dataform-et-al-in-wordpress-6-9/"] - }, - { - "id": "k-dataviews-groupby-001", - "category": "dataviews", - "difficulty": "hard", - "prompt": "Which view option groups items by a field in the enhanced DataViews API in 6.9?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "group" }, - { "key": "B", "text": "groupByField" }, - { "key": "C", "text": "aggregateBy" }, - { "key": "D", "text": "clusterField" } - ], - "correct_answer": "B", - "explanation": "Set groupByField to a field ID to have table, grid, or list layouts group items by that field.", - "references": ["https://make.wordpress.org/core/2025/11/11/dataviews-dataform-et-al-in-wordpress-6-9/"] - }, - { - "id": "k-dataviews-readonly-001", - "category": "dataviews", - "difficulty": "hard", - "prompt": "Which property marks a DataViews field as display-only in WordPress 6.9?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "locked" }, - { "key": "B", "text": "readOnly" }, - { "key": "C", "text": "editable:false" }, - { "key": "D", "text": "viewOnly" } - ], - "correct_answer": "B", - "explanation": "DataViews fields accept readOnly:true to render a non-editable value while still showing in the UI.", - "references": ["https://make.wordpress.org/core/2025/11/11/dataviews-dataform-et-al-in-wordpress-6-9/"] - }, - { - "id": "k-dataviews-getelements-001", - "category": "dataviews", - "difficulty": "hard", - "prompt": "What does the new getElements() hook in DataViews 6.9 do?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Groups table rows by a field" }, - { "key": "B", "text": "Fetches filter option values lazily on demand" }, - { "key": "C", "text": "Locks filters from user edits" }, - { "key": "D", "text": "Persists user layouts to theme.json" } - ], - "correct_answer": "B", - "explanation": "getElements returns options asynchronously when a user opens a filter/edit control, avoiding upfront fetches.", - "references": ["https://make.wordpress.org/core/2025/11/11/dataviews-dataform-et-al-in-wordpress-6-9/"] - } - ] -} diff --git a/datasets/suites/wp-core-v1/knowledge/font-library.json b/datasets/suites/wp-core-v1/knowledge/font-library.json deleted file mode 100644 index bea9029..0000000 --- a/datasets/suites/wp-core-v1/knowledge/font-library.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "wp-core-knowledge-v1-font_library", - "version": "1.1.0", - "metadata": { - "name": "WordPress Core Knowledge - Font Library", - "description": "Tests knowledge of WordPress Font Library API", - "wp_version": "6.9", - "created_at": "2025-12-16" - }, - "tests": [ - { - "id": "k-font-library-disable-001", - "category": "font-library", - "difficulty": "hard", - "prompt": "What is the supported way to hide the Font Library UI across editors in WordPress 6.9?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Remove the Fonts menu via remove_menu_page()" }, - { "key": "B", "text": "Set fontLibraryEnabled=false in the block_editor_settings_all filter" }, - { "key": "C", "text": "Define DISABLE_FONT_LIBRARY in wp-config.php" }, - { "key": "D", "text": "Unregister wp_font_family post type" } - ], - "correct_answer": "B", - "explanation": "Filtering block_editor_settings_all and setting fontLibraryEnabled to false disables the Font Library UI globally without unregistering post types.", - "references": ["https://developer.wordpress.org/news/snippets/how-to-disable-the-font-library/"] - } - ] -} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-block-api.json b/datasets/suites/wp-core-v1/knowledge/gb-block-api.json new file mode 100644 index 0000000..d8e188b --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-block-api.json @@ -0,0 +1,440 @@ +{ + "id": "wp-core-knowledge-v1-block_api", + "version": "1.3.0", + "metadata": { + "name": "WordPress Core Knowledge - Block API", + "description": "Tests knowledge of WordPress Block API internals", + "wp_version": "6.9", + "created_at": "2025-12-16" + }, + "tests": [ + { + "id": "k-blockprocessor-001", + "category": "gb-block-api", + "subcategory": "streaming-parser", + "difficulty": "hard", + "prompt": "In WordPress 6.9, which WP_Block_Processor method replaced extract_block() to return a parsed block and advance the cursor?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "extract_block()" + }, + { + "key": "B", + "text": "extract_current_block()" + }, + { + "key": "C", + "text": "extract_full_block_and_advance()" + }, + { + "key": "D", + "text": "get_block_and_seek()" + } + ], + "correct_answer": "C", + "explanation": "The RC renamed extract_block() to extract_full_block_and_advance() before the 6.9 release; older names now fail.", + "references": [ + "https://make.wordpress.org/core/2025/11/19/introducing-the-streaming-block-parser-in-wordpress-6-9/" + ] + }, + { + "id": "k-blockprocessor-attrs-001", + "category": "gb-block-api", + "subcategory": "streaming-parser", + "difficulty": "hard", + "prompt": "When using WP_Block_Processor in 6.9, which method triggers JSON parsing of the current block's attributes (and is intentionally verbose about its cost)?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "get_block_attributes()" + }, + { + "key": "B", + "text": "allocate_and_return_parsed_attributes()" + }, + { + "key": "C", + "text": "parse_attributes()" + }, + { + "key": "D", + "text": "get_attributes_json()" + } + ], + "correct_answer": "B", + "explanation": "allocate_and_return_parsed_attributes() is the costly, explicitly named method that parses JSON attributes on demand in WP_Block_Processor.", + "references": [ + "https://make.wordpress.org/core/2025/11/19/introducing-the-streaming-block-parser-in-wordpress-6-9/" + ] + }, + { + "id": "k-blockapi-003", + "category": "gb-block-api", + "subcategory": "registration", + "difficulty": "basic", + "prompt": "WordPress block type names must match a specific pattern. Which of these is a valid block type name?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "my-plugin/my-block" + }, + { + "key": "B", + "text": "MyPlugin/MyBlock" + }, + { + "key": "C", + "text": "my_plugin_my_block" + }, + { + "key": "D", + "text": "my-plugin:my-block" + } + ], + "correct_answer": "A", + "explanation": "Block type names must match the regex /^[a-z0-9-]+\\/[a-z0-9-]+$/ -- lowercase alphanumeric and dashes, with a required forward slash separator between namespace and name. No uppercase, underscores, or colons.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block_type_registry/register/" + ] + }, + { + "id": "k-blockapi-004", + "category": "gb-block-api", + "subcategory": "parsing", + "difficulty": "basic", + "prompt": "When WP_Block_Parser encounters a block comment like with no explicit namespace prefix, what namespace does it assign?", + "type": "short_answer", + "correct_answer": "core", + "answer_type": "contains", + "explanation": "WP_Block_Parser defaults the namespace to 'core/' when none is present. So produces blockName 'core/paragraph'.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block_parser/" + ] + }, + { + "id": "k-blockapi-005", + "category": "gb-block-api", + "subcategory": "parsing", + "difficulty": "basic", + "prompt": "What does parse_blocks() set as blockName for freeform HTML content that isn't inside a block comment delimiter?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "core/freeform" + }, + { + "key": "B", + "text": "null" + }, + { + "key": "C", + "text": "An empty string" + }, + { + "key": "D", + "text": "core/html" + } + ], + "correct_answer": "B", + "explanation": "Freeform HTML (content not inside block delimiters) is represented as a WP_Block_Parser_Block with blockName set to null. The freeform() method creates new WP_Block_Parser_Block( null, ... ).", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block_parser/" + ] + }, + { + "id": "k-blockapi-006", + "category": "gb-block-api", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "WP_Block_Type defines a GLOBAL_ATTRIBUTES constant that adds attributes to every registered block. What are these two global attributes?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "lock (object) and metadata (object)" + }, + { + "key": "B", + "text": "className (string) and anchor (string)" + }, + { + "key": "C", + "text": "align (string) and style (object)" + }, + { + "key": "D", + "text": "id (string) and className (string)" + } + ], + "correct_answer": "A", + "explanation": "WP_Block_Type::GLOBAL_ATTRIBUTES contains 'lock' (type: object) and 'metadata' (type: object). These are merged into every block's attributes in set_props(), giving all blocks locking and metadata capabilities.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block_type/" + ] + }, + { + "id": "k-blockapi-007", + "category": "gb-block-api", + "subcategory": "rendering", + "difficulty": "intermediate", + "prompt": "When block.json specifies a 'render' PHP template file (since 6.1), which three variables are available inside that template?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "$attributes, $content, $block" + }, + { + "key": "B", + "text": "$args, $inner_content, $context" + }, + { + "key": "C", + "text": "$block_attributes, $block_content, $block_type" + }, + { + "key": "D", + "text": "$attributes, $inner_html, $instance" + } + ], + "correct_answer": "A", + "explanation": "The render_callback closure created from the 'render' field in block.json receives $attributes (array), $content (string of inner HTML), and $block (WP_Block instance), and makes them available inside the required PHP file.", + "references": [ + "https://developer.wordpress.org/reference/functions/register_block_type_from_metadata/" + ] + }, + { + "id": "k-blockapi-008", + "category": "gb-block-api", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "What filter allows plugins to modify the raw block.json metadata array before it is processed into registration settings?", + "type": "short_answer", + "correct_answer": "block_type_metadata", + "answer_type": "contains", + "explanation": "The block_type_metadata filter fires in register_block_type_from_metadata() on the raw metadata array. A second filter, block_type_metadata_settings, fires later on the processed settings before final registration.", + "references": [ + "https://developer.wordpress.org/reference/hooks/block_type_metadata/" + ] + }, + { + "id": "k-blockapi-009", + "category": "gb-block-api", + "subcategory": "parsing", + "difficulty": "intermediate", + "prompt": "What filter allows plugins to replace the default WP_Block_Parser class used by parse_blocks()?", + "type": "short_answer", + "correct_answer": "block_parser_class", + "answer_type": "contains", + "explanation": "parse_blocks() applies the block_parser_class filter with default value 'WP_Block_Parser'. Plugins can return a different class name to use a custom parser implementation.", + "references": [ + "https://developer.wordpress.org/reference/functions/parse_blocks/" + ] + }, + { + "id": "k-blockapi-010", + "category": "gb-block-api", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "WP_Block_Type_Registry is declared as a final class using which design pattern?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Singleton with a static get_instance() method" + }, + { + "key": "B", + "text": "Factory with a static create() method" + }, + { + "key": "C", + "text": "Service locator with dependency injection" + }, + { + "key": "D", + "text": "Registry with an event-driven observer pattern" + } + ], + "correct_answer": "A", + "explanation": "WP_Block_Type_Registry is a final class that uses the singleton pattern. The only way to obtain it is via the static get_instance() method, which returns the single shared instance.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block_type_registry/" + ] + }, + { + "id": "k-blockapi-011", + "category": "gb-block-api", + "subcategory": "rendering", + "difficulty": "hard", + "prompt": "WP_Block_Type::is_dynamic() determines if a block is dynamic. What does it check?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Whether the block has a 'render' field in block.json" + }, + { + "key": "B", + "text": "is_callable( $this->render_callback )" + }, + { + "key": "C", + "text": "Whether the block's supports include 'dynamic' => true" + }, + { + "key": "D", + "text": "Whether the block type was registered with register_block_type_from_metadata()" + } + ], + "correct_answer": "B", + "explanation": "is_dynamic() simply returns is_callable( $this->render_callback ). If the render_callback property is a valid callable, the block is dynamic. This is true whether the callback was set directly or generated from the 'render' field in block.json.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block_type/is_dynamic/" + ] + }, + { + "id": "k-blockapi-012", + "category": "gb-block-api", + "subcategory": "serialization", + "difficulty": "hard", + "prompt": "serialize_block_attributes() escapes the character sequence '--' to '\\u002d\\u002d' in JSON output. What does this prevent?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Accidental closing of block comment delimiters ()" + }, + { + "key": "B", + "text": "CSS custom property injection via block attributes" + }, + { + "key": "C", + "text": "JavaScript template literal interpolation" + }, + { + "key": "D", + "text": "HTML entity decoding in attribute values" + } + ], + "correct_answer": "A", + "explanation": "Block attributes are embedded inside HTML comments (). The '--' sequence inside HTML comments would prematurely close the comment, so it must be escaped to \\u002d\\u002d. Similar escaping applies to <, >, &, \\\\ and \\\".", + "references": [ + "https://developer.wordpress.org/reference/functions/serialize_block_attributes/" + ] + }, + { + "id": "k-blockapi-013", + "category": "gb-block-api", + "subcategory": "rendering", + "difficulty": "hard", + "prompt": "When WP_Block::render() calls a dynamic block's render_callback, how many arguments does it pass?", + "type": "short_answer", + "correct_answer": "3", + "answer_type": "exact", + "explanation": "WP_Block::render() passes 3 arguments: $this->attributes (array), $block_content (string), and $this (the WP_Block instance). Note this differs from WP_Block_Type::render(), which only passes 2 arguments.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block/render/" + ] + }, + { + "id": "k-blockapi-014", + "category": "gb-block-api", + "subcategory": "rendering", + "difficulty": "hard", + "prompt": "Some block types set skip_inner_blocks to true. What effect does this have during WP_Block::render()?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Inner blocks are removed from the parsed block tree entirely" + }, + { + "key": "B", + "text": "Inner blocks are not pre-rendered; the render_callback handles them itself" + }, + { + "key": "C", + "text": "Inner blocks are rendered but their output is discarded" + }, + { + "key": "D", + "text": "Inner block scripts and styles are not enqueued" + } + ], + "correct_answer": "B", + "explanation": "When skip_inner_blocks is true, WP_Block::render() skips the normal pre-rendering of inner blocks. The dynamic render_callback is expected to iterate and render them itself. Blocks like core/post-template use this to control inner block rendering context.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block/render/" + ] + }, + { + "id": "k-blockapi-015", + "category": "gb-block-api", + "subcategory": "serialization", + "difficulty": "basic", + "prompt": "What CSS class is automatically generated for a block based on its block type name?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "wp-block-{name} (with the namespace prefix stripped)" + }, + { + "key": "B", + "text": "block-{namespace}-{name}" + }, + { + "key": "C", + "text": "wp-{namespace}--{name}" + }, + { + "key": "D", + "text": "gutenberg-block-{name}" + } + ], + "correct_answer": "A", + "explanation": "WordPress generates a default CSS class by stripping the 'core/' namespace prefix and prepending 'wp-block-'. For example, 'core/button' becomes 'wp-block-button', and 'my-plugin/my-block' becomes 'wp-block-my-plugin-my-block'. This can be disabled with supports.className: false.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/" + ] + }, + { + "id": "k-blockapi-016", + "category": "gb-block-api", + "subcategory": "deprecation", + "difficulty": "intermediate", + "prompt": "What are the six valid keys allowed in a block type deprecation entry?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "attributes, supports, save, migrate, isEligible, apiVersion" + }, + { + "key": "B", + "text": "attributes, edit, save, transform, validate, version" + }, + { + "key": "C", + "text": "name, attributes, save, migrate, deprecated, category" + }, + { + "key": "D", + "text": "attributes, supports, render, migrate, isEligible, title" + } + ], + "correct_answer": "A", + "explanation": "Block deprecation entries support exactly six keys defined in DEPRECATED_ENTRY_KEYS: 'attributes' (old attribute schema), 'supports' (old supports), 'save' (old save function), 'migrate' (transforms old attributes/inner blocks to new), 'isEligible' (checks if block needs migration), and 'apiVersion' (API version for the deprecated save).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-deprecation/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-block-bindings.json b/datasets/suites/wp-core-v1/knowledge/gb-block-bindings.json new file mode 100644 index 0000000..71c5477 --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-block-bindings.json @@ -0,0 +1,288 @@ +{ + "id": "wp-core-knowledge-v1-block_bindings", + "version": "1.2.0", + "metadata": { + "name": "WordPress Core Knowledge - Block Bindings", + "description": "Tests knowledge of WordPress Block Bindings API", + "wp_version": "6.9", + "created_at": "2025-12-16" + }, + "tests": [ + { + "id": "k-bindings-001", + "category": "gb-block-bindings", + "subcategory": "registration", + "difficulty": "basic", + "prompt": "Which function registers a custom block bindings source?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "register_block_source()" + }, + { + "key": "B", + "text": "register_block_bindings_source()" + }, + { + "key": "C", + "text": "add_block_binding()" + }, + { + "key": "D", + "text": "wp_register_binding()" + } + ], + "correct_answer": "B", + "explanation": "register_block_bindings_source() adds a new source to the WP_Block_Bindings_Registry. It requires a namespaced source name, a label, and a get_value_callback.", + "references": [ + "https://developer.wordpress.org/reference/functions/register_block_bindings_source/" + ] + }, + { + "id": "k-bindings-002", + "category": "gb-block-bindings", + "subcategory": "sources", + "difficulty": "basic", + "prompt": "How many built-in block binding sources ship with WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "2 (post-meta, pattern-overrides)" + }, + { + "key": "B", + "text": "3 (post-meta, pattern-overrides, post-data)" + }, + { + "key": "C", + "text": "4 (post-meta, pattern-overrides, post-data, term-data)" + }, + { + "key": "D", + "text": "5 (post-meta, pattern-overrides, post-data, term-data, site-data)" + } + ], + "correct_answer": "C", + "explanation": "WordPress 6.9 ships with four built-in binding sources: core/post-meta (since 6.5), core/pattern-overrides (since 6.5), core/post-data (since 6.9), and core/term-data (since 6.9).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-bindings/" + ] + }, + { + "id": "k-bindings-003", + "category": "gb-block-bindings", + "subcategory": "supported-blocks", + "difficulty": "basic", + "prompt": "Which of these blocks supports bindings by default in WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "core/paragraph, core/heading, core/image, and core/button" + }, + { + "key": "B", + "text": "All core blocks support bindings" + }, + { + "key": "C", + "text": "Only core/paragraph and core/heading" + }, + { + "key": "D", + "text": "Only blocks with supports.bindings in block.json" + } + ], + "correct_answer": "A", + "explanation": "The default supported blocks are core/paragraph (content), core/heading (content), core/image (id, url, title, alt, caption), core/button (url, text, linkTarget, rel), plus core/post-date, core/navigation-link, and core/navigation-submenu.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-bindings/" + ] + }, + { + "id": "k-bindings-004", + "category": "gb-block-bindings", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "What is the callback signature for a block binding source's get_value_callback?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "function($source_args, $block_instance, $attribute_name)" + }, + { + "key": "B", + "text": "function($block_instance, $attribute_name)" + }, + { + "key": "C", + "text": "function($source_name, $source_args)" + }, + { + "key": "D", + "text": "function($attribute_name, $value, $context)" + } + ], + "correct_answer": "A", + "explanation": "The get_value_callback receives three arguments: $source_args (binding-specific arguments like the meta key), $block_instance (the WP_Block being rendered), and $attribute_name (the bound attribute name like 'content' or 'url').", + "references": [ + "https://developer.wordpress.org/reference/functions/register_block_bindings_source/" + ] + }, + { + "id": "k-bindings-005", + "category": "gb-block-bindings", + "subcategory": "storage", + "difficulty": "intermediate", + "prompt": "Where in a block's attributes structure are bindings defined?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "attrs.metadata.bindings" + }, + { + "key": "B", + "text": "attrs.bindings" + }, + { + "key": "C", + "text": "attrs.supports.bindings" + }, + { + "key": "D", + "text": "attrs.data.bindings" + } + ], + "correct_answer": "A", + "explanation": "Bindings are stored in the block's attrs.metadata.bindings object. The 'metadata' attribute is one of WP_Block_Type's GLOBAL_ATTRIBUTES, available on every block. Each key maps an attribute name to a binding source and args.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-bindings/" + ] + }, + { + "id": "k-bindings-006", + "category": "gb-block-bindings", + "subcategory": "filters", + "difficulty": "intermediate", + "prompt": "What filter fires after a binding source's get_value_callback returns a value, allowing plugins to modify any bound value?", + "type": "short_answer", + "correct_answer": "block_bindings_source_value", + "answer_type": "contains", + "explanation": "The block_bindings_source_value filter (since 6.7) fires in WP_Block_Bindings_Source::get_value() with five arguments: the value, source name, source args, block instance, and attribute name.", + "references": [ + "https://developer.wordpress.org/reference/hooks/block_bindings_source_value/" + ] + }, + { + "id": "k-bindings-007", + "category": "gb-block-bindings", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "Which filter lets you declare which attributes of a specific block type can be bound via the Block Bindings API?", + "type": "short_answer", + "correct_answer": "block_bindings_supported_attributes", + "answer_type": "contains", + "explanation": "The block_bindings_supported_attributes filter and its dynamic variant block_bindings_supported_attributes_{$block_type} (both since 6.9) control which attributes can be bound per block type.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-bindings/" + ] + }, + { + "id": "k-bindings-008", + "category": "gb-block-bindings", + "subcategory": "sources", + "difficulty": "hard", + "prompt": "For the core/post-meta binding source to return a value, which condition must the meta key satisfy?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "It must be registered with show_in_rest set to true" + }, + { + "key": "B", + "text": "It must be of type string" + }, + { + "key": "C", + "text": "It must be prefixed with _wp_" + }, + { + "key": "D", + "text": "It must have a non-null default value" + } + ], + "correct_answer": "A", + "explanation": "core/post-meta checks multiple conditions: the meta key must exist in source_args, the post must be publicly viewable or the user must have read_post cap, the meta key must not be protected, and the meta key must be registered with show_in_rest.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-bindings/" + ] + }, + { + "id": "k-bindings-009", + "category": "gb-block-bindings", + "subcategory": "sources", + "difficulty": "hard", + "prompt": "Which two block binding sources were added new in WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "core/post-data and core/term-data" + }, + { + "key": "B", + "text": "core/site-data and core/user-data" + }, + { + "key": "C", + "text": "core/custom-field and core/taxonomy" + }, + { + "key": "D", + "text": "core/widget-data and core/nav-data" + } + ], + "correct_answer": "A", + "explanation": "WordPress 6.9 added core/post-data (supports date, modified, link fields) and core/term-data (supports id, name, link, slug, description, parent, count). Both complement the original 6.5 sources: core/post-meta and core/pattern-overrides.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-bindings/" + ] + }, + { + "id": "k-bindings-010", + "category": "gb-block-bindings", + "subcategory": "rendering", + "difficulty": "hard", + "prompt": "When core/pattern-overrides encounters a binding with the key '__default', what does it do?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Uses the block's default attribute values" + }, + { + "key": "B", + "text": "Expands the binding to all supported attributes of that block type" + }, + { + "key": "C", + "text": "Falls back to the post meta with that key" + }, + { + "key": "D", + "text": "Skips the binding entirely" + } + ], + "correct_answer": "B", + "explanation": "The '__default' key in core/pattern-overrides is a shorthand that expands to cover all supported attributes of the current block type. This is handled in WP_Block::process_block_bindings(), which maps the __default source to each individual supported attribute.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-bindings/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-block-editor.json b/datasets/suites/wp-core-v1/knowledge/gb-block-editor.json new file mode 100644 index 0000000..01bc854 --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-block-editor.json @@ -0,0 +1,423 @@ +{ + "id": "wp-core-knowledge-v1-block_editor", + "version": "1.3.0", + "metadata": { + "name": "WordPress Core Knowledge - Block Editor", + "description": "Tests knowledge of WordPress Block Editor", + "wp_version": "6.9", + "created_at": "2025-12-16" + }, + "tests": [ + { + "id": "k-blocks-001", + "category": "gb-block-editor", + "subcategory": "registration", + "difficulty": "basic", + "prompt": "What function is used to register a block type in WordPress?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "wp_register_block()" + }, + { + "key": "B", + "text": "add_block_type()" + }, + { + "key": "C", + "text": "register_block_type()" + }, + { + "key": "D", + "text": "create_block()" + } + ], + "correct_answer": "C", + "explanation": "register_block_type() is the PHP function to register a block type. Since 5.8, it also accepts a path to a block.json file, delegating to register_block_type_from_metadata().", + "references": [ + "https://developer.wordpress.org/reference/functions/register_block_type/" + ] + }, + { + "id": "k-visibility-001", + "category": "gb-block-editor", + "subcategory": "supports", + "difficulty": "basic", + "prompt": "Which block support flag enables the built-in block visibility toggle in the editor?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "supports.lock" + }, + { + "key": "B", + "text": "supports.visibility" + }, + { + "key": "C", + "text": "supports.hidden" + }, + { + "key": "D", + "text": "supports.render" + } + ], + "correct_answer": "B", + "explanation": "Setting supports.visibility to true enables the visibility control for the block. When blockVisibility is set to false, the block's content is suppressed during rendering.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/" + ] + }, + { + "id": "k-blockapi-001", + "category": "gb-block-editor", + "subcategory": "api-version", + "difficulty": "intermediate", + "prompt": "For blocks that run inside the iframe-backed editor in WordPress 6.9+, which Block API version must block.json declare?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "apiVersion: 1" + }, + { + "key": "B", + "text": "apiVersion: 2" + }, + { + "key": "C", + "text": "apiVersion: 3" + }, + { + "key": "D", + "text": "Any version is accepted" + } + ], + "correct_answer": "C", + "explanation": "apiVersion 3 is required for blocks to work correctly in the iframe-backed editor.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/" + ] + }, + { + "id": "k-accordion-001", + "category": "gb-block-editor", + "subcategory": "core-blocks", + "difficulty": "basic", + "prompt": "Which new core block introduced in WordPress 6.9 provides collapsible content sections?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Tabs" + }, + { + "key": "B", + "text": "Details" + }, + { + "key": "C", + "text": "Accordion" + }, + { + "key": "D", + "text": "Summary" + } + ], + "correct_answer": "C", + "explanation": "WordPress 6.9 ships with a native Accordion block for collapsible content sections, using the Interactivity API for client-side behavior.", + "references": [ + "https://make.wordpress.org/core/2025/11/25/wordpress-6-9-field-guide/" + ] + }, + { + "id": "k-blockeditor-005", + "category": "gb-block-editor", + "subcategory": "theme-json", + "difficulty": "basic", + "prompt": "What is the latest theme.json schema version as defined by WP_Theme_JSON::LATEST_SCHEMA?", + "type": "short_answer", + "correct_answer": "3", + "answer_type": "exact", + "explanation": "WP_Theme_JSON::LATEST_SCHEMA is 3. Version 1 was introduced in WP 5.8, version 2 in 5.9, and version 3 in 6.6.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/" + ] + }, + { + "id": "k-blockeditor-006", + "category": "gb-block-editor", + "subcategory": "categories", + "difficulty": "basic", + "prompt": "Which of these is NOT a default block category in WordPress?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "navigation" + }, + { + "key": "B", + "text": "text" + }, + { + "key": "C", + "text": "theme" + }, + { + "key": "D", + "text": "embed" + } + ], + "correct_answer": "A", + "explanation": "There is no 'navigation' block category. The default categories registered by get_default_block_categories() are: text, media, design, widgets, theme, embed, and reusable (labeled 'Patterns' in the UI since 6.3). Navigation blocks fall under the 'theme' category.", + "references": [ + "https://developer.wordpress.org/reference/functions/get_default_block_categories/" + ] + }, + { + "id": "k-blockeditor-007", + "category": "gb-block-editor", + "subcategory": "categories", + "difficulty": "basic", + "prompt": "What filter allows plugins to modify the list of block categories available in the editor?", + "type": "short_answer", + "correct_answer": "block_categories_all", + "answer_type": "contains", + "explanation": "The block_categories_all filter (since 5.8) replaces the deprecated block_categories filter. It receives the array of categories and a WP_Block_Editor_Context object.", + "references": [ + "https://developer.wordpress.org/reference/hooks/block_categories_all/" + ] + }, + { + "id": "k-blockeditor-008", + "category": "gb-block-editor", + "subcategory": "theme-json", + "difficulty": "intermediate", + "prompt": "WP_Theme_JSON merges data from four origins. What is the correct merge priority order (lowest to highest)?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "default, blocks, theme, custom" + }, + { + "key": "B", + "text": "core, plugin, theme, user" + }, + { + "key": "C", + "text": "global, block, theme, local" + }, + { + "key": "D", + "text": "base, extension, theme, override" + } + ], + "correct_answer": "A", + "explanation": "WP_Theme_JSON defines VALID_ORIGINS as: 'default' (core data), 'blocks' (block.json files, since 6.1), 'theme' (theme.json), and 'custom' (user customizations via wp_global_styles). Each subsequent origin overrides the previous.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/" + ] + }, + { + "id": "k-blockeditor-009", + "category": "gb-block-editor", + "subcategory": "global-styles", + "difficulty": "intermediate", + "prompt": "What post type stores user global style customizations in WordPress?", + "type": "short_answer", + "correct_answer": "wp_global_styles", + "answer_type": "contains", + "explanation": "User customizations from the Global Styles panel are stored in the wp_global_styles custom post type. The post content contains JSON matching the theme.json structure, scoped to the current theme via the wp_theme taxonomy.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/" + ] + }, + { + "id": "k-blockeditor-010", + "category": "gb-block-editor", + "subcategory": "layout", + "difficulty": "intermediate", + "prompt": "What are the four layout types available in WordPress block themes?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "flow, constrained, flex, grid" + }, + { + "key": "B", + "text": "block, inline, flex, grid" + }, + { + "key": "C", + "text": "flow, fixed, flex, grid" + }, + { + "key": "D", + "text": "default, wide, flex, grid" + } + ], + "correct_answer": "A", + "explanation": "WordPress defines four layout types: 'flow' (default block layout), 'constrained' (enforces content/wide max-widths), 'flex' (CSS flexbox), and 'grid' (CSS grid). Each generates its own layout class (is-layout-flow, is-layout-constrained, etc.).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/" + ] + }, + { + "id": "k-blockeditor-011", + "category": "gb-block-editor", + "subcategory": "theme-json", + "difficulty": "intermediate", + "prompt": "In theme.json, what CSS custom property pattern do color palette presets generate?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "--wp--preset--color--{slug}" + }, + { + "key": "B", + "text": "--wp-color-{slug}" + }, + { + "key": "C", + "text": "--theme-color--{slug}" + }, + { + "key": "D", + "text": "--wp--custom--color--{slug}" + } + ], + "correct_answer": "A", + "explanation": "Color palette presets generate CSS custom properties following the pattern --wp--preset--color--{slug}. All preset types follow a similar pattern: --wp--preset--{type}--{slug} (e.g., --wp--preset--font-size--large, --wp--preset--spacing--20).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/" + ] + }, + { + "id": "k-blockeditor-012", + "category": "gb-block-editor", + "subcategory": "theme-json", + "difficulty": "intermediate", + "prompt": "What is the ROOT_BLOCK_SELECTOR constant in WP_Theme_JSON?", + "type": "short_answer", + "correct_answer": "body", + "answer_type": "exact", + "explanation": "WP_Theme_JSON::ROOT_BLOCK_SELECTOR is 'body'. This is the CSS selector used for root-level block styles. A separate constant ROOT_CSS_PROPERTIES_SELECTOR is ':root' and is used for CSS custom properties.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_theme_json/" + ] + }, + { + "id": "k-blockeditor-013", + "category": "gb-block-editor", + "subcategory": "patterns", + "difficulty": "hard", + "prompt": "When register_block_pattern() specifies a filePath instead of content, how is the pattern content loaded?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "The file is included via PHP include at registration time" + }, + { + "key": "B", + "text": "The file is lazy-loaded via include only when the pattern content is requested" + }, + { + "key": "C", + "text": "The file path is converted to a URL and fetched via HTTP" + }, + { + "key": "D", + "text": "The file is read into a string via file_get_contents()" + } + ], + "correct_answer": "B", + "explanation": "When filePath is set instead of content, WP_Block_Patterns_Registry lazy-loads the content via include only when get_content() is called. This avoids the cost of loading pattern content for patterns that are never displayed.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_block_patterns_registry/" + ] + }, + { + "id": "k-blockeditor-014", + "category": "gb-block-editor", + "subcategory": "script-modules", + "difficulty": "hard", + "prompt": "In the WordPress Script Modules API (since 6.5), what are the two types of dependencies a module can declare?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "static and dynamic" + }, + { + "key": "B", + "text": "sync and async" + }, + { + "key": "C", + "text": "required and optional" + }, + { + "key": "D", + "text": "blocking and deferred" + } + ], + "correct_answer": "A", + "explanation": "Script module dependencies can be 'static' or 'dynamic'. Static dependencies are automatically added to the import map and preloaded. Dynamic dependencies are only added to the import map but not preloaded -- they load on demand when imported.", + "references": [ + "https://developer.wordpress.org/reference/functions/wp_register_script_module/" + ] + }, + { + "id": "k-blockeditor-015", + "category": "gb-block-editor", + "subcategory": "theme-json", + "difficulty": "hard", + "prompt": "In theme.json, which two elements support pseudo selectors like :hover, :focus, and :active for styling?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "link and button" + }, + { + "key": "B", + "text": "heading and caption" + }, + { + "key": "C", + "text": "button and input" + }, + { + "key": "D", + "text": "link and heading" + } + ], + "correct_answer": "A", + "explanation": "WP_Theme_JSON::VALID_ELEMENT_PSEUDO_SELECTORS defines only 'link' and 'button' as elements that support pseudo selectors (:link, :any-link, :visited, :hover, :focus, :focus-visible, :active).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/" + ] + }, + { + "id": "k-blockeditor-016", + "category": "gb-block-editor", + "subcategory": "layout", + "difficulty": "intermediate", + "prompt": "What CSS class does the 'constrained' layout type generate on its container element?", + "type": "short_answer", + "correct_answer": "is-layout-constrained", + "answer_type": "contains", + "explanation": "Each layout type generates its own CSS class: 'flow' produces is-layout-flow, 'constrained' produces is-layout-constrained, 'flex' produces is-layout-flex, and 'grid' produces is-layout-grid. These classes are added to container elements to apply the corresponding layout styles.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-block-hooks.json b/datasets/suites/wp-core-v1/knowledge/gb-block-hooks.json new file mode 100644 index 0000000..757e7af --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-block-hooks.json @@ -0,0 +1,288 @@ +{ + "id": "wp-core-knowledge-v1-block_hooks", + "version": "1.2.0", + "metadata": { + "name": "WordPress Core Knowledge - Block Hooks", + "description": "Tests knowledge of WordPress Block Hooks API", + "wp_version": "6.9", + "created_at": "2025-12-16" + }, + "tests": [ + { + "id": "k-blockhooks-001", + "category": "gb-block-hooks", + "subcategory": "positions", + "difficulty": "basic", + "prompt": "What are the four valid relative positions for block hooks in WordPress?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "before, after, first_child, last_child" + }, + { + "key": "B", + "text": "before, after, prepend, append" + }, + { + "key": "C", + "text": "top, bottom, before, after" + }, + { + "key": "D", + "text": "start, end, inside_start, inside_end" + } + ], + "correct_answer": "A", + "explanation": "Block hooks support four positions: 'before' (before the anchor), 'after' (after the anchor), 'first_child' (first child inside the anchor), and 'last_child' (last child inside the anchor). In block.json, camelCase (firstChild, lastChild) is mapped to snake_case.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#block-hooks" + ] + }, + { + "id": "k-blockhooks-002", + "category": "gb-block-hooks", + "subcategory": "registration", + "difficulty": "basic", + "prompt": "In block.json, which field declares the anchor blocks and positions for a block hook?", + "type": "short_answer", + "correct_answer": "blockHooks", + "answer_type": "contains", + "explanation": "The blockHooks field in block.json is an object mapping anchor block type names to relative positions (e.g., {\"core/navigation\": \"after\"}). It is processed during register_block_type_from_metadata().", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#block-hooks" + ] + }, + { + "id": "k-blockhooks-003", + "category": "gb-block-hooks", + "subcategory": "filters", + "difficulty": "intermediate", + "prompt": "What filter allows plugins to modify which blocks are hooked at a specific position relative to an anchor block?", + "type": "short_answer", + "correct_answer": "hooked_block_types", + "answer_type": "contains", + "explanation": "The hooked_block_types filter fires in insert_hooked_blocks() with four arguments: the hooked block types array, relative position, anchor block type, and the context (template/post/pattern).", + "references": [ + "https://developer.wordpress.org/reference/hooks/hooked_block_types/" + ] + }, + { + "id": "k-blockhooks-004", + "category": "gb-block-hooks", + "subcategory": "rendering", + "difficulty": "intermediate", + "prompt": "On the_content filter, at what priority does block hooks processing run relative to do_blocks?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Priority 8, before do_blocks at priority 9" + }, + { + "key": "B", + "text": "Priority 10, after do_blocks at priority 9" + }, + { + "key": "C", + "text": "Same priority 9, but registered first" + }, + { + "key": "D", + "text": "Priority 5, much earlier than do_blocks" + } + ], + "correct_answer": "A", + "explanation": "apply_block_hooks_to_content_from_post_object is registered on the_content at priority 8, while do_blocks runs at priority 9. This ensures hooked blocks are inserted into the content before blocks are rendered.", + "references": [ + "https://developer.wordpress.org/reference/functions/apply_block_hooks_to_content/" + ] + }, + { + "id": "k-blockhooks-005", + "category": "gb-block-hooks", + "subcategory": "persistence", + "difficulty": "intermediate", + "prompt": "For posts, pages, and navigation items, where is the ignoredHookedBlocks metadata persisted?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "In the block's attributes within the serialized post content" + }, + { + "key": "B", + "text": "In post meta with key _wp_ignored_hooked_blocks" + }, + { + "key": "C", + "text": "In the wp_options table" + }, + { + "key": "D", + "text": "In a custom taxonomy term" + } + ], + "correct_answer": "B", + "explanation": "For post types (pages, posts, navigation, reusable blocks), ignoredHookedBlocks is stored as JSON-encoded data in post meta under the key _wp_ignored_hooked_blocks. For templates and template parts, it is stored inline in the block's attrs.metadata.ignoredHookedBlocks array.", + "references": [ + "https://developer.wordpress.org/reference/functions/set_ignored_hooked_blocks_metadata/" + ] + }, + { + "id": "k-blockhooks-context-001", + "category": "gb-block-hooks", + "subcategory": "context", + "difficulty": "hard", + "prompt": "Why might a hooked block fail to appear when a user adds a new anchor block after activating the plugin in WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Hooked blocks only work on classic themes" + }, + { + "key": "B", + "text": "Hooks only fire for dynamic blocks" + }, + { + "key": "C", + "text": "Hooked blocks are applied to templates present at activation; anchors added later are not auto-filled" + }, + { + "key": "D", + "text": "Block hooks require a REST request to refresh" + } + ], + "correct_answer": "C", + "explanation": "Current block hooks implementation auto-inserts into existing templates at activation time; anchors created afterward are not backfilled, so the hooked block stays missing until manually added.", + "references": [ + "https://core.trac.wordpress.org/ticket/63608" + ] + }, + { + "id": "k-blockhooks-postcontent-001", + "category": "gb-block-hooks", + "subcategory": "context", + "difficulty": "hard", + "prompt": "Why won't a block hook that targets core/post-content appear inside regular post content in WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Block hooks only work on classic themes" + }, + { + "key": "B", + "text": "Hooks are limited to templates, template parts, and patterns -- not per-post content" + }, + { + "key": "C", + "text": "core/post-content is a static block so hooks are skipped" + }, + { + "key": "D", + "text": "Hooks require REST context" + } + ], + "correct_answer": "B", + "explanation": "Block hooks are applied to theme templates, template parts, and patterns when registered; user-authored post content isn't rewritten, so the hooked block never appears there.", + "references": [ + "https://make.wordpress.org/core/2023/10/15/introducing-block-hooks-for-dynamic-blocks/" + ] + }, + { + "id": "k-blockhooks-multiple-001", + "category": "gb-block-hooks", + "subcategory": "deduplication", + "difficulty": "hard", + "prompt": "What happens when a hooked block declares \"multiple\": false and an instance already exists in the current context?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Another instance is inserted after the anchor" + }, + { + "key": "B", + "text": "The hook is skipped for that context" + }, + { + "key": "C", + "text": "The hook throws a block validation error" + }, + { + "key": "D", + "text": "The hook is deferred to render time" + } + ], + "correct_answer": "B", + "explanation": "With multiple:false, block hooks insert at most one instance per context; if one already exists, no new instance is injected.", + "references": [ + "https://core.trac.wordpress.org/ticket/61902" + ] + }, + { + "id": "k-blockhooks-ignored-001", + "category": "gb-block-hooks", + "subcategory": "persistence", + "difficulty": "hard", + "prompt": "How do user-modified templates prevent auto-insertion of a hooked block after removal?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Set multiple:false on the hooked block" + }, + { + "key": "B", + "text": "Add the block to ignoredHookedBlocks on the anchor block" + }, + { + "key": "C", + "text": "Disable block hooks in theme.json" + }, + { + "key": "D", + "text": "Rename the anchor block" + } + ], + "correct_answer": "B", + "explanation": "User edits persist by storing the removed block type in the anchor's ignoredHookedBlocks array, which stops re-insertion on render.", + "references": [ + "https://make.wordpress.org/core/2024/03/04/updates-to-block-hooks-in-6-5/" + ] + }, + { + "id": "k-blockhooks-context-002", + "category": "gb-block-hooks", + "subcategory": "context", + "difficulty": "intermediate", + "prompt": "Where are block hooks applied by default as of WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Only classic template files" + }, + { + "key": "B", + "text": "Templates, template parts, patterns, and navigation posts" + }, + { + "key": "C", + "text": "Post content and reusable blocks only" + }, + { + "key": "D", + "text": "Widgets and sidebars only" + } + ], + "correct_answer": "B", + "explanation": "Block hooks run in layout contexts (templates, template parts, patterns, navigation posts), not inside post content.", + "references": [ + "https://make.wordpress.org/core/2023/10/15/introducing-block-hooks-for-dynamic-blocks/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-blocks.json b/datasets/suites/wp-core-v1/knowledge/gb-blocks.json new file mode 100644 index 0000000..7bb70f7 --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-blocks.json @@ -0,0 +1,324 @@ +{ + "id": "wp-core-knowledge-v1-wp_blocks", + "version": "1.0.0", + "metadata": { + "name": "WordPress Core Knowledge - @wordpress/blocks", + "description": "Tests knowledge of the @wordpress/blocks JavaScript block API", + "wp_version": "6.9", + "created_at": "2026-02-24" + }, + "tests": [ + { + "id": "k-wpblocks-001", + "category": "gb-blocks", + "subcategory": "store", + "difficulty": "basic", + "prompt": "What is the @wordpress/data store name for the @wordpress/blocks package?", + "type": "short_answer", + "correct_answer": "core/blocks", + "answer_type": "exact", + "explanation": "The blocks store is registered under the name 'core/blocks' (STORE_NAME constant). It holds the registry of block types, categories, collections, styles, and variations. Accessed via select('core/blocks') or dispatch('core/blocks').", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-002", + "category": "gb-blocks", + "subcategory": "factory", + "difficulty": "basic", + "prompt": "What function creates a new block instance in the JavaScript block editor?", + "type": "short_answer", + "correct_answer": "createBlock", + "answer_type": "contains", + "explanation": "createBlock(name, attributes, innerBlocks) returns a block object with a UUID v4 clientId, the block name, sanitized attributes, innerBlocks array, and isValid set to true. Attributes are validated against the block type's attribute schema.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-003", + "category": "gb-blocks", + "subcategory": "constants", + "difficulty": "basic", + "prompt": "What is the value of the BLOCK_ICON_DEFAULT constant in @wordpress/blocks?", + "type": "short_answer", + "correct_answer": "block-default", + "answer_type": "exact", + "explanation": "BLOCK_ICON_DEFAULT is 'block-default', which is the Dashicon slug used when a block type does not specify a custom icon. The icon system also supports SVG elements and React components.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-004", + "category": "gb-blocks", + "subcategory": "variations", + "difficulty": "intermediate", + "prompt": "What are the three scope values that control where a block variation appears?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "block, inserter, transform" + }, + { + "key": "B", + "text": "editor, frontend, api" + }, + { + "key": "C", + "text": "global, local, inline" + }, + { + "key": "D", + "text": "view, edit, preview" + } + ], + "correct_answer": "A", + "explanation": "WPBlockVariationScope has three values: 'block' (shown in block-level contexts), 'inserter' (appears in the block inserter UI), and 'transform' (available as a block transformation option). If scope is not specified, the variation appears in all scopes.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/" + ] + }, + { + "id": "k-wpblocks-005", + "category": "gb-blocks", + "subcategory": "transforms", + "difficulty": "intermediate", + "prompt": "What are the two direction values for getBlockTransforms()?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "'to' and 'from'" + }, + { + "key": "B", + "text": "'source' and 'target'" + }, + { + "key": "C", + "text": "'input' and 'output'" + }, + { + "key": "D", + "text": "'up' and 'down'" + } + ], + "correct_answer": "A", + "explanation": "getBlockTransforms(direction, blockTypeOrName) accepts 'to' (transforms this block can convert into) or 'from' (transforms other blocks can use to become this block). Each returned transform has a blockName property added automatically.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-transforms/" + ] + }, + { + "id": "k-wpblocks-006", + "category": "gb-blocks", + "subcategory": "serialization", + "difficulty": "intermediate", + "prompt": "What is the HTML comment delimiter format used to serialize a block with content?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "content" + }, + { + "key": "B", + "text": "content" + }, + { + "key": "C", + "text": "[wp:name {attrs}]content[/wp:name]" + }, + { + "key": "D", + "text": "{{wp name attrs}}content{{/wp name}}" + } + ], + "correct_answer": "A", + "explanation": "Blocks are serialized as HTML comments: opening delimiter , followed by the HTML content, followed by closing delimiter . Self-closing blocks (no content) use .", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-007", + "category": "gb-blocks", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "What function registers a visual style variant for an existing block type?", + "type": "short_answer", + "correct_answer": "registerBlockStyle", + "answer_type": "contains", + "explanation": "registerBlockStyle(blockNames, styleVariation) adds a style variant to one or more blocks (blockNames can be a string or array). Each style variation has a name and label, and optionally isDefault. The corresponding CSS class is 'is-style-{name}'.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/" + ] + }, + { + "id": "k-wpblocks-008", + "category": "gb-blocks", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "What function registers a namespace collection of blocks with a shared title and icon?", + "type": "short_answer", + "correct_answer": "registerBlockCollection", + "answer_type": "contains", + "explanation": "registerBlockCollection(namespace, { title, icon }) groups all blocks from a namespace under a collection label in the inserter. For example, registerBlockCollection('my-plugin', { title: 'My Plugin' }) groups all 'my-plugin/*' blocks.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-009", + "category": "gb-blocks", + "subcategory": "templates", + "difficulty": "intermediate", + "prompt": "What function checks whether a set of blocks matches a template structure?", + "type": "short_answer", + "correct_answer": "doBlocksMatchTemplate", + "answer_type": "contains", + "explanation": "doBlocksMatchTemplate(blocks, template) recursively compares block names and inner block structure against a template array. Templates are arrays of [blockName, attributes, innerBlocksTemplate] tuples. Returns a boolean.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-010", + "category": "gb-blocks", + "subcategory": "transforms", + "difficulty": "hard", + "prompt": "What does isWildcardBlockTransform check?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Whether a transform targets '*' — meaning it accepts any block type" + }, + { + "key": "B", + "text": "Whether a transform uses glob patterns in block names" + }, + { + "key": "C", + "text": "Whether a transform matches blocks by regex" + }, + { + "key": "D", + "text": "Whether a transform applies to all blocks in a group" + } + ], + "correct_answer": "A", + "explanation": "isWildcardBlockTransform(transform) returns true if the transform's blocks array contains '*', indicating it can accept any block type as input. This is used for universal transforms like the 'Group' block which can wrap any selection.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-011", + "category": "gb-blocks", + "subcategory": "parsing", + "difficulty": "hard", + "prompt": "What are the attribute source types available for extracting block attribute values from saved HTML?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "attribute, property, text, html, query, rich-text" + }, + { + "key": "B", + "text": "dom, css, xpath, regex, json" + }, + { + "key": "C", + "text": "selector, content, meta, comment, style" + }, + { + "key": "D", + "text": "innerHTML, textContent, dataset, className, value" + } + ], + "correct_answer": "A", + "explanation": "Block attributes can be sourced from saved HTML via: 'attribute' (reads an HTML attribute via selector), 'property' (reads a DOM property), 'text' (text content), 'html' (innerHTML), 'query' (repeating fields via selector), and 'rich-text' (returns RichTextData). Without a source, attributes are stored in the block comment delimiter.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/" + ] + }, + { + "id": "k-wpblocks-012", + "category": "gb-blocks", + "subcategory": "variations", + "difficulty": "hard", + "prompt": "What property on a block variation makes it the one selected by default when inserting the parent block?", + "type": "short_answer", + "correct_answer": "isDefault", + "answer_type": "contains", + "explanation": "Setting isDefault: true on a variation makes it the default when the parent block is inserted. Only one variation should have isDefault set to true. The isActive property (function or string array) determines which variation matches the current block attributes.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/" + ] + }, + { + "id": "k-wpblocks-013", + "category": "gb-blocks", + "subcategory": "store", + "difficulty": "hard", + "prompt": "What selector checks whether a block type supports a specific feature like 'color' or 'typography'?", + "type": "short_answer", + "correct_answer": "hasBlockSupport", + "answer_type": "contains", + "explanation": "hasBlockSupport(state, nameOrType, feature, defaultSupports) returns a boolean. A related selector getBlockSupport returns the actual support configuration value. Both accept the feature as a dot-separated path (e.g., 'color.text').", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-014", + "category": "gb-blocks", + "subcategory": "factory", + "difficulty": "hard", + "prompt": "What does switchToBlockType return when transforming blocks?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "An array of the resulting blocks, or null if no valid transform exists" + }, + { + "key": "B", + "text": "A single transformed block instance" + }, + { + "key": "C", + "text": "A Promise that resolves to the transformed blocks" + }, + { + "key": "D", + "text": "A boolean indicating success or failure" + } + ], + "correct_answer": "A", + "explanation": "switchToBlockType(blocks, name) applies the matching transform and returns an array of resulting blocks (transforms can produce multiple blocks). It returns null if no valid transform exists between the source blocks and the target block type.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + }, + { + "id": "k-wpblocks-015", + "category": "gb-blocks", + "subcategory": "registration", + "difficulty": "intermediate", + "prompt": "What function registers a block bindings source on the JavaScript side?", + "type": "short_answer", + "correct_answer": "registerBlockBindingsSource", + "answer_type": "contains", + "explanation": "registerBlockBindingsSource(source) registers a client-side block bindings source. The source object requires a name (namespaced, e.g., 'my-plugin/my-source') and can include label, usesContext, getValues, setValues, canUserEditValue, and getFieldsList. Introduced in WordPress 6.7.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-core-data.json b/datasets/suites/wp-core-v1/knowledge/gb-core-data.json new file mode 100644 index 0000000..0b37747 --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-core-data.json @@ -0,0 +1,358 @@ +{ + "id": "wp-core-knowledge-v1-wp_core_data", + "version": "1.0.0", + "metadata": { + "name": "WordPress Core Knowledge - @wordpress/core-data", + "description": "Tests knowledge of the @wordpress/core-data entity data layer", + "wp_version": "6.9", + "created_at": "2026-02-24" + }, + "tests": [ + { + "id": "k-coredata-001", + "category": "gb-core-data", + "subcategory": "store", + "difficulty": "basic", + "prompt": "What is the store name (namespace) for the @wordpress/core-data package?", + "type": "short_answer", + "correct_answer": "core", + "answer_type": "exact", + "explanation": "The @wordpress/core-data store is registered under the name 'core' (exported as STORE_NAME). This is accessed via select('core') or dispatch('core'), or by importing the store descriptor directly.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-002", + "category": "gb-core-data", + "subcategory": "hooks", + "difficulty": "basic", + "prompt": "What React hook retrieves a single WordPress entity record with edit and save capabilities?", + "type": "short_answer", + "correct_answer": "useEntityRecord", + "answer_type": "contains", + "explanation": "useEntityRecord(kind, name, recordId, options) returns an object with record, editedRecord, edits, edit(), save(), isResolving, hasEdits, hasResolved, and status. It provides a complete CRUD interface for a single entity.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-003", + "category": "gb-core-data", + "subcategory": "selectors", + "difficulty": "basic", + "prompt": "What selector retrieves a single entity record from the core-data store?", + "type": "short_answer", + "correct_answer": "getEntityRecord", + "answer_type": "contains", + "explanation": "getEntityRecord(state, kind, name, key, query) returns a single record identified by kind (e.g., 'postType'), name (e.g., 'post'), and key (usually the record ID). A related selector, getEditedEntityRecord, returns the record with pending local edits merged in.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-004", + "category": "gb-core-data", + "subcategory": "actions", + "difficulty": "basic", + "prompt": "What action saves all pending local edits for an entity record to the server?", + "type": "short_answer", + "correct_answer": "saveEditedEntityRecord", + "answer_type": "contains", + "explanation": "saveEditedEntityRecord(kind, name, recordId, options) takes all pending edits made via editEntityRecord and persists them to the server via the REST API. A lower-level alternative is saveEntityRecord which saves a specific record object.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-005", + "category": "gb-core-data", + "subcategory": "entities", + "difficulty": "intermediate", + "prompt": "Entities in @wordpress/core-data are identified by a two-part key. What are the two parts called?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "kind and name" + }, + { + "key": "B", + "text": "type and slug" + }, + { + "key": "C", + "text": "namespace and resource" + }, + { + "key": "D", + "text": "group and identifier" + } + ], + "correct_answer": "A", + "explanation": "Entities are identified by 'kind' (grouping like 'root', 'postType', or 'taxonomy') and 'name' (specific entity like 'post', 'page', 'user'). For example, getEntityRecord('postType', 'post', 123) retrieves post ID 123.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-006", + "category": "gb-core-data", + "subcategory": "entities", + "difficulty": "intermediate", + "prompt": "What is the DEFAULT_ENTITY_KEY used when an entity config does not specify a custom key property?", + "type": "short_answer", + "correct_answer": "id", + "answer_type": "exact", + "explanation": "DEFAULT_ENTITY_KEY is 'id', meaning entity records are identified by their 'id' property by default. Some entities override this: postType and taxonomy use 'slug', menuLocation uses 'name', and theme uses 'stylesheet'.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-007", + "category": "gb-core-data", + "subcategory": "hooks", + "difficulty": "intermediate", + "prompt": "What does the useEntityProp hook return?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "A tuple: [value, setValue, fullValue]" + }, + { + "key": "B", + "text": "Just the property value" + }, + { + "key": "C", + "text": "An object with { value, onChange, reset }" + }, + { + "key": "D", + "text": "A tuple: [value, setValue]" + } + ], + "correct_answer": "A", + "explanation": "useEntityProp(kind, name, prop, id) returns a 3-element array: the edited property value, a setter function for local edits, and the full (unedited) property value from the server. The setter calls editEntityRecord internally.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-008", + "category": "gb-core-data", + "subcategory": "selectors", + "difficulty": "intermediate", + "prompt": "What selector checks whether the current user has permission to perform a CRUD action on a resource?", + "type": "short_answer", + "correct_answer": "canUser", + "answer_type": "contains", + "explanation": "canUser(state, action, resource, id) checks permissions where action is one of 'create', 'read', 'update', or 'delete'. Its resolver makes an OPTIONS request to the REST API endpoint to determine allowed methods.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-009", + "category": "gb-core-data", + "subcategory": "hooks", + "difficulty": "intermediate", + "prompt": "What are the four values of the Status enum returned by useEntityRecord and useEntityRecords?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "IDLE, RESOLVING, ERROR, SUCCESS" + }, + { + "key": "B", + "text": "PENDING, LOADING, FAILED, COMPLETE" + }, + { + "key": "C", + "text": "INIT, FETCHING, REJECTED, RESOLVED" + }, + { + "key": "D", + "text": "WAITING, ACTIVE, TIMEOUT, DONE" + } + ], + "correct_answer": "A", + "explanation": "The Status enum defines four states: IDLE (no request made yet), RESOLVING (request in progress), ERROR (request failed), and SUCCESS (data loaded). These are available on the 'status' property of the hook return value.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-010", + "category": "gb-core-data", + "subcategory": "actions", + "difficulty": "intermediate", + "prompt": "What is the difference between editEntityRecord and saveEntityRecord?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "editEntityRecord makes local in-memory edits; saveEntityRecord persists to the server via REST API" + }, + { + "key": "B", + "text": "editEntityRecord validates changes; saveEntityRecord skips validation" + }, + { + "key": "C", + "text": "editEntityRecord is for post types only; saveEntityRecord works with all entities" + }, + { + "key": "D", + "text": "editEntityRecord queues changes for batch saving; saveEntityRecord saves immediately without queuing" + } + ], + "correct_answer": "A", + "explanation": "editEntityRecord(kind, name, recordId, edits) stores changes locally in the Redux state with undo support. saveEntityRecord(kind, name, record, options) sends the record data to the server. Typically you call editEntityRecord for each change, then saveEditedEntityRecord to persist.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-011", + "category": "gb-core-data", + "subcategory": "entities", + "difficulty": "hard", + "prompt": "What are 'transient edits' in @wordpress/core-data entity configuration?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Edits that don't create undo levels, such as block content and selection state" + }, + { + "key": "B", + "text": "Edits that expire after a timeout period" + }, + { + "key": "C", + "text": "Edits stored in sessionStorage instead of Redux state" + }, + { + "key": "D", + "text": "Edits that are automatically reverted when the user navigates away" + } + ], + "correct_answer": "A", + "explanation": "transientEdits is an entity config property specifying which edit keys should NOT create new undo levels. For example, sidebars and widgets define { blocks: true } as transient, meaning block content changes don't individually stack in the undo history.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-012", + "category": "gb-core-data", + "subcategory": "entities", + "difficulty": "hard", + "prompt": "What does the mergedEdits property in an entity configuration control?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Edits to those keys are deep-merged with existing values instead of replacing them" + }, + { + "key": "B", + "text": "Edits from multiple users are merged in real-time collaboration" + }, + { + "key": "C", + "text": "Edits are merged into a single undo level" + }, + { + "key": "D", + "text": "Edits are merged with the server response after saving" + } + ], + "correct_answer": "A", + "explanation": "mergedEdits specifies keys where editEntityRecord should deep-merge instead of replace. For example, post types define { meta: true }, so editing a single meta key merges with existing meta rather than overwriting the entire meta object.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-013", + "category": "gb-core-data", + "subcategory": "selectors", + "difficulty": "hard", + "prompt": "What HTTP method does the canUser resolver use to check permissions against the REST API?", + "type": "short_answer", + "correct_answer": "OPTIONS", + "answer_type": "exact", + "explanation": "The canUser resolver sends an OPTIONS request to the entity's REST endpoint. The response's Allow header lists the permitted HTTP methods (GET, POST, PUT, DELETE), which are mapped to CRUD actions to determine what the current user can do.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-014", + "category": "gb-core-data", + "subcategory": "entities", + "difficulty": "hard", + "prompt": "How does @wordpress/core-data auto-generate convenience selectors and actions for root entities?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "It creates get/get/save/delete methods from the entity plural name" + }, + { + "key": "B", + "text": "It generates REST API endpoint handlers from the entity baseURL" + }, + { + "key": "C", + "text": "It creates React hooks for each entity type automatically" + }, + { + "key": "D", + "text": "It registers a separate Redux store for each entity type" + } + ], + "correct_answer": "A", + "explanation": "For root entities with a plural name, core-data dynamically generates convenience methods: getUser()/getUsers()/saveUser()/deleteUser() for the user entity, etc. These are thin wrappers around getEntityRecord/getEntityRecords/saveEntityRecord/deleteEntityRecord with the kind and name pre-bound.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + }, + { + "id": "k-coredata-015", + "category": "gb-core-data", + "subcategory": "selectors", + "difficulty": "hard", + "prompt": "What is the difference between getEntityRecord and getRawEntityRecord?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "getEntityRecord returns the record with edits merged; getRawEntityRecord returns the server-state record without local edits" + }, + { + "key": "B", + "text": "getEntityRecord parses HTML; getRawEntityRecord returns raw HTML strings" + }, + { + "key": "C", + "text": "getEntityRecord uses cache; getRawEntityRecord always fetches from the server" + }, + { + "key": "D", + "text": "getEntityRecord returns JSON; getRawEntityRecord returns XML" + } + ], + "correct_answer": "A", + "explanation": "getEntityRecord returns the record from the store (server data). getRawEntityRecord returns the same data but explicitly without any pending local edits merged in. For the version with edits merged, use getEditedEntityRecord.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-core-data/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-data-controls.json b/datasets/suites/wp-core-v1/knowledge/gb-data-controls.json new file mode 100644 index 0000000..99f7e27 --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-data-controls.json @@ -0,0 +1,226 @@ +{ + "id": "wp-core-knowledge-v1-wp_data_controls", + "version": "1.0.0", + "metadata": { + "name": "WordPress Core Knowledge - @wordpress/data-controls", + "description": "Tests knowledge of the @wordpress/data-controls async side-effect controls", + "wp_version": "6.9", + "created_at": "2026-02-24" + }, + "tests": [ + { + "id": "k-datacontrols-001", + "category": "gb-data-controls", + "subcategory": "purpose", + "difficulty": "basic", + "prompt": "What pattern does the @wordpress/data-controls package enable in @wordpress/data store action creators?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Generator functions that yield control descriptors for async side effects" + }, + { + "key": "B", + "text": "Decorator functions that wrap actions with logging" + }, + { + "key": "C", + "text": "Observable streams that emit action sequences" + }, + { + "key": "D", + "text": "Proxy objects that intercept action dispatches" + } + ], + "correct_answer": "A", + "explanation": "data-controls enables generator-based action creators (function*) that yield control descriptor objects. The @wordpress/data middleware intercepts these yields, executes the corresponding async operation (API fetch, select, dispatch), and returns the result to the generator.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data-controls/" + ] + }, + { + "id": "k-datacontrols-002", + "category": "gb-data-controls", + "subcategory": "exports", + "difficulty": "basic", + "prompt": "What is the primary non-deprecated control function exported by @wordpress/data-controls?", + "type": "short_answer", + "correct_answer": "apiFetch", + "answer_type": "contains", + "explanation": "apiFetch(request) is the main active export. It creates a control descriptor with type 'API_FETCH' that triggers a @wordpress/api-fetch call. The select, syncSelect, and dispatch exports are all deprecated since version 5.7 in favor of @wordpress/data built-ins.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data-controls/" + ] + }, + { + "id": "k-datacontrols-003", + "category": "gb-data-controls", + "subcategory": "controls", + "difficulty": "intermediate", + "prompt": "What control action type does the apiFetch control return?", + "type": "short_answer", + "correct_answer": "API_FETCH", + "answer_type": "exact", + "explanation": "apiFetch(request) returns a descriptor object with type: 'API_FETCH' and the request options. The controls handler then calls @wordpress/api-fetch's triggerFetch with the request, returning a Promise of the API response.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data-controls/" + ] + }, + { + "id": "k-datacontrols-004", + "category": "gb-data-controls", + "subcategory": "deprecation", + "difficulty": "intermediate", + "prompt": "The select, syncSelect, and dispatch exports from @wordpress/data-controls are deprecated. What replaces them?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Built-in controls with the same names in @wordpress/data itself" + }, + { + "key": "B", + "text": "React hooks (useSelect, useDispatch) from @wordpress/data" + }, + { + "key": "C", + "text": "Direct REST API calls via @wordpress/api-fetch" + }, + { + "key": "D", + "text": "The @wordpress/data-async package" + } + ], + "correct_answer": "A", + "explanation": "Since @wordpress/data version 5.7, the data package ships built-in controls for select, resolveSelect, and dispatch (with action types @@data/SELECT, @@data/RESOLVE_SELECT, @@data/DISPATCH). The @wordpress/data-controls versions now just alias these and emit deprecation warnings.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data-controls/" + ] + }, + { + "id": "k-datacontrols-005", + "category": "gb-data-controls", + "subcategory": "controls", + "difficulty": "intermediate", + "prompt": "What was the key difference between the deprecated select and syncSelect controls in @wordpress/data-controls?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "select waits for resolvers to complete (async); syncSelect returns the current value immediately (sync)" + }, + { + "key": "B", + "text": "select works with Redux stores; syncSelect works with generic stores" + }, + { + "key": "C", + "text": "select returns a Promise; syncSelect returns an Observable" + }, + { + "key": "D", + "text": "select reads from cache; syncSelect bypasses cache" + } + ], + "correct_answer": "A", + "explanation": "select was mapped to resolveSelect (waits for the selector's resolver to finish fetching async data before returning). syncSelect was mapped to the synchronous select that returns the current state value immediately, even if a resolver hasn't completed.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data-controls/" + ] + }, + { + "id": "k-datacontrols-006", + "category": "gb-data-controls", + "subcategory": "migration", + "difficulty": "hard", + "prompt": "What modern @wordpress/data pattern largely replaces generator-based controls for async action creators?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Thunks — action creators that return async functions receiving { dispatch, select, resolveSelect, registry }" + }, + { + "key": "B", + "text": "Sagas — long-running background processes that watch for actions" + }, + { + "key": "C", + "text": "Middleware — custom Redux middleware for each async operation" + }, + { + "key": "D", + "text": "Observables — RxJS-based reactive action streams" + } + ], + "correct_answer": "A", + "explanation": "Thunks are the modern replacement. Instead of function* generators yielding control descriptors, action creators return async functions that receive { dispatch, select, resolveSelect, registry }. Thunks support standard async/await syntax and don't require control registration.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-datacontrols-007", + "category": "gb-data-controls", + "subcategory": "controls", + "difficulty": "hard", + "prompt": "What does the __unstableAwaitPromise control in @wordpress/data-controls do?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Yields to await any arbitrary Promise in a generator-based action creator" + }, + { + "key": "B", + "text": "Blocks the Redux store until a Promise resolves" + }, + { + "key": "C", + "text": "Converts a callback-based API to a Promise" + }, + { + "key": "D", + "text": "Cancels a pending Promise when a new action is dispatched" + } + ], + "correct_answer": "A", + "explanation": "__unstableAwaitPromise(promise) creates a control descriptor with type 'AWAIT_PROMISE'. When yielded in a generator action, the middleware resolves the Promise and returns its value to the generator. The __unstable prefix indicates the API may change.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data-controls/" + ] + }, + { + "id": "k-datacontrols-008", + "category": "gb-data-controls", + "subcategory": "usage", + "difficulty": "intermediate", + "prompt": "How are @wordpress/data-controls handlers registered with a store?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Pass the exported controls object as the controls property in createReduxStore options" + }, + { + "key": "B", + "text": "Call registerControls() after store creation" + }, + { + "key": "C", + "text": "Import them and they auto-register globally" + }, + { + "key": "D", + "text": "Add them as middleware via applyMiddleware()" + } + ], + "correct_answer": "A", + "explanation": "The package exports a controls object (its default export) containing handler functions keyed by control type. This object is passed as the 'controls' property when creating a store: createReduxStore('my-store', { reducer, controls, actions }).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data-controls/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-data.json b/datasets/suites/wp-core-v1/knowledge/gb-data.json new file mode 100644 index 0000000..22abf5a --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-data.json @@ -0,0 +1,341 @@ +{ + "id": "wp-core-knowledge-v1-wp_data", + "version": "1.0.0", + "metadata": { + "name": "WordPress Core Knowledge - @wordpress/data", + "description": "Tests knowledge of the @wordpress/data state management library", + "wp_version": "6.9", + "created_at": "2026-02-24" + }, + "tests": [ + { + "id": "k-wpdata-001", + "category": "gb-data", + "subcategory": "store-creation", + "difficulty": "basic", + "prompt": "What function creates a store descriptor in the modern @wordpress/data API?", + "type": "short_answer", + "correct_answer": "createReduxStore", + "answer_type": "contains", + "explanation": "createReduxStore(key, options) creates a StoreDescriptor with a namespace key and configuration (reducer, actions, selectors, resolvers). The descriptor is then passed to register(). The older registerStore() is deprecated.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-002", + "category": "gb-data", + "subcategory": "hooks", + "difficulty": "basic", + "prompt": "What React hook reads data from a @wordpress/data store?", + "type": "short_answer", + "correct_answer": "useSelect", + "answer_type": "contains", + "explanation": "useSelect(mapSelect, deps) subscribes a component to store data. It accepts a mapping function receiving (select, registry), or a StoreDescriptor for static mode. It uses React 18's useSyncExternalStore internally.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-003", + "category": "gb-data", + "subcategory": "hooks", + "difficulty": "basic", + "prompt": "What React hook dispatches actions to a @wordpress/data store?", + "type": "short_answer", + "correct_answer": "useDispatch", + "answer_type": "contains", + "explanation": "useDispatch(storeNameOrDescriptor) returns the action creators for a store. If called without arguments, it returns the registry's dispatch function. It must be used within a RegistryProvider context.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-004", + "category": "gb-data", + "subcategory": "store-creation", + "difficulty": "basic", + "prompt": "Which state management library does @wordpress/data use internally?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Redux" + }, + { + "key": "B", + "text": "MobX" + }, + { + "key": "C", + "text": "Zustand" + }, + { + "key": "D", + "text": "A custom reactive system with no external dependency" + } + ], + "correct_answer": "A", + "explanation": "@wordpress/data uses Redux (version 5) as its underlying state container. It adds WordPress-specific features on top: resolvers for async data fetching, registry for multi-store management, controls/thunks for side effects, and persistence plugins.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-005", + "category": "gb-data", + "subcategory": "selectors", + "difficulty": "intermediate", + "prompt": "What does resolveSelect() do differently from select() in @wordpress/data?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Returns promise-wrapped selectors that wait for resolvers to complete before returning values" + }, + { + "key": "B", + "text": "Validates selector arguments against a schema before calling" + }, + { + "key": "C", + "text": "Caches selector results in localStorage for persistence" + }, + { + "key": "D", + "text": "Runs selectors on a Web Worker for better performance" + } + ], + "correct_answer": "A", + "explanation": "select() returns the current state value synchronously. resolveSelect() returns Promise-wrapped selectors that wait for the selector's resolver to finish fetching data before returning. This is useful when you need to ensure async data is loaded.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-006", + "category": "gb-data", + "subcategory": "thunks", + "difficulty": "intermediate", + "prompt": "What properties does the argument object passed to a thunk action creator contain?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "registry, dispatch, select, resolveSelect" + }, + { + "key": "B", + "text": "state, dispatch, getState" + }, + { + "key": "C", + "text": "store, actions, selectors" + }, + { + "key": "D", + "text": "context, next, action" + } + ], + "correct_answer": "A", + "explanation": "Thunk action creators receive an object with: registry (full registry instance), dispatch (bound action creators for the store), select (bound selectors), and resolveSelect (promise-wrapped selectors). The dispatch, select, and resolveSelect properties are lazy-evaluated getters.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-007", + "category": "gb-data", + "subcategory": "registry", + "difficulty": "intermediate", + "prompt": "What does the batch() function in @wordpress/data do?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Groups multiple store updates and notifies listeners only once at the end" + }, + { + "key": "B", + "text": "Sends multiple REST API requests in a single HTTP call" + }, + { + "key": "C", + "text": "Queues actions to run sequentially in a Web Worker" + }, + { + "key": "D", + "text": "Defers all dispatches until the next animation frame" + } + ], + "correct_answer": "A", + "explanation": "batch(callback) pauses all store emitters during the callback, preventing selector recomputation and component re-renders. Once the callback completes, listeners are notified once. This enables atomic operations across multiple stores.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-008", + "category": "gb-data", + "subcategory": "selectors", + "difficulty": "intermediate", + "prompt": "What function creates a selector that can access other stores via the registry?", + "type": "short_answer", + "correct_answer": "createRegistrySelector", + "answer_type": "contains", + "explanation": "createRegistrySelector((select) => (state, ...args) => result) creates a selector with cross-store access. The returned selector is marked with isRegistrySelector: true and its select binding is cached per registry using WeakMap.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-009", + "category": "gb-data", + "subcategory": "persistence", + "difficulty": "intermediate", + "prompt": "What is the default localStorage key used by the @wordpress/data persistence plugin?", + "type": "short_answer", + "correct_answer": "WP_DATA", + "answer_type": "exact", + "explanation": "The persistence plugin stores data under the 'WP_DATA' key by default. This can be changed via the storageKey option when registering the plugin with registry.use(persistencePlugin, { storageKey }).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-010", + "category": "gb-data", + "subcategory": "store-creation", + "difficulty": "intermediate", + "prompt": "Which property is the only required field in a createReduxStore configuration object?", + "type": "short_answer", + "correct_answer": "reducer", + "answer_type": "contains", + "explanation": "The reducer function is the only required property. Optional properties include actions, selectors, resolvers, controls (deprecated), and initialState. The reducer accepts (state, action) and returns the new state.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-011", + "category": "gb-data", + "subcategory": "resolvers", + "difficulty": "hard", + "prompt": "What are the three possible resolution status values for a resolver in @wordpress/data?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "resolving, finished, error" + }, + { + "key": "B", + "text": "pending, fulfilled, rejected" + }, + { + "key": "C", + "text": "loading, success, failure" + }, + { + "key": "D", + "text": "idle, running, complete" + } + ], + "correct_answer": "A", + "explanation": "The resolution metadata reducer tracks three status values: 'resolving' (resolver started), 'finished' (resolver completed), and 'error' (resolver failed with an error). These are checked via selectors like hasStartedResolution, hasFinishedResolution, and hasResolutionFailed.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-012", + "category": "gb-data", + "subcategory": "hooks", + "difficulty": "hard", + "prompt": "What React hook in @wordpress/data supports React Suspense by throwing promises for unresolved selectors?", + "type": "short_answer", + "correct_answer": "useSuspenseSelect", + "answer_type": "contains", + "explanation": "useSuspenseSelect(mapSelect, deps) works like useSelect but uses registry.suspendSelect internally. If a selector's resolver hasn't completed, it throws a Promise, which React Suspense catches to show a fallback UI.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-013", + "category": "gb-data", + "subcategory": "middleware", + "difficulty": "hard", + "prompt": "What is the correct order of middleware in the @wordpress/data redux store stack?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "resolvers-cache, promise, redux-routine (generators), thunk" + }, + { + "key": "B", + "text": "thunk, promise, resolvers-cache, redux-routine" + }, + { + "key": "C", + "text": "promise, thunk, resolvers-cache, redux-routine" + }, + { + "key": "D", + "text": "redux-routine, promise, thunk, resolvers-cache" + } + ], + "correct_answer": "A", + "explanation": "The middleware stack is applied in this order: (1) resolvers-cache middleware handles resolver invalidation via shouldInvalidate, (2) promise middleware handles Promise-valued actions, (3) redux-routine middleware handles generator-based controls, (4) thunk middleware handles function-based action creators.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-014", + "category": "gb-data", + "subcategory": "components", + "difficulty": "hard", + "prompt": "What does the AsyncModeProvider component do in @wordpress/data?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Makes useSelect component updates asynchronous, deferring re-renders until the browser is idle" + }, + { + "key": "B", + "text": "Enables server-side rendering of data stores" + }, + { + "key": "C", + "text": "Switches all API requests to use async/await instead of generators" + }, + { + "key": "D", + "text": "Wraps the store in a React concurrent mode boundary" + } + ], + "correct_answer": "A", + "explanation": "AsyncModeProvider is a React context provider that, when set to true, makes useSelect updates asynchronous using @wordpress/priority-queue. Re-renders are deferred until the browser becomes idle, improving performance for components that don't need immediate updates.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + }, + { + "id": "k-wpdata-015", + "category": "gb-data", + "subcategory": "resolvers", + "difficulty": "hard", + "prompt": "What action invalidates the cached resolution state for a specific selector call in @wordpress/data?", + "type": "short_answer", + "correct_answer": "invalidateResolution", + "answer_type": "contains", + "explanation": "invalidateResolution(selectorName, args) clears the cached resolution for a specific selector and arguments, forcing the resolver to re-run on next access. Related actions include invalidateResolutionForStore() (clears all) and invalidateResolutionForStoreSelector(selectorName) (clears all for one selector).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-dataviews.json b/datasets/suites/wp-core-v1/knowledge/gb-dataviews.json new file mode 100644 index 0000000..20ba715 --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-dataviews.json @@ -0,0 +1,409 @@ +{ + "id": "wp-core-knowledge-v1-dataviews", + "version": "1.3.0", + "metadata": { + "name": "WordPress Core Knowledge - DataViews", + "description": "Tests knowledge of WordPress DataViews API", + "wp_version": "6.9", + "created_at": "2025-12-16" + }, + "tests": [ + { + "id": "k-dataviews-001", + "category": "gb-dataviews", + "subcategory": "architecture", + "difficulty": "basic", + "prompt": "The DataViews component in WordPress is implemented primarily as what?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "A JavaScript/React component (@wordpress/dataviews) with no dedicated PHP backend API" + }, + { + "key": "B", + "text": "A PHP class with REST API endpoints" + }, + { + "key": "C", + "text": "A WordPress admin screen template" + }, + { + "key": "D", + "text": "A block type registered via block.json" + } + ], + "correct_answer": "A", + "explanation": "DataViews is purely a JavaScript/React component in the @wordpress/dataviews package. There are no PHP classes, functions, or dedicated REST endpoints for it. The PHP side provides standard REST API endpoints (posts, templates, etc.) that the JS component consumes.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-002", + "category": "gb-dataviews", + "subcategory": "exports", + "difficulty": "basic", + "prompt": "What are the three main components exported by the @wordpress/dataviews package?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "DataViews, DataViewsPicker, and DataForm" + }, + { + "key": "B", + "text": "DataTable, DataGrid, and DataList" + }, + { + "key": "C", + "text": "DataViews, DataEditor, and DataFilter" + }, + { + "key": "D", + "text": "ViewTable, ViewGrid, and ViewList" + } + ], + "correct_answer": "A", + "explanation": "The @wordpress/dataviews package exports three main components: DataViews (the primary data display component), DataViewsPicker (a picker variant for item selection), and DataForm (for editing data records). It also exports the filterSortAndPaginate utility function.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-003", + "category": "gb-dataviews", + "subcategory": "layouts", + "difficulty": "basic", + "prompt": "What are the three primary layout types for the DataViews component?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "table, grid, list" + }, + { + "key": "B", + "text": "table, card, detail" + }, + { + "key": "C", + "text": "list, tree, kanban" + }, + { + "key": "D", + "text": "rows, columns, tiles" + } + ], + "correct_answer": "A", + "explanation": "DataViews supports table, grid, and list as its three primary layout types. Additional layouts include 'activity' (for timeline-style views) and picker variants (pickerGrid, pickerTable) for item selection UIs. The layout constants are LAYOUT_TABLE, LAYOUT_GRID, and LAYOUT_LIST.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-004", + "category": "gb-dataviews", + "subcategory": "utilities", + "difficulty": "intermediate", + "prompt": "What utility function does @wordpress/dataviews export for client-side filtering, sorting, and pagination of data?", + "type": "short_answer", + "correct_answer": "filterSortAndPaginate", + "answer_type": "contains", + "explanation": "filterSortAndPaginate is a utility function exported by @wordpress/dataviews that applies the current view's filters, sorting, and pagination to a data array on the client side, returning the processed subset of items.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-005", + "category": "gb-dataviews", + "subcategory": "fields", + "difficulty": "intermediate", + "prompt": "Which property on a DataViews field definition makes it searchable via the global search bar?", + "type": "short_answer", + "correct_answer": "enableGlobalSearch", + "answer_type": "contains", + "explanation": "Setting enableGlobalSearch to true on a field definition includes that field in global search queries. The default is false, so fields must explicitly opt in to be searched.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-006", + "category": "gb-dataviews", + "subcategory": "layouts", + "difficulty": "intermediate", + "prompt": "What are the three density options available for the DataViews table layout?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "compact, balanced, comfortable" + }, + { + "key": "B", + "text": "small, medium, large" + }, + { + "key": "C", + "text": "tight, normal, loose" + }, + { + "key": "D", + "text": "dense, default, spacious" + } + ], + "correct_answer": "A", + "explanation": "Table, list, and activity layouts support a density property with three values: 'compact', 'balanced', and 'comfortable', controlling the vertical spacing between rows.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-007", + "category": "gb-dataviews", + "subcategory": "fields", + "difficulty": "intermediate", + "prompt": "What property on a DataViews field definition specifies validation rules like required, pattern, minLength, and maxLength?", + "type": "short_answer", + "correct_answer": "isValid", + "answer_type": "contains", + "explanation": "The isValid property on a field definition accepts a rules object with properties like required, elements, pattern (regex), minLength, maxLength, min, max, and custom (a function returning null or an error string). These rules are enforced when editing items via DataForm.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-locked-001", + "category": "gb-dataviews", + "subcategory": "filters", + "difficulty": "intermediate", + "prompt": "In WordPress 6.9 DataViews, how do you make a filter visible but non-editable by users?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Set editable:false on the view" + }, + { + "key": "B", + "text": "Use isLocked:true on the filter definition" + }, + { + "key": "C", + "text": "Omit the operator field" + }, + { + "key": "D", + "text": "Register the filter server-side only" + } + ], + "correct_answer": "B", + "explanation": "Locked filters are defined with isLocked:true, keeping them applied while preventing user edits or removal.", + "references": [ + "https://make.wordpress.org/core/2025/11/11/dataviews-dataform-et-al-in-wordpress-6-9/" + ] + }, + { + "id": "k-dataviews-008", + "category": "gb-dataviews", + "subcategory": "filters", + "difficulty": "hard", + "prompt": "Which DataViews filter operator matches items that contain ALL of the specified values?", + "type": "short_answer", + "correct_answer": "isAll", + "answer_type": "exact", + "explanation": "DataViews defines set-based filter operators: 'is' (exact single match), 'isNot' (not equal), 'isAny' (includes some), 'isNone' (excludes all), 'isAll' (includes all specified), and 'isNotAll' (excludes all specified). Additional operators include comparison types like 'between', 'lessThan', 'greaterThan', 'contains', 'startsWith', and date-specific operators.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-009", + "category": "gb-dataviews", + "subcategory": "dataform", + "difficulty": "hard", + "prompt": "Which of these is a valid set of layout types for the DataForm component?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "regular, panel, card, row, details" + }, + { + "key": "B", + "text": "form, modal, inline, sidebar, popover" + }, + { + "key": "C", + "text": "simple, tabbed, accordion, grid, list" + }, + { + "key": "D", + "text": "basic, advanced, compact, full, custom" + } + ], + "correct_answer": "A", + "explanation": "DataForm supports five layout types: 'regular' (standard stacked form), 'panel' (collapsible panels with dropdown/modal options), 'card' (card containers with headers), 'row' (horizontal layout with flex alignment), and 'details' (HTML details/summary style).", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-010", + "category": "gb-dataviews", + "subcategory": "actions", + "difficulty": "hard", + "prompt": "What distinguishes an ActionButton from an ActionModal in the DataViews actions API?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "ActionButton provides a callback function; ActionModal provides a RenderModal component" + }, + { + "key": "B", + "text": "ActionButton is for primary actions; ActionModal is for secondary actions" + }, + { + "key": "C", + "text": "ActionButton runs synchronously; ActionModal runs asynchronously" + }, + { + "key": "D", + "text": "ActionButton operates on single items; ActionModal operates on bulk selections" + } + ], + "correct_answer": "A", + "explanation": "ActionButton defines a 'callback' function that executes immediately when triggered. ActionModal defines a 'RenderModal' React component that renders inside a dialog, with optional modalHeader, modalSize (small/medium/large/fill), and hideModalHeader properties.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-groupby-001", + "category": "gb-dataviews", + "subcategory": "layout", + "difficulty": "hard", + "prompt": "Which view option groups items by a field in the DataViews API?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "group" + }, + { + "key": "B", + "text": "groupBy" + }, + { + "key": "C", + "text": "aggregateBy" + }, + { + "key": "D", + "text": "clusterField" + } + ], + "correct_answer": "B", + "explanation": "The groupBy property on a view definition accepts an object with field (the field ID to group by), direction ('asc' or 'desc'), and an optional showLabel boolean. Table, grid, and list layouts all support grouping.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + }, + { + "id": "k-dataviews-readonly-001", + "category": "gb-dataviews", + "subcategory": "fields", + "difficulty": "hard", + "prompt": "Which property marks a DataViews field as display-only in WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "locked" + }, + { + "key": "B", + "text": "readOnly" + }, + { + "key": "C", + "text": "editable:false" + }, + { + "key": "D", + "text": "viewOnly" + } + ], + "correct_answer": "B", + "explanation": "DataViews fields accept readOnly:true to render a non-editable value while still showing in the UI. The default is false.", + "references": [ + "https://make.wordpress.org/core/2025/11/11/dataviews-dataform-et-al-in-wordpress-6-9/" + ] + }, + { + "id": "k-dataviews-getelements-001", + "category": "gb-dataviews", + "subcategory": "data-loading", + "difficulty": "hard", + "prompt": "What does the getElements() hook on a DataViews field do?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Groups table rows by a field" + }, + { + "key": "B", + "text": "Fetches filter option values lazily on demand" + }, + { + "key": "C", + "text": "Locks filters from user edits" + }, + { + "key": "D", + "text": "Persists user layouts to theme.json" + } + ], + "correct_answer": "B", + "explanation": "getElements returns a Promise of options asynchronously when a user opens a filter/edit control, avoiding upfront fetches. It serves the same purpose as the static 'elements' array but loads data on demand.", + "references": [ + "https://make.wordpress.org/core/2025/11/11/dataviews-dataform-et-al-in-wordpress-6-9/" + ] + }, + { + "id": "k-dataviews-011", + "category": "gb-dataviews", + "subcategory": "fields", + "difficulty": "intermediate", + "prompt": "What field properties control whether a DataViews column can be hidden or sorted by the user?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "enableHiding and enableSorting" + }, + { + "key": "B", + "text": "hideable and sortable" + }, + { + "key": "C", + "text": "canHide and canSort" + }, + { + "key": "D", + "text": "visible and orderable" + } + ], + "correct_answer": "A", + "explanation": "enableHiding (default: true) controls whether users can toggle the field's visibility. enableSorting (default: true) controls whether users can sort by that field. Both default to true, so fields are hideable and sortable unless explicitly disabled.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-font-library.json b/datasets/suites/wp-core-v1/knowledge/gb-font-library.json new file mode 100644 index 0000000..89ed105 --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-font-library.json @@ -0,0 +1,209 @@ +{ + "id": "wp-core-knowledge-v1-font_library", + "version": "1.2.0", + "metadata": { + "name": "WordPress Core Knowledge - Font Library", + "description": "Tests knowledge of WordPress Font Library API", + "wp_version": "6.9", + "created_at": "2025-12-16" + }, + "tests": [ + { + "id": "k-font-001", + "category": "gb-font-library", + "subcategory": "post-types", + "difficulty": "basic", + "prompt": "What are the two post types used by the WordPress Font Library?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "wp_font_family and wp_font_face" + }, + { + "key": "B", + "text": "wp_font and wp_font_style" + }, + { + "key": "C", + "text": "wp_typography and wp_font_variant" + }, + { + "key": "D", + "text": "font_family and font_face" + } + ], + "correct_answer": "A", + "explanation": "The Font Library uses two custom post types: wp_font_family (representing a font family) and wp_font_face (representing individual font faces within a family). Both require edit_theme_options capability.", + "references": [ + "https://developer.wordpress.org/reference/functions/create_initial_post_types/" + ] + }, + { + "id": "k-font-002", + "category": "gb-font-library", + "subcategory": "storage", + "difficulty": "basic", + "prompt": "In which directory are uploaded fonts stored by default in WordPress?", + "type": "short_answer", + "correct_answer": "uploads/fonts", + "answer_type": "contains", + "explanation": "Uploaded fonts are stored in wp-content/uploads/fonts/. The wp_font_dir() function returns this path, computed by appending '/fonts' to the base upload directory. The location is filterable via the font_dir filter.", + "references": [ + "https://developer.wordpress.org/reference/functions/wp_font_dir/" + ] + }, + { + "id": "k-font-003", + "category": "gb-font-library", + "subcategory": "css-generation", + "difficulty": "intermediate", + "prompt": "What is the default font-display value used by WordPress's WP_Font_Face class?", + "type": "short_answer", + "correct_answer": "fallback", + "answer_type": "exact", + "explanation": "WP_Font_Face defaults font-display to 'fallback', not 'swap'. This means the browser uses a short block period followed by a swap period, providing a balance between avoiding invisible text and layout shifts.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_font_face/" + ] + }, + { + "id": "k-font-004", + "category": "gb-font-library", + "subcategory": "collections", + "difficulty": "intermediate", + "prompt": "What function registers a font collection with the Font Library?", + "type": "short_answer", + "correct_answer": "wp_register_font_collection", + "answer_type": "contains", + "explanation": "wp_register_font_collection() accepts a slug and an args array (with required name and font_families keys). It delegates to WP_Font_Library::get_instance()->register_font_collection().", + "references": [ + "https://developer.wordpress.org/reference/functions/wp_register_font_collection/" + ] + }, + { + "id": "k-font-005", + "category": "gb-font-library", + "subcategory": "rest-api", + "difficulty": "intermediate", + "prompt": "The Font Library's font face REST routes have a unique nested structure. What is the REST base for font faces?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "font-families/(?P[\\d]+)/font-faces" + }, + { + "key": "B", + "text": "font-faces" + }, + { + "key": "C", + "text": "fonts/faces" + }, + { + "key": "D", + "text": "typography/font-faces" + } + ], + "correct_answer": "A", + "explanation": "Font face routes are nested under their parent font family: font-families/{font_family_id}/font-faces. This is one of the few REST API endpoints in WordPress that uses a nested parent-child route structure.", + "references": [ + "https://developer.wordpress.org/rest-api/reference/font-faces/" + ] + }, + { + "id": "k-font-006", + "category": "gb-font-library", + "subcategory": "collections", + "difficulty": "hard", + "prompt": "When a WP_Font_Collection loads font data from a remote URL, how is the fetched JSON cached?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Site transient with DAY_IN_SECONDS TTL" + }, + { + "key": "B", + "text": "Object cache with no expiration" + }, + { + "key": "C", + "text": "File-based cache in wp-content/cache/" + }, + { + "key": "D", + "text": "Browser localStorage via REST response headers" + } + ], + "correct_answer": "A", + "explanation": "WP_Font_Collection uses set_site_transient() with DAY_IN_SECONDS TTL to cache remote JSON data. The transient key is limited to 167 characters to fit the database column constraint.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_font_collection/" + ] + }, + { + "id": "k-font-007", + "category": "gb-font-library", + "subcategory": "css-generation", + "difficulty": "hard", + "prompt": "Which of these CSS properties is accepted by WP_Font_Face for @font-face declarations?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "size-adjust" + }, + { + "key": "B", + "text": "font-kerning" + }, + { + "key": "C", + "text": "letter-spacing" + }, + { + "key": "D", + "text": "text-rendering" + } + ], + "correct_answer": "A", + "explanation": "WP_Font_Face validates a specific set of @font-face CSS properties including size-adjust, ascent-override, descent-override, font-display, font-family, font-stretch, font-style, font-weight, font-variant, font-feature-settings, font-variation-settings, line-gap-override, src, and unicode-range. Properties like font-kerning, letter-spacing, and text-rendering are not valid @font-face properties.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_font_face/" + ] + }, + { + "id": "k-font-008", + "category": "gb-font-library", + "subcategory": "ui", + "difficulty": "hard", + "prompt": "What is the supported way to hide the Font Library UI across editors in WordPress 6.9?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Remove the Fonts menu via remove_menu_page()" + }, + { + "key": "B", + "text": "Set fontLibraryEnabled=false in the block_editor_settings_all filter" + }, + { + "key": "C", + "text": "Define DISABLE_FONT_LIBRARY in wp-config.php" + }, + { + "key": "D", + "text": "Unregister wp_font_family post type" + } + ], + "correct_answer": "B", + "explanation": "Filtering block_editor_settings_all and setting fontLibraryEnabled to false disables the Font Library UI globally without unregistering post types.", + "references": [ + "https://developer.wordpress.org/news/snippets/how-to-disable-the-font-library/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gb-interactivity-api.json b/datasets/suites/wp-core-v1/knowledge/gb-interactivity-api.json new file mode 100644 index 0000000..e5c677e --- /dev/null +++ b/datasets/suites/wp-core-v1/knowledge/gb-interactivity-api.json @@ -0,0 +1,392 @@ +{ + "id": "wp-core-knowledge-v1-interactivity_api", + "version": "1.3.0", + "metadata": { + "name": "WordPress Core Knowledge - Interactivity API", + "description": "Tests knowledge of WordPress Interactivity API", + "wp_version": "6.9", + "created_at": "2025-12-16" + }, + "tests": [ + { + "id": "k-interactivity-001", + "category": "gb-interactivity-api", + "subcategory": "directives", + "difficulty": "basic", + "prompt": "What HTML attribute marks an element as part of an interactive region in the WordPress Interactivity API?", + "type": "short_answer", + "correct_answer": "data-wp-interactive", + "answer_type": "contains", + "explanation": "The data-wp-interactive attribute is the entry point for the Interactivity API. It specifies the namespace for the interactive region (e.g., data-wp-interactive=\"core/image\").", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-002", + "category": "gb-interactivity-api", + "subcategory": "runtime", + "difficulty": "basic", + "prompt": "What is the script module ID for the WordPress Interactivity API runtime?", + "type": "short_answer", + "correct_answer": "@wordpress/interactivity", + "answer_type": "contains", + "explanation": "The Interactivity API runtime is registered as the @wordpress/interactivity script module. It is loaded with fetchpriority 'low' and in_footer true. A companion module @wordpress/interactivity-router provides client-side navigation.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-003", + "category": "gb-interactivity-api", + "subcategory": "php-api", + "difficulty": "basic", + "prompt": "What PHP function returns the WP_Interactivity_API singleton instance?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "wp_interactivity()" + }, + { + "key": "B", + "text": "WP_Interactivity_API::get_instance()" + }, + { + "key": "C", + "text": "get_interactivity_api()" + }, + { + "key": "D", + "text": "wp_get_interactivity()" + } + ], + "correct_answer": "A", + "explanation": "wp_interactivity() returns the WP_Interactivity_API singleton. Unlike most WordPress registries, it uses a global variable rather than a traditional get_instance() method.", + "references": [ + "https://developer.wordpress.org/reference/functions/wp_interactivity/" + ] + }, + { + "id": "k-interactivity-004", + "category": "gb-interactivity-api", + "subcategory": "directives", + "difficulty": "intermediate", + "prompt": "What is the purpose of processing Interactivity API directives on the server side in PHP?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "To provide server-rendered initial HTML so interactive regions display correct content before JavaScript loads" + }, + { + "key": "B", + "text": "To validate directive syntax and catch errors before sending HTML to the client" + }, + { + "key": "C", + "text": "To compile directives into optimized JavaScript bundles" + }, + { + "key": "D", + "text": "To generate CSS stylesheets from directive values" + } + ], + "correct_answer": "A", + "explanation": "Server-side directive processing (for directives like data-wp-bind, data-wp-text, data-wp-class, data-wp-style, data-wp-context, and data-wp-each) ensures the initial HTML sent to the browser already reflects the correct state. This avoids a flash of incorrect content while waiting for JavaScript to hydrate the page.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-005", + "category": "gb-interactivity-api", + "subcategory": "directives", + "difficulty": "intermediate", + "prompt": "Which of these Interactivity API directives is processed server-side in PHP?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "data-wp-on" + }, + { + "key": "B", + "text": "data-wp-init" + }, + { + "key": "C", + "text": "data-wp-text" + }, + { + "key": "D", + "text": "data-wp-watch" + } + ], + "correct_answer": "C", + "explanation": "data-wp-text is one of the 8 server-side directives. data-wp-on, data-wp-init, data-wp-watch, data-wp-run, and data-wp-key are client-side only directives that have no server-side processing.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-006", + "category": "gb-interactivity-api", + "subcategory": "namespaces", + "difficulty": "intermediate", + "prompt": "What separator character(s) are used in Interactivity API directive values to specify a namespace?", + "type": "short_answer", + "correct_answer": "::", + "answer_type": "exact", + "explanation": "The double colon '::' separates the namespace from the reference in directive values. For example: data-wp-text=\"myPlugin::state.title\" references the 'title' state in the 'myPlugin' namespace.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-007", + "category": "gb-interactivity-api", + "subcategory": "directives", + "difficulty": "intermediate", + "prompt": "In the data-wp-interactive attribute, a namespace can be specified in two formats. What are they?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "A plain string or a JSON object with a namespace key" + }, + { + "key": "B", + "text": "A data attribute or a CSS class" + }, + { + "key": "C", + "text": "A URL hash or query parameter" + }, + { + "key": "D", + "text": "An HTML comment or meta tag" + } + ], + "correct_answer": "A", + "explanation": "data-wp-interactive accepts either a plain string namespace (e.g., data-wp-interactive=\"myPlugin\") or a JSON object with a namespace key (e.g., data-wp-interactive='{\"namespace\":\"myPlugin\"}'). Core blocks use the simple string form.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-008", + "category": "gb-interactivity-api", + "subcategory": "rendering", + "difficulty": "hard", + "prompt": "Which HTML elements are completely skipped during server-side Interactivity API directive processing?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "SVG and MATH elements" + }, + { + "key": "B", + "text": "Only IFRAME elements" + }, + { + "key": "C", + "text": "TABLE and FORM elements" + }, + { + "key": "D", + "text": "No elements are skipped" + } + ], + "correct_answer": "A", + "explanation": "SVG and MATH elements and their children are completely skipped during server-side directive processing. Additionally, SCRIPT, IFRAME, NOEMBED, NOFRAMES, STYLE, TEXTAREA, TITLE, and XMP tags don't visit their closers.", + "references": [ + "https://developer.wordpress.org/reference/classes/wp_interactivity_api/" + ] + }, + { + "id": "k-interactivity-009", + "category": "gb-interactivity-api", + "subcategory": "state", + "difficulty": "hard", + "prompt": "State values in the Interactivity API can be PHP Closures. What are these called and how are they handled?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Derived state -- closures are called during server evaluation and tracked for client serialization" + }, + { + "key": "B", + "text": "Lazy state -- closures are serialized and executed on the client" + }, + { + "key": "C", + "text": "Computed state -- closures are memoized and cached in the object cache" + }, + { + "key": "D", + "text": "Reactive state -- closures trigger re-renders when dependencies change" + } + ], + "correct_answer": "A", + "explanation": "PHP Closures in Interactivity API state are 'derived state'. When encountered during evaluate(), they are called and their result is used. Since 6.9, accessed derived state closures are tracked in $derived_state_closures and serialized to the client as derivedStateClosures.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-010", + "category": "gb-interactivity-api", + "subcategory": "block-support", + "difficulty": "hard", + "prompt": "In block.json, the interactivity support can be set to true or to an object. What are the two properties in the object form?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "interactive and clientNavigation" + }, + { + "key": "B", + "text": "enabled and router" + }, + { + "key": "C", + "text": "serverRendered and clientHydrated" + }, + { + "key": "D", + "text": "namespace and directives" + } + ], + "correct_answer": "A", + "explanation": "The object form is { \"interactive\": true, \"clientNavigation\": true }. Setting interactivity to boolean true is shorthand for both properties being true. The interactive property marks the block as an interactive root; clientNavigation marks it as compatible with SPA navigation.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/" + ] + }, + { + "id": "k-interactivity-attach-001", + "category": "gb-interactivity-api", + "subcategory": "router", + "difficulty": "hard", + "prompt": "In WordPress 6.9, what does the Interactivity API router option attachTo solve?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Persisting server state across page loads" + }, + { + "key": "B", + "text": "Choosing the DOM parent where a router region renders to avoid collisions" + }, + { + "key": "C", + "text": "Deferring hydration until window.onload" + }, + { + "key": "D", + "text": "Forcing interactivity scripts into the footer" + } + ], + "correct_answer": "B", + "explanation": "attachTo is a selector telling the router which parent node to render into, preventing conflicting renders when multiple router regions share a page.", + "references": [ + "https://make.wordpress.org/core/2025/11/25/wordpress-6-9-field-guide/" + ] + }, + { + "id": "k-interactivity-module-001", + "category": "gb-interactivity-api", + "subcategory": "client-navigation", + "difficulty": "hard", + "prompt": "When manually registering a script module for the Interactivity API in WordPress 6.9, how do you mark it as compatible with client navigation so it loads on SPA transitions?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "Set supports.interactivity=true in block.json" + }, + { + "key": "B", + "text": "Pass 'client_navigation' => true to wp_register_script_module()" + }, + { + "key": "C", + "text": "Call wp_interactivity()->add_client_navigation_support_to_script_module( 'module-id' )" + }, + { + "key": "D", + "text": "Prefix the handle with 'interactivity-'" + } + ], + "correct_answer": "C", + "explanation": "Manually enqueued script modules must be flagged with add_client_navigation_support_to_script_module so router navigation imports them during SPA transitions.", + "references": [ + "https://make.wordpress.org/core/2025/11/12/interactivity-apis-client-navigation-improvements-in-wordpress-6-9/" + ] + }, + { + "id": "k-interactivity-011", + "category": "gb-interactivity-api", + "subcategory": "client-api", + "difficulty": "basic", + "prompt": "What are the three main functions exported by the @wordpress/interactivity package for use in client-side store definitions?", + "type": "multiple_choice", + "choices": [ + { + "key": "A", + "text": "store, getContext, getElement" + }, + { + "key": "B", + "text": "init, watch, render" + }, + { + "key": "C", + "text": "createStore, useContext, useRef" + }, + { + "key": "D", + "text": "reactive, computed, effect" + } + ], + "correct_answer": "A", + "explanation": "The @wordpress/interactivity module exports store() for creating/managing reactive stores with state and actions, getContext() for retrieving an element's inherited context from data-wp-context, and getElement() for accessing a read-only representation of the current DOM element including its ref and attributes. Additional exports include getServerState() and getConfig().", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-012", + "category": "gb-interactivity-api", + "subcategory": "directives", + "difficulty": "intermediate", + "prompt": "What directive prevents the Interactivity API from processing directives on an element's children?", + "type": "short_answer", + "correct_answer": "data-wp-ignore", + "answer_type": "contains", + "explanation": "The data-wp-ignore directive tells the Interactivity API runtime to skip directive processing on the element's subtree. This is useful for portions of the DOM that should remain static or are managed by other libraries.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + }, + { + "id": "k-interactivity-013", + "category": "gb-interactivity-api", + "subcategory": "router", + "difficulty": "hard", + "prompt": "What is the default timeout in milliseconds for the Interactivity API router's navigate() function?", + "type": "short_answer", + "correct_answer": "10000", + "answer_type": "exact", + "explanation": "The @wordpress/interactivity-router's navigate() function defaults to a 10000ms (10 second) timeout. It also accepts options for force (re-fetch), replace (history entry), html (use provided HTML), loadingAnimation, and screenReaderAnnouncement.", + "references": [ + "https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/" + ] + } + ] +} diff --git a/datasets/suites/wp-core-v1/knowledge/gotchas.json b/datasets/suites/wp-core-v1/knowledge/gotchas.json index 71062ff..2d0f163 100644 --- a/datasets/suites/wp-core-v1/knowledge/gotchas.json +++ b/datasets/suites/wp-core-v1/knowledge/gotchas.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-gotchas", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Gotchas", "description": "Tests knowledge of common WordPress pitfalls and edge cases", @@ -9,8 +9,9 @@ }, "tests": [ { - "id": "k-meta-gotcha-001", + "id": "k-gotchas-001", "category": "gotchas", + "subcategory": "meta", "difficulty": "hard", "prompt": "What does get_post_meta($post_id, 'missing_key', true) return when the meta key does not exist?", "type": "multiple_choice", @@ -21,8 +22,185 @@ { "key": "D", "text": "WP_Error" } ], "correct_answer": "C", - "explanation": "With $single=true and no row present, get_post_meta returns an empty string—easy to misread as a real value with loose checks.", + "explanation": "With $single=true and no row present, get_post_meta returns an empty string — easy to misread as a real value with loose checks. With $single=false (the default), it returns an empty array instead. This inconsistency between the two modes is a common source of bugs.", "references": ["https://developer.wordpress.org/reference/functions/get_post_meta/"] + }, + { + "id": "k-gotchas-002", + "category": "gotchas", + "subcategory": "queries", + "difficulty": "intermediate", + "prompt": "After running a secondary WP_Query loop, what function must you call to restore the global $post to the main query's current post?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "wp_reset_query()" }, + { "key": "B", "text": "wp_reset_postdata()" }, + { "key": "C", "text": "rewind_posts()" }, + { "key": "D", "text": "wp_reset_post()" } + ], + "correct_answer": "B", + "explanation": "wp_reset_postdata() restores the global $post to the main query's current post. wp_reset_query() is for undoing query_posts() (which should be avoided). Calling wp_reset_query() after a WP_Query loop destroys the main query's state. This is one of the most common WordPress loop mistakes.", + "references": ["https://developer.wordpress.org/reference/functions/wp_reset_postdata/"] + }, + { + "id": "k-gotchas-003", + "category": "gotchas", + "subcategory": "meta", + "difficulty": "intermediate", + "prompt": "update_post_meta() returns false in two different situations. Besides database failure, when else does it return false?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "When the meta key doesn't exist yet" }, + { "key": "B", "text": "When the new value is identical to the existing value" }, + { "key": "C", "text": "When the post doesn't exist" }, + { "key": "D", "text": "When the meta value exceeds the column size" } + ], + "correct_answer": "B", + "explanation": "update_post_meta() (via update_metadata) returns false when the new value is identical to the existing value, since no update is needed. This makes it impossible to distinguish 'no change needed' from 'update failed' using the return value alone.", + "references": ["https://developer.wordpress.org/reference/functions/update_post_meta/"] + }, + { + "id": "k-gotchas-004", + "category": "gotchas", + "subcategory": "functions", + "difficulty": "basic", + "prompt": "What does is_admin() actually check in WordPress?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Whether the current user has the Administrator role" }, + { "key": "B", "text": "Whether the current request is for an administrative interface page (URL path)" }, + { "key": "C", "text": "Whether the current user has the manage_options capability" }, + { "key": "D", "text": "Whether the current user is a super admin on multisite" } + ], + "correct_answer": "B", + "explanation": "is_admin() checks if the current request is for an admin page (WP_ADMIN constant), NOT whether the user is an administrator. It returns true on admin-ajax.php too. Use current_user_can() to check user roles.", + "references": ["https://developer.wordpress.org/reference/functions/is_admin/"] + }, + { + "id": "k-gotchas-005", + "category": "gotchas", + "subcategory": "redirects", + "difficulty": "basic", + "prompt": "What must you call immediately after wp_redirect() or wp_safe_redirect()?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "return" }, + { "key": "B", "text": "wp_die()" }, + { "key": "C", "text": "exit" }, + { "key": "D", "text": "Nothing — they terminate execution automatically" } + ], + "correct_answer": "C", + "explanation": "Neither wp_redirect() nor wp_safe_redirect() call exit or die. They only set the Location header and return. If you don't call exit afterward, PHP continues executing the rest of the script.", + "references": ["https://developer.wordpress.org/reference/functions/wp_redirect/"] + }, + { + "id": "k-gotchas-006", + "category": "gotchas", + "subcategory": "options", + "difficulty": "intermediate", + "prompt": "What is the key behavioral difference between add_option() and update_option() when the option already exists?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "add_option() silently does nothing and returns false; update_option() overwrites the value" }, + { "key": "B", "text": "add_option() throws an error; update_option() succeeds" }, + { "key": "C", "text": "Both overwrite the existing value" }, + { "key": "D", "text": "add_option() appends to the value; update_option() replaces it" } + ], + "correct_answer": "A", + "explanation": "add_option() returns false without modifying existing options. update_option() creates the option if it doesn't exist and updates it if it does. This makes update_option() the more common choice for most use cases.", + "references": ["https://developer.wordpress.org/reference/functions/add_option/"] + }, + { + "id": "k-gotchas-007", + "category": "gotchas", + "subcategory": "database", + "difficulty": "intermediate", + "prompt": "$wpdb->insert() does NOT return the new row's auto-increment ID. Where is the insert ID stored?", + "type": "short_answer", + "correct_answer": "insert_id", + "answer_type": "contains", + "explanation": "$wpdb->insert() returns int|false (rows affected count, typically 1). The auto-increment ID is stored in $wpdb->insert_id, which is set after any successful query via mysqli_insert_id().", + "references": ["https://developer.wordpress.org/reference/classes/wpdb/insert/"] + }, + { + "id": "k-gotchas-008", + "category": "gotchas", + "subcategory": "urls", + "difficulty": "intermediate", + "prompt": "What is the difference between home_url() and site_url() in WordPress?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "home_url() returns where visitors access the site; site_url() returns where WordPress files are installed" }, + { "key": "B", "text": "home_url() includes the port; site_url() does not" }, + { "key": "C", "text": "home_url() is for the front-end only; site_url() works everywhere" }, + { "key": "D", "text": "They always return the same value" } + ], + "correct_answer": "A", + "explanation": "home_url() returns the public-facing URL (option 'home'). site_url() returns where WordPress application files live (option 'siteurl'). They differ when WordPress is installed in a subdirectory — e.g., home_url() = example.com, site_url() = example.com/wp.", + "references": ["https://developer.wordpress.org/reference/functions/home_url/"] + }, + { + "id": "k-gotchas-009", + "category": "gotchas", + "subcategory": "filters", + "difficulty": "intermediate", + "prompt": "In the the_content filter chain, does wpautop run before or after do_shortcode?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "wpautop runs BEFORE do_shortcode (priority 10 vs 11)" }, + { "key": "B", "text": "do_shortcode runs BEFORE wpautop" }, + { "key": "C", "text": "They run at the same priority" }, + { "key": "D", "text": "do_shortcode is not hooked to the_content" } + ], + "correct_answer": "A", + "explanation": "wpautop runs at default priority 10, do_shortcode at priority 11. This means paragraph tags are inserted BEFORE shortcodes are processed, which can cause unwanted

and
tags around shortcode output.", + "references": ["https://developer.wordpress.org/reference/hooks/the_content/"] + }, + { + "id": "k-gotchas-010", + "category": "gotchas", + "subcategory": "http", + "difficulty": "basic", + "prompt": "What does wp_remote_get() return on failure?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "false" }, + { "key": "B", "text": "null" }, + { "key": "C", "text": "A WP_Error object" }, + { "key": "D", "text": "An empty array" } + ], + "correct_answer": "C", + "explanation": "wp_remote_get() returns array|WP_Error. On failure (connection timeout, DNS failure, etc.), it returns a WP_Error object, NOT false. Always check with is_wp_error() before accessing the response.", + "references": ["https://developer.wordpress.org/reference/functions/wp_remote_get/"] + }, + { + "id": "k-gotchas-011", + "category": "gotchas", + "subcategory": "functions", + "difficulty": "intermediate", + "prompt": "wp_update_post() is implemented by calling which function internally?", + "type": "short_answer", + "correct_answer": "wp_insert_post", + "answer_type": "contains", + "explanation": "wp_update_post() merges the provided data with the existing post and then calls wp_insert_post() to save. This means all the same hooks (save_post, wp_insert_post, etc.) fire on both create and update.", + "references": ["https://developer.wordpress.org/reference/functions/wp_update_post/"] + }, + { + "id": "k-gotchas-012", + "category": "gotchas", + "subcategory": "functions", + "difficulty": "hard", + "prompt": "The __return_* utility functions were not all introduced in the same version. Which one was added later than the others?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "__return_false (3.0.0)" }, + { "key": "B", "text": "__return_zero (3.0.0)" }, + { "key": "C", "text": "__return_empty_string (3.7.0)" }, + { "key": "D", "text": "__return_empty_array (3.0.0)" } + ], + "correct_answer": "C", + "explanation": "__return_true, __return_false, __return_zero, and __return_empty_array were all introduced in 3.0.0. __return_null was added in 3.4.0 and __return_empty_string in 3.7.0.", + "references": ["https://developer.wordpress.org/reference/functions/__return_empty_string/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/hooks.json b/datasets/suites/wp-core-v1/knowledge/hooks.json index 3fad692..3ce49a4 100644 --- a/datasets/suites/wp-core-v1/knowledge/hooks.json +++ b/datasets/suites/wp-core-v1/knowledge/hooks.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-hooks", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Hooks", "description": "Tests knowledge of WordPress hooks (actions and filters)", @@ -39,7 +39,7 @@ { "key": "D", "text": "query_vars" } ], "correct_answer": "B", - "explanation": "pre_get_posts fires before the query is executed and allows modification of query variables.", + "explanation": "pre_get_posts fires after query vars are parsed but before the SQL query is built, allowing modification of query variables.", "references": ["https://developer.wordpress.org/reference/hooks/pre_get_posts/"] }, { @@ -58,6 +58,265 @@ "correct_answer": "C", "explanation": "wp_enqueue_scripts is the proper hook for enqueueing scripts and styles on the front-end.", "references": ["https://developer.wordpress.org/reference/hooks/wp_enqueue_scripts/"] + }, + { + "id": "k-hooks-004", + "category": "hooks", + "subcategory": "internals", + "difficulty": "basic", + "prompt": "In WordPress core, add_action() is implemented as a wrapper around which function?", + "type": "short_answer", + "correct_answer": "add_filter", + "answer_type": "contains", + "explanation": "add_action() literally calls add_filter() with the same arguments. Internally, actions and filters use the same registration mechanism in the $wp_filter global.", + "references": ["https://developer.wordpress.org/reference/functions/add_action/"] + }, + { + "id": "k-hooks-005", + "category": "hooks", + "subcategory": "internals", + "difficulty": "basic", + "prompt": "What is the default priority when registering a hook callback with add_action() or add_filter()?", + "type": "short_answer", + "correct_answer": "10", + "answer_type": "exact", + "explanation": "Both add_action() and add_filter() default to priority 10. Lower numbers execute earlier.", + "references": ["https://developer.wordpress.org/reference/functions/add_filter/"] + }, + { + "id": "k-hooks-006", + "category": "hooks", + "subcategory": "internals", + "difficulty": "basic", + "prompt": "How many arguments does a hook callback receive by default if $accepted_args is not specified?", + "type": "short_answer", + "correct_answer": "1", + "answer_type": "exact", + "explanation": "The default $accepted_args parameter in add_filter() and add_action() is 1. Callbacks receive only the first argument unless a higher value is specified.", + "references": ["https://developer.wordpress.org/reference/functions/add_filter/"] + }, + { + "id": "k-hooks-007", + "category": "hooks", + "subcategory": "internals", + "difficulty": "intermediate", + "prompt": "When you call has_filter($hook, $callback), what does it return if the callback IS registered?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "true" }, + { "key": "B", "text": "The priority (int) at which the callback is registered" }, + { "key": "C", "text": "The callback function itself" }, + { "key": "D", "text": "The number of times the callback is registered" } + ], + "correct_answer": "B", + "explanation": "has_filter() with a callback argument returns the priority (int) if found, or false if not. Since priority 0 is valid and loosely equals false, you must use === false to check for absence.", + "references": ["https://developer.wordpress.org/reference/functions/has_filter/"] + }, + { + "id": "k-hooks-008", + "category": "hooks", + "subcategory": "internals", + "difficulty": "basic", + "prompt": "Does do_action() return a value?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Yes, it returns the filtered value like apply_filters()" }, + { "key": "B", "text": "No, it returns void" }, + { "key": "C", "text": "It returns the number of callbacks executed" }, + { "key": "D", "text": "It returns true on success, false on failure" } + ], + "correct_answer": "B", + "explanation": "do_action() returns void. Unlike apply_filters() which returns a filtered value, do_action() simply executes callbacks without tracking a return value.", + "references": ["https://developer.wordpress.org/reference/functions/do_action/"] + }, + { + "id": "k-hooks-009", + "category": "hooks", + "subcategory": "internals", + "difficulty": "intermediate", + "prompt": "What does did_action() return?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "true if the action has fired at least once" }, + { "key": "B", "text": "The number of times the action has fired during the current request" }, + { "key": "C", "text": "The timestamp of when the action last fired" }, + { "key": "D", "text": "An array of callbacks that were executed" } + ], + "correct_answer": "B", + "explanation": "did_action() returns the number of times the action has fired during the current request (from the $wp_actions global), or 0 if it has never fired.", + "references": ["https://developer.wordpress.org/reference/functions/did_action/"] + }, + { + "id": "k-hooks-010", + "category": "hooks", + "subcategory": "internals", + "difficulty": "intermediate", + "prompt": "In which WordPress version was the WP_Hook class introduced to replace the plain array-based hook system?", + "type": "short_answer", + "correct_answer": "4.7", + "answer_type": "contains", + "explanation": "WP_Hook was introduced in WordPress 4.7.0 as a proper class implementing Iterator and ArrayAccess interfaces, replacing the old $wp_filter plain array system while maintaining backward compatibility.", + "references": ["https://developer.wordpress.org/reference/classes/wp_hook/"] + }, + { + "id": "k-hooks-011", + "category": "hooks", + "subcategory": "loading_sequence", + "difficulty": "intermediate", + "prompt": "Which is the correct order of these WordPress loading sequence hooks?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "plugins_loaded → init → after_setup_theme → wp_loaded" }, + { "key": "B", "text": "plugins_loaded → after_setup_theme → init → wp_loaded" }, + { "key": "C", "text": "init → plugins_loaded → after_setup_theme → wp_loaded" }, + { "key": "D", "text": "after_setup_theme → plugins_loaded → init → wp_loaded" } + ], + "correct_answer": "B", + "explanation": "The correct loading order in wp-settings.php is: muplugins_loaded → plugins_loaded → setup_theme → after_setup_theme → init → wp_loaded.", + "references": ["https://developer.wordpress.org/reference/hooks/wp_loaded/"] + }, + { + "id": "k-hooks-012", + "category": "hooks", + "subcategory": "internals", + "difficulty": "intermediate", + "prompt": "What is the relationship between doing_action() and doing_filter() in WordPress?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "doing_action() is an alias that calls doing_filter() — they check the same $wp_current_filter stack" }, + { "key": "B", "text": "They check separate stacks: doing_action() uses $wp_actions, doing_filter() uses $wp_filters" }, + { "key": "C", "text": "doing_action() only works for do_action hooks, doing_filter() only for apply_filters hooks" }, + { "key": "D", "text": "doing_action() returns an action name, doing_filter() returns a boolean" } + ], + "correct_answer": "A", + "explanation": "doing_action() literally calls doing_filter(). Both check the $wp_current_filter stack. Since actions and filters share the same internal mechanism, there is no separate tracking.", + "references": ["https://developer.wordpress.org/reference/functions/doing_action/"] + }, + { + "id": "k-hooks-013", + "category": "hooks", + "subcategory": "internals", + "difficulty": "hard", + "prompt": "What does the special 'all' hook do in WordPress?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It fires once after all hooks have been processed at the end of the request" }, + { "key": "B", "text": "It fires for every single action and filter, receiving the hook name as the first argument" }, + { "key": "C", "text": "It registers a callback that runs for all hooks of a specific type (action or filter)" }, + { "key": "D", "text": "It returns a list of all registered hooks and their callbacks" } + ], + "correct_answer": "B", + "explanation": "Both apply_filters() and do_action() check for isset($wp_filter['all']). If present, callbacks on the 'all' hook fire for every hook invocation, receiving all arguments with the hook name prepended as the first argument.", + "references": ["https://developer.wordpress.org/reference/functions/_wp_call_all_hook/"] + }, + { + "id": "k-hooks-014", + "category": "hooks", + "subcategory": "actions", + "difficulty": "hard", + "prompt": "The pre_get_posts action uses do_action_ref_array instead of do_action. What is the practical significance of this?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It allows callbacks to receive multiple arguments instead of just one" }, + { "key": "B", "text": "The WP_Query object is passed by reference, so callbacks can modify it directly" }, + { "key": "C", "text": "It makes the hook fire asynchronously" }, + { "key": "D", "text": "It ensures the hook fires before all other actions" } + ], + "correct_answer": "B", + "explanation": "do_action_ref_array passes arguments as an array, preserving PHP references. For pre_get_posts, this means the WP_Query instance is passed by reference (&$this), allowing callbacks to modify query variables directly on the original object.", + "references": ["https://developer.wordpress.org/reference/functions/do_action_ref_array/"] + }, + { + "id": "k-hooks-015", + "category": "hooks", + "subcategory": "actions", + "difficulty": "hard", + "prompt": "When wp_insert_post() fires post-save hooks, in what order do save_post_{$post_type}, save_post, and wp_insert_post fire?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "save_post → save_post_{$post_type} → wp_insert_post" }, + { "key": "B", "text": "wp_insert_post → save_post → save_post_{$post_type}" }, + { "key": "C", "text": "save_post_{$post_type} → save_post → wp_insert_post" }, + { "key": "D", "text": "save_post → wp_insert_post → save_post_{$post_type}" } + ], + "correct_answer": "C", + "explanation": "The dynamic save_post_{$post_type} fires first (line 5212 in post.php), then save_post (line 5223), then wp_insert_post (line 5234). The most specific hook fires first.", + "references": ["https://developer.wordpress.org/reference/hooks/save_post/"] + }, + { + "id": "k-hooks-016", + "category": "hooks", + "subcategory": "filters", + "difficulty": "hard", + "prompt": "The cron_schedules filter is unusual because core schedules are merged AFTER the filter runs. What initial value do callbacks receive?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "An array containing the three built-in schedules (hourly, twicedaily, daily)" }, + { "key": "B", "text": "An empty array" }, + { "key": "C", "text": "null" }, + { "key": "D", "text": "The previously registered custom schedules only" } + ], + "correct_answer": "B", + "explanation": "The cron_schedules filter passes an empty array: apply_filters('cron_schedules', array()). Core schedules (hourly, twicedaily, daily, weekly) are merged in AFTER the filter via array_merge. Callbacks should return their additional schedules, not the core ones.", + "references": ["https://developer.wordpress.org/reference/hooks/cron_schedules/"] + }, + { + "id": "k-hooks-017", + "category": "hooks", + "subcategory": "filters", + "difficulty": "intermediate", + "prompt": "How many arguments does the 'the_title' filter pass to callbacks?", + "type": "short_answer", + "correct_answer": "2", + "answer_type": "exact", + "explanation": "the_title passes 2 arguments: the title string and the post ID. Callbacks must set $accepted_args to 2 in add_filter() to receive both. The the_content filter, by contrast, passes only 1 argument.", + "references": ["https://developer.wordpress.org/reference/hooks/the_title/"] + }, + { + "id": "k-hooks-018", + "category": "hooks", + "subcategory": "internals", + "difficulty": "hard", + "prompt": "In which WordPress version was did_filter() introduced, allowing you to count how many times a filter has been applied?", + "type": "short_answer", + "correct_answer": "6.1", + "answer_type": "contains", + "explanation": "did_filter() was introduced in WordPress 6.1.0, much later than did_action() which has existed since 2.1.0. They use separate globals: $wp_filters for filters and $wp_actions for actions.", + "references": ["https://developer.wordpress.org/reference/functions/did_filter/"] + }, + { + "id": "k-hooks-019", + "category": "hooks", + "subcategory": "actions", + "difficulty": "intermediate", + "prompt": "The wp_enqueue_scripts action is itself hooked to which other hook to fire at the right time?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "init" }, + { "key": "B", "text": "wp_head at priority 1" }, + { "key": "C", "text": "template_redirect" }, + { "key": "D", "text": "wp_loaded" } + ], + "correct_answer": "B", + "explanation": "The wp_enqueue_scripts() function is hooked to wp_head at priority 1 in default-filters.php. This function then fires the 'wp_enqueue_scripts' action, allowing plugins and themes to enqueue their assets.", + "references": ["https://developer.wordpress.org/reference/hooks/wp_enqueue_scripts/"] + }, + { + "id": "k-hooks-020", + "category": "hooks", + "subcategory": "filters", + "difficulty": "hard", + "prompt": "The 'authenticate' filter starts with what initial value before any callback processes it?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "false" }, + { "key": "B", "text": "null" }, + { "key": "C", "text": "An empty WP_User object" }, + { "key": "D", "text": "A WP_Error object" } + ], + "correct_answer": "B", + "explanation": "The authenticate filter starts with null: apply_filters('authenticate', null, $username, $password). Callbacks return WP_User on success, WP_Error on failure, or null to pass to the next handler in the chain.", + "references": ["https://developer.wordpress.org/reference/hooks/authenticate/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/html-api.json b/datasets/suites/wp-core-v1/knowledge/html-api.json index af17af0..693fe56 100644 --- a/datasets/suites/wp-core-v1/knowledge/html-api.json +++ b/datasets/suites/wp-core-v1/knowledge/html-api.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-html_api", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - HTML API", "description": "Tests knowledge of WordPress HTML API (WP_HTML_Tag_Processor, WP_HTML_Processor)", @@ -11,6 +11,7 @@ { "id": "k-html-serialize-001", "category": "html-api", + "subcategory": "processor", "difficulty": "hard", "prompt": "What does WP_HTML_Processor::serialize_token() return now that the method is public in WordPress 6.9?", "type": "multiple_choice", @@ -27,6 +28,7 @@ { "id": "k-html-script-reject-001", "category": "html-api", + "subcategory": "tag_processor", "difficulty": "hard", "prompt": "What does WP_HTML_Tag_Processor::set_modifiable_text() do when you try to set script contents that include '?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "The string 'disabled'" }, + { "key": "B", "text": "The string 'true'" }, + { "key": "C", "text": "The PHP boolean true" }, + { "key": "D", "text": "An empty string" } + ], + "correct_answer": "C", + "explanation": "get_attribute() returns the PHP boolean true for boolean/valueless attributes (e.g.,

). It returns null if the attribute is missing, and the decoded string value for attributes with values.", + "references": ["https://developer.wordpress.org/reference/classes/wp_html_tag_processor/get_attribute/"] + }, + { + "id": "k-html-007", + "category": "html-api", + "subcategory": "tag_processor", + "difficulty": "intermediate", + "prompt": "What is the maximum number of bookmarks allowed at one time in WP_HTML_Tag_Processor?", + "type": "short_answer", + "correct_answer": "10", + "answer_type": "exact", + "explanation": "WP_HTML_Tag_Processor::MAX_BOOKMARKS is 10. WP_HTML_Processor overrides this to 100 to accommodate the more complex tree-aware parsing.", + "references": ["https://developer.wordpress.org/reference/classes/wp_html_tag_processor/"] + }, + { + "id": "k-html-008", + "category": "html-api", + "subcategory": "processor", + "difficulty": "hard", + "prompt": "WP_HTML_Processor overrides MAX_BOOKMARKS from its parent class. What is the new limit?", + "type": "short_answer", + "correct_answer": "100", + "answer_type": "exact", + "explanation": "WP_HTML_Processor sets MAX_BOOKMARKS to 100 (vs 10 in WP_HTML_Tag_Processor) because the tree construction algorithm needs more bookmarks to track the stack of open elements.", + "references": ["https://developer.wordpress.org/reference/classes/wp_html_processor/"] + }, + { + "id": "k-html-009", + "category": "html-api", + "subcategory": "processor", + "difficulty": "intermediate", + "prompt": "WP_HTML_Processor::create_fragment() only supports one context element. Which one?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "" }, + { "key": "B", "text": "" }, + { "key": "C", "text": "" }, + { "key": "D", "text": "
" } + ], + "correct_answer": "C", + "explanation": "create_fragment() only supports '' as the context element (the default). Passing any other context returns null. This means all fragment parsing assumes the HTML is inside a body element.", + "references": ["https://developer.wordpress.org/reference/classes/wp_html_processor/create_fragment/"] + }, + { + "id": "k-html-010", + "category": "html-api", + "subcategory": "processor", + "difficulty": "intermediate", + "prompt": "When using WP_HTML_Processor::create_fragment() to parse '

text

', what does get_breadcrumbs() return when positioned on the STRONG tag?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "array( 'P', 'STRONG' )" }, + { "key": "B", "text": "array( 'HTML', 'BODY', 'P', 'STRONG' )" }, + { "key": "C", "text": "array( 'BODY', 'P', 'STRONG' )" }, + { "key": "D", "text": "array( 'STRONG' )" } + ], + "correct_answer": "B", + "explanation": "get_breadcrumbs() always includes the implicit outer elements. In a fragment context, breadcrumbs start with array('HTML', 'BODY', ...) followed by the actual nesting path to the current node.", + "references": ["https://developer.wordpress.org/reference/classes/wp_html_processor/get_breadcrumbs/"] + }, + { + "id": "k-html-011", + "category": "html-api", + "subcategory": "processor", + "difficulty": "hard", + "prompt": "How is WP_HTML_Processor::normalize() implemented internally?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It uses a regex-based cleanup pass over the HTML string" }, + { "key": "B", "text": "It creates a fragment parser and calls serialize() on it" }, + { "key": "C", "text": "It delegates to PHP's DOMDocument::normalizeDocument()" }, + { "key": "D", "text": "It runs the Tag Processor in a loop, fixing each token" } + ], + "correct_answer": "B", + "explanation": "normalize() is simply: static::create_fragment($html)->serialize(). It round-trips the HTML through the full HTML5 parser, producing normalized output with consistent quoting, lowercased names, and invalid UTF-8 replaced.", + "references": ["https://developer.wordpress.org/reference/classes/wp_html_processor/normalize/"] + }, + { + "id": "k-html-012", + "category": "html-api", + "subcategory": "tag_processor", + "difficulty": "intermediate", + "prompt": "In what letter case does WP_HTML_Tag_Processor::get_tag() return tag names?", + "type": "short_answer", + "correct_answer": "uppercase", + "answer_type": "contains", + "explanation": "get_tag() always returns tag names in uppercase (e.g., 'DIV', 'SPAN', 'IMG'). This normalization matches how the HTML spec treats tag names as case-insensitive.", + "references": ["https://developer.wordpress.org/reference/classes/wp_html_tag_processor/get_tag/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/interactivity-api.json b/datasets/suites/wp-core-v1/knowledge/interactivity-api.json deleted file mode 100644 index 4285ac1..0000000 --- a/datasets/suites/wp-core-v1/knowledge/interactivity-api.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "wp-core-knowledge-v1-interactivity_api", - "version": "1.1.0", - "metadata": { - "name": "WordPress Core Knowledge - Interactivity API", - "description": "Tests knowledge of WordPress Interactivity API", - "wp_version": "6.9", - "created_at": "2025-12-16" - }, - "tests": [ - { - "id": "k-interactivity-attach-001", - "category": "interactivity-api", - "difficulty": "hard", - "prompt": "In WordPress 6.9, what does the Interactivity API router option attachTo solve?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Persisting server state across page loads" }, - { "key": "B", "text": "Choosing the DOM parent where a router region renders to avoid collisions" }, - { "key": "C", "text": "Deferring hydration until window.onload" }, - { "key": "D", "text": "Forcing interactivity scripts into the footer" } - ], - "correct_answer": "B", - "explanation": "attachTo is a selector telling the router which parent node to render into, preventing conflicting renders when multiple router regions share a page.", - "references": ["https://make.wordpress.org/core/2025/11/25/wordpress-6-9-field-guide/"] - }, - { - "id": "k-interactivity-module-001", - "category": "interactivity-api", - "difficulty": "hard", - "prompt": "When manually registering a script module for the Interactivity API in WordPress 6.9, how do you mark it as compatible with client navigation so it loads on SPA transitions?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "Set supports.interactivity=true in block.json" }, - { "key": "B", "text": "Pass 'client_navigation' => true to wp_register_script_module()" }, - { "key": "C", "text": "Call wp_interactivity()->add_client_navigation_support_to_script_module( 'module-id' )" }, - { "key": "D", "text": "Prefix the handle with 'interactivity-'" } - ], - "correct_answer": "C", - "explanation": "Manually enqueued script modules must be flagged with add_client_navigation_support_to_script_module so router navigation imports them and adds the data-wp-router-options directive.", - "references": ["https://make.wordpress.org/core/2025/11/12/interactivity-apis-client-navigation-improvements-in-wordpress-6-9/"] - }, - { - "id": "k-interactivity-router-attr-001", - "category": "interactivity-api", - "difficulty": "hard", - "prompt": "After marking a script module as client-navigation compatible in 6.9, what attribute appears on its script tag?", - "type": "multiple_choice", - "choices": [ - { "key": "A", "text": "data-wp-interactive" }, - { "key": "B", "text": "data-wp-router-options='{\"loadOnClientNavigation\":true}'" }, - { "key": "C", "text": "data-wp-client-nav" }, - { "key": "D", "text": "type=\"module/nav\"" } - ], - "correct_answer": "B", - "explanation": "add_client_navigation_support_to_script_module injects data-wp-router-options with loadOnClientNavigation true so the router imports the module on SPA transitions.", - "references": ["https://make.wordpress.org/core/2025/11/12/interactivity-apis-client-navigation-improvements-in-wordpress-6-9/"] - } - ] -} diff --git a/datasets/suites/wp-core-v1/knowledge/internationalization.json b/datasets/suites/wp-core-v1/knowledge/internationalization.json index 89f7d70..f12c054 100644 --- a/datasets/suites/wp-core-v1/knowledge/internationalization.json +++ b/datasets/suites/wp-core-v1/knowledge/internationalization.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-internationalization", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Internationalization", "description": "Tests knowledge of WordPress internationalization (i18n)", @@ -11,6 +11,7 @@ { "id": "k-utf8-fallback-001", "category": "internationalization", + "subcategory": "encoding", "difficulty": "hard", "prompt": "How does WordPress 6.9 handle UTF-8 detection when the hosting environment lacks mbstring or iconv?", "type": "multiple_choice", @@ -23,6 +24,190 @@ "correct_answer": "B", "explanation": "6.9 ships a self-contained UTF-8 detection/handling layer in PHP so WordPress can reliably process UTF-8 even without mbstring or iconv.", "references": ["https://make.wordpress.org/core/2025/11/25/wordpress-6-9-field-guide/"] + }, + { + "id": "k-i18n-001", + "category": "internationalization", + "subcategory": "functions", + "difficulty": "basic", + "prompt": "What is the default text domain when calling __() without specifying a domain?", + "type": "short_answer", + "correct_answer": "default", + "answer_type": "exact", + "explanation": "All core i18n functions default the $domain parameter to 'default', which is the text domain reserved for WordPress core translations. Plugins and themes must specify their own text domain.", + "references": ["https://developer.wordpress.org/reference/functions/__/"] + }, + { + "id": "k-i18n-002", + "category": "internationalization", + "subcategory": "functions", + "difficulty": "basic", + "prompt": "What does _x() provide that __() does not?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Automatic HTML escaping of the translated string" }, + { "key": "B", "text": "A gettext context parameter for disambiguating identical source strings" }, + { "key": "C", "text": "Support for plural forms" }, + { "key": "D", "text": "Lazy loading of the text domain" } + ], + "correct_answer": "B", + "explanation": "_x() adds a $context parameter that helps translators distinguish identical source strings that need different translations. For example, 'Post' could mean a blog post (noun) or to post something (verb).", + "references": ["https://developer.wordpress.org/reference/functions/_x/"] + }, + { + "id": "k-i18n-003", + "category": "internationalization", + "subcategory": "functions", + "difficulty": "basic", + "prompt": "What is the difference between __() and _e() in WordPress i18n?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "__() returns the translated string; _e() echoes it directly" }, + { "key": "B", "text": "__() is for themes; _e() is for plugins" }, + { "key": "C", "text": "__() escapes HTML; _e() does not" }, + { "key": "D", "text": "__() loads the translation file; _e() uses a cached version" } + ], + "correct_answer": "A", + "explanation": "__() returns the translated string for use in expressions or assignments. _e() echoes the translated string directly, equivalent to echo __().", + "references": ["https://developer.wordpress.org/reference/functions/_e/"] + }, + { + "id": "k-i18n-004", + "category": "internationalization", + "subcategory": "functions", + "difficulty": "intermediate", + "prompt": "What is the correct parameter order for the _n() pluralization function?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "_n( $single, $plural, $number, $domain )" }, + { "key": "B", "text": "_n( $number, $single, $plural, $domain )" }, + { "key": "C", "text": "_n( $single, $plural, $domain, $number )" }, + { "key": "D", "text": "_n( $domain, $single, $plural, $number )" } + ], + "correct_answer": "A", + "explanation": "_n() takes singular form first, then plural, then the count, then the domain. _nx() adds a $context parameter between $number and $domain.", + "references": ["https://developer.wordpress.org/reference/functions/_n/"] + }, + { + "id": "k-i18n-005", + "category": "internationalization", + "subcategory": "functions", + "difficulty": "intermediate", + "prompt": "What does esc_html__() do compared to __() alone?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It translates and then passes the result through esc_html() before returning" }, + { "key": "B", "text": "It strips all HTML tags from the translated string" }, + { "key": "C", "text": "It translates and then echoes the escaped result" }, + { "key": "D", "text": "It only translates strings that contain HTML entities" } + ], + "correct_answer": "A", + "explanation": "esc_html__() returns esc_html(translate(...)). Similarly, esc_attr__() returns esc_attr(translate(...)). The _e variants (esc_html_e, esc_attr_e) echo instead of returning.", + "references": ["https://developer.wordpress.org/reference/functions/esc_html__/"] + }, + { + "id": "k-i18n-006", + "category": "internationalization", + "subcategory": "file_formats", + "difficulty": "intermediate", + "prompt": "Since WordPress 6.5, what translation file format does WordPress prefer to load first?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": ".po (Portable Object) files" }, + { "key": "B", "text": ".mo (Machine Object) files" }, + { "key": "C", "text": ".l10n.php (PHP translation) files" }, + { "key": "D", "text": ".json (JED format) files" } + ], + "correct_answer": "C", + "explanation": "Since 6.5.0, load_textdomain() tries .l10n.php files first, falling back to .mo files. PHP translation files are faster because they can be cached by PHP's opcache.", + "references": ["https://developer.wordpress.org/reference/functions/load_textdomain/"] + }, + { + "id": "k-i18n-007", + "category": "internationalization", + "subcategory": "file_formats", + "difficulty": "intermediate", + "prompt": "In which WordPress version were .l10n.php translation files introduced?", + "type": "short_answer", + "correct_answer": "6.5", + "answer_type": "contains", + "explanation": "PHP translation files (.l10n.php) were introduced in WordPress 6.5.0 alongside the WP_Translation_Controller class. They load faster than .mo files because PHP's opcache can cache them.", + "references": ["https://developer.wordpress.org/reference/functions/load_textdomain/"] + }, + { + "id": "k-i18n-008", + "category": "internationalization", + "subcategory": "functions", + "difficulty": "intermediate", + "prompt": "What is the purpose of _n_noop() in WordPress i18n?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It registers plural strings for translation extraction without translating immediately, deferring until the count is known" }, + { "key": "B", "text": "It silently ignores translation for specific strings" }, + { "key": "C", "text": "It creates a no-operation fallback when translations are unavailable" }, + { "key": "D", "text": "It registers a string that should never be translated" } + ], + "correct_answer": "A", + "explanation": "_n_noop() registers singular and plural forms so translation tools can extract them, but defers actual translation until translate_nooped_plural() is called with a known count. This is useful when the count is determined later in code.", + "references": ["https://developer.wordpress.org/reference/functions/_n_noop/"] + }, + { + "id": "k-i18n-009", + "category": "internationalization", + "subcategory": "locale", + "difficulty": "hard", + "prompt": "How does determine_locale() differ from get_locale() on WordPress admin pages?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "determine_locale() returns the user's locale via get_user_locale(); get_locale() returns the site locale" }, + { "key": "B", "text": "determine_locale() checks the Accept-Language header; get_locale() uses the database" }, + { "key": "C", "text": "They return the same value on admin pages" }, + { "key": "D", "text": "determine_locale() returns the network locale on multisite; get_locale() returns the site locale" } + ], + "correct_answer": "A", + "explanation": "determine_locale() is context-aware: on admin screens it returns the user's preferred locale (via get_user_locale()), while on the front-end it returns the site locale. get_locale() always returns the site locale regardless of context.", + "references": ["https://developer.wordpress.org/reference/functions/determine_locale/"] + }, + { + "id": "k-i18n-010", + "category": "internationalization", + "subcategory": "loading", + "difficulty": "intermediate", + "prompt": "In which WordPress version was just-in-time (JIT) translation loading introduced?", + "type": "short_answer", + "correct_answer": "4.6", + "answer_type": "contains", + "explanation": "JIT translation loading was introduced in WordPress 4.6.0 via _load_textdomain_just_in_time(). Instead of loading all translation files upfront, domains are loaded on first use when a string is requested.", + "references": ["https://developer.wordpress.org/reference/functions/_load_textdomain_just_in_time/"] + }, + { + "id": "k-i18n-011", + "category": "internationalization", + "subcategory": "javascript", + "difficulty": "intermediate", + "prompt": "What function sets up translations for a JavaScript file in WordPress?", + "type": "short_answer", + "correct_answer": "wp_set_script_translations", + "answer_type": "contains", + "explanation": "wp_set_script_translations() associates translated strings with a script handle, enabling the wp.i18n JavaScript API (__(), _x(), _n(), etc.) to work with that script's text domain.", + "references": ["https://developer.wordpress.org/reference/functions/wp_set_script_translations/"] + }, + { + "id": "k-i18n-012", + "category": "internationalization", + "subcategory": "locale", + "difficulty": "basic", + "prompt": "The 'default' text domain in WordPress is reserved for which purpose?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "WordPress core translations" }, + { "key": "B", "text": "The active theme's translations" }, + { "key": "C", "text": "Untranslated fallback strings" }, + { "key": "D", "text": "Translation strings from all plugins" } + ], + "correct_answer": "A", + "explanation": "The 'default' text domain is exclusively reserved for WordPress core. JIT loading explicitly skips it since core translations are loaded separately. Plugins and themes must use their own unique text domains.", + "references": ["https://developer.wordpress.org/reference/functions/__/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/queries.json b/datasets/suites/wp-core-v1/knowledge/queries.json index a906387..5105840 100644 --- a/datasets/suites/wp-core-v1/knowledge/queries.json +++ b/datasets/suites/wp-core-v1/knowledge/queries.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-queries", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Queries", "description": "Tests knowledge of WordPress database queries", @@ -11,6 +11,7 @@ { "id": "k-queries-001", "category": "queries", + "subcategory": "wpdb", "difficulty": "basic", "prompt": "What WordPress function should be used to safely insert data into the database?", "type": "multiple_choice", @@ -27,6 +28,7 @@ { "id": "k-queries-002", "category": "queries", + "subcategory": "wp_query", "difficulty": "intermediate", "prompt": "In WP_Query, which parameter limits the number of posts returned?", "type": "multiple_choice", @@ -37,8 +39,419 @@ { "key": "D", "text": "numberposts" } ], "correct_answer": "C", - "explanation": "posts_per_page is the WP_Query parameter that limits the number of posts returned per page.", + "explanation": "posts_per_page is the WP_Query parameter that limits the number of posts returned per page. numberposts is a get_posts() alias that maps to posts_per_page.", "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-003", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "basic", + "prompt": "What happens when you set posts_per_page to -1 in WP_Query?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It triggers a fatal error" }, + { "key": "B", "text": "It returns all matching posts with no limit" }, + { "key": "C", "text": "It returns a single post" }, + { "key": "D", "text": "It uses the value from Settings > Reading" } + ], + "correct_answer": "B", + "explanation": "Setting posts_per_page to -1 sets the nopaging flag to true, which removes the LIMIT clause from the SQL query and returns all matching posts.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-004", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "basic", + "prompt": "What is the default sort direction (order) in WP_Query?", + "type": "short_answer", + "correct_answer": "DESC", + "answer_type": "exact", + "explanation": "WP_Query defaults to 'DESC' (descending) order, sorting newest posts first.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-005", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "basic", + "prompt": "What function should you call after a custom WP_Query loop to restore the global $post variable?", + "type": "short_answer", + "correct_answer": "wp_reset_postdata", + "answer_type": "contains", + "explanation": "wp_reset_postdata() restores the global $post to the current post in the main query. It should be called after every secondary WP_Query loop.", + "references": ["https://developer.wordpress.org/reference/functions/wp_reset_postdata/"] + }, + { + "id": "k-queries-006", + "category": "queries", + "subcategory": "get_posts", + "difficulty": "basic", + "prompt": "What is the default value of the suppress_filters parameter in get_posts()?", + "type": "short_answer", + "correct_answer": "true", + "answer_type": "exact", + "explanation": "get_posts() defaults suppress_filters to true, unlike WP_Query which defaults it to false. This means get_posts() skips all query modification filters (posts_where, posts_join, posts_clauses, etc.) by default.", + "references": ["https://developer.wordpress.org/reference/functions/get_posts/"] + }, + { + "id": "k-queries-007", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "basic", + "prompt": "What expensive SQL feature does setting no_found_rows to true in WP_Query skip?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "SQL_CALC_FOUND_ROWS and the subsequent SELECT FOUND_ROWS() query" }, + { "key": "B", "text": "The JOIN against the postmeta table" }, + { "key": "C", "text": "Post meta and term cache priming" }, + { "key": "D", "text": "The WHERE clause for post_status filtering" } + ], + "correct_answer": "A", + "explanation": "When no_found_rows is true, WP_Query omits SQL_CALC_FOUND_ROWS from the SELECT and skips the follow-up SELECT FOUND_ROWS() query. This improves performance when you do not need pagination totals.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-008", + "category": "queries", + "subcategory": "meta_query", + "difficulty": "basic", + "prompt": "What is the default CAST type for meta_value comparisons in WP_Meta_Query?", + "type": "short_answer", + "correct_answer": "CHAR", + "answer_type": "exact", + "explanation": "WP_Meta_Query::get_cast_for_type() returns 'CHAR' when no type is specified. This means meta_value is compared as a string by default.", + "references": ["https://developer.wordpress.org/reference/classes/wp_meta_query/"] + }, + { + "id": "k-queries-009", + "category": "queries", + "subcategory": "tax_query", + "difficulty": "basic", + "prompt": "What is the default value of the 'field' parameter in a WP_Tax_Query clause?", + "type": "short_answer", + "correct_answer": "term_id", + "answer_type": "exact", + "explanation": "WP_Tax_Query defaults the 'field' parameter to 'term_id'. Other valid options are 'slug', 'name', and 'term_taxonomy_id'.", + "references": ["https://developer.wordpress.org/reference/classes/wp_tax_query/"] + }, + { + "id": "k-queries-010", + "category": "queries", + "subcategory": "get_posts", + "difficulty": "basic", + "prompt": "What is the default numberposts value in the get_posts() function?", + "type": "short_answer", + "correct_answer": "5", + "answer_type": "exact", + "explanation": "get_posts() defaults numberposts to 5, which is mapped to posts_per_page internally. This differs from WP_Query, which defaults to the value in Settings > Reading (typically 10).", + "references": ["https://developer.wordpress.org/reference/functions/get_posts/"] + }, + { + "id": "k-queries-011", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "intermediate", + "prompt": "Which set correctly lists all valid values for the WP_Query 'fields' parameter?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "'ids', 'id=>parent', and '' (empty string for full post objects)" }, + { "key": "B", "text": "'ids', 'titles', 'id=>parent', and '' (empty string)" }, + { "key": "C", "text": "'all', 'ids', 'id=>parent', and 'id=>slug'" }, + { "key": "D", "text": "'ids', 'id=>parent', 'id=>type', and '' (empty string)" } + ], + "correct_answer": "A", + "explanation": "WP_Query's fields parameter accepts exactly three values: '' (empty string, returns full WP_Post objects), 'ids' (returns an array of post IDs), and 'id=>parent' (returns an associative array of parent IDs keyed by post ID).", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-012", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "intermediate", + "prompt": "Why is query_posts() discouraged in WordPress development?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It was deprecated in WordPress 5.0 and will be removed" }, + { "key": "B", "text": "It replaces the global $wp_query with a new WP_Query, destroying the original main query" }, + { "key": "C", "text": "It does not support custom post types or taxonomies" }, + { "key": "D", "text": "It bypasses the object cache and always hits the database" } + ], + "correct_answer": "B", + "explanation": "query_posts() creates a new WP_Query and assigns it to $GLOBALS['wp_query'], completely overriding the original main query. This breaks conditional tags, pagination, and other features that depend on the main query. Using pre_get_posts or a secondary WP_Query is preferred.", + "references": ["https://developer.wordpress.org/reference/functions/query_posts/"] + }, + { + "id": "k-queries-013", + "category": "queries", + "subcategory": "user_query", + "difficulty": "intermediate", + "prompt": "In WP_User_Query, what is the difference between the 'role' and 'role__in' parameters?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "'role' requires users to match ALL listed roles (AND); 'role__in' requires at least one (OR)" }, + { "key": "B", "text": "'role' accepts a single role string; 'role__in' accepts an array" }, + { "key": "C", "text": "'role' matches by role slug; 'role__in' matches by display name" }, + { "key": "D", "text": "'role' is for the current site only; 'role__in' searches across all sites" } + ], + "correct_answer": "A", + "explanation": "'role' is an inclusive list where users must match each role (AND logic). 'role__in' requires users to have at least one of the listed roles (OR logic). Both accept arrays of role slugs.", + "references": ["https://developer.wordpress.org/reference/classes/wp_user_query/"] + }, + { + "id": "k-queries-014", + "category": "queries", + "subcategory": "query_filters", + "difficulty": "intermediate", + "prompt": "What does the 'posts_pre_query' filter allow you to do in WP_Query?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Modify the query arguments before they are parsed" }, + { "key": "B", "text": "Short-circuit the database query entirely by returning a non-null array of posts" }, + { "key": "C", "text": "Add custom SQL clauses to the WHERE portion of the query" }, + { "key": "D", "text": "Filter post objects after they are fetched but before template tags work" } + ], + "correct_answer": "B", + "explanation": "posts_pre_query (since 4.6.0) lets you return a non-null value to bypass WordPress's default post queries entirely. When used, the filtering function should also set found_posts and max_num_pages on the WP_Query object.", + "references": ["https://developer.wordpress.org/reference/hooks/posts_pre_query/"] + }, + { + "id": "k-queries-015", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "intermediate", + "prompt": "Under what conditions does WP_Query prepend sticky posts to the results?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "On any archive page when post_type is 'post'" }, + { "key": "B", "text": "On the home page (is_home), on the first page only, and when ignore_sticky_posts is false" }, + { "key": "C", "text": "Whenever sticky posts exist, regardless of query context" }, + { "key": "D", "text": "On the home page and category archives, on any page" } + ], + "correct_answer": "B", + "explanation": "Sticky post prepending requires three conditions: is_home must be true, the page must be 1 or less, and ignore_sticky_posts must be false. On subsequent pages or non-home queries, sticky posts are not given special treatment.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-016", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "intermediate", + "prompt": "After using query_posts(), which function must you call to restore the global $wp_query to its original state?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "wp_reset_postdata()" }, + { "key": "B", "text": "wp_reset_query()" }, + { "key": "C", "text": "rewind_posts()" }, + { "key": "D", "text": "wp_restore_query()" } + ], + "correct_answer": "B", + "explanation": "wp_reset_query() restores $wp_query to the original $wp_the_query and then calls wp_reset_postdata(). wp_reset_postdata() alone only restores the global $post but does NOT restore $wp_query, making it insufficient after query_posts().", + "references": ["https://developer.wordpress.org/reference/functions/wp_reset_query/"] + }, + { + "id": "k-queries-017", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "intermediate", + "prompt": "When post_type is set to 'any' in WP_Query, post types with which registration property set to true are excluded from the results?", + "type": "short_answer", + "correct_answer": "exclude_from_search", + "answer_type": "contains", + "explanation": "post_type => 'any' queries all post types returned by get_post_types( array( 'exclude_from_search' => false ) ), excluding types like 'revision' and 'nav_menu_item' that have exclude_from_search set to true.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-018", + "category": "queries", + "subcategory": "date_query", + "difficulty": "intermediate", + "prompt": "In WP_Date_Query, what number represents Sunday in the 'dayofweek' parameter?", + "type": "short_answer", + "correct_answer": "1", + "answer_type": "exact", + "explanation": "In WP_Date_Query, 'dayofweek' uses MySQL's DAYOFWEEK() convention where 1 = Sunday and 7 = Saturday. This differs from 'dayofweek_iso' where 1 = Monday and 7 = Sunday.", + "references": ["https://developer.wordpress.org/reference/classes/wp_date_query/"] + }, + { + "id": "k-queries-019", + "category": "queries", + "subcategory": "tax_query", + "difficulty": "intermediate", + "prompt": "What is the default value of the 'relation' parameter in a WP_Tax_Query?", + "type": "short_answer", + "correct_answer": "AND", + "answer_type": "exact", + "explanation": "WP_Tax_Query defaults the relation to 'AND', meaning all taxonomy clauses must match. The only other accepted value is 'OR'.", + "references": ["https://developer.wordpress.org/reference/classes/wp_tax_query/"] + }, + { + "id": "k-queries-020", + "category": "queries", + "subcategory": "tax_query", + "difficulty": "intermediate", + "prompt": "What is the default value of 'include_children' in a WP_Tax_Query clause?", + "type": "short_answer", + "correct_answer": "true", + "answer_type": "exact", + "explanation": "WP_Tax_Query defaults include_children to true. When the taxonomy is hierarchical, child terms are automatically included in the query. Set it to false to match only the exact terms specified.", + "references": ["https://developer.wordpress.org/reference/classes/wp_tax_query/"] + }, + { + "id": "k-queries-021", + "category": "queries", + "subcategory": "comment_query", + "difficulty": "intermediate", + "prompt": "What is the default value of no_found_rows in WP_Comment_Query?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "false, same as WP_Query" }, + { "key": "B", "text": "true, unlike WP_Query which defaults to false" }, + { "key": "C", "text": "It depends on the 'count' parameter" }, + { "key": "D", "text": "WP_Comment_Query does not support no_found_rows" } + ], + "correct_answer": "B", + "explanation": "WP_Comment_Query defaults no_found_rows to true, unlike WP_Query which defaults to false. This means pagination totals are not calculated by default for comment queries.", + "references": ["https://developer.wordpress.org/reference/classes/wp_comment_query/"] + }, + { + "id": "k-queries-022", + "category": "queries", + "subcategory": "get_posts", + "difficulty": "hard", + "prompt": "Which set of parameters have DIFFERENT default values in get_posts() compared to a direct WP_Query?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "suppress_filters (true vs false), no_found_rows (true vs false), ignore_sticky_posts (true vs false)" }, + { "key": "B", "text": "cache_results (false vs true), suppress_filters (true vs false), post_status ('any' vs 'publish')" }, + { "key": "C", "text": "orderby ('title' vs 'date'), no_found_rows (true vs false), post_type ('any' vs 'post')" }, + { "key": "D", "text": "suppress_filters (true vs false), update_post_meta_cache (false vs true), fields ('ids' vs '')" } + ], + "correct_answer": "A", + "explanation": "get_posts() differs from WP_Query in three key defaults: suppress_filters is true (WP_Query: false), no_found_rows is true (WP_Query: false), and ignore_sticky_posts is true (WP_Query: false). It also defaults numberposts to 5 instead of the Reading setting.", + "references": ["https://developer.wordpress.org/reference/functions/get_posts/"] + }, + { + "id": "k-queries-023", + "category": "queries", + "subcategory": "query_filters", + "difficulty": "hard", + "prompt": "Which WP_Query filter fires BEFORE sticky post handling and post status checks: 'posts_results' or 'the_posts'?", + "type": "short_answer", + "correct_answer": "posts_results", + "answer_type": "contains", + "explanation": "'posts_results' filters the raw post results array prior to status checks and sticky post handling. 'the_posts' fires after all internal processing including sticky post prepending and status checks.", + "references": ["https://developer.wordpress.org/reference/hooks/posts_results/", "https://developer.wordpress.org/reference/hooks/the_posts/"] + }, + { + "id": "k-queries-024", + "category": "queries", + "subcategory": "query_filters", + "difficulty": "hard", + "prompt": "The 'posts_clauses' filter receives an associative array of SQL clause strings. Which set lists all seven keys in that array?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "where, groupby, join, orderby, distinct, fields, limits" }, + { "key": "B", "text": "where, groupby, join, orderby, having, fields, limits" }, + { "key": "C", "text": "select, where, groupby, join, orderby, distinct, limits" }, + { "key": "D", "text": "where, groupby, join, orderby, distinct, fields, offset" } + ], + "correct_answer": "A", + "explanation": "The posts_clauses filter receives compact($pieces) where $pieces = array('where', 'groupby', 'join', 'orderby', 'distinct', 'fields', 'limits'). There is no 'having', 'select', or 'offset' key.", + "references": ["https://developer.wordpress.org/reference/hooks/posts_clauses/"] + }, + { + "id": "k-queries-025", + "category": "queries", + "subcategory": "tax_query", + "difficulty": "hard", + "prompt": "Which two WP_Tax_Query operators were added in WordPress 4.1?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "LIKE and NOT LIKE" }, + { "key": "B", "text": "EXISTS and NOT EXISTS" }, + { "key": "C", "text": "BETWEEN and NOT BETWEEN" }, + { "key": "D", "text": "REGEXP and NOT REGEXP" } + ], + "correct_answer": "B", + "explanation": "EXISTS and NOT EXISTS were added to WP_Tax_Query in WordPress 4.1. They check whether a post has any term in a given taxonomy (EXISTS) or no terms (NOT EXISTS), without specifying particular terms. The original operators are IN, NOT IN, and AND.", + "references": ["https://developer.wordpress.org/reference/classes/wp_tax_query/"] + }, + { + "id": "k-queries-026", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "hard", + "prompt": "How does the WP_Query::is_main_query() method determine whether the current instance is the main query?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It checks an internal $is_main boolean property set during construction" }, + { "key": "B", "text": "It compares $this to the global $wp_the_query using strict identity (===)" }, + { "key": "C", "text": "It checks whether the query was initiated by the 'wp' action hook" }, + { "key": "D", "text": "It compares the query vars against the original request URI parsed by WP_Rewrite" } + ], + "correct_answer": "B", + "explanation": "WP_Query::is_main_query() simply returns $wp_the_query === $this, checking whether the current instance is the exact same object as the global $wp_the_query that was set during WordPress initialization.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/is_main_query/"] + }, + { + "id": "k-queries-027", + "category": "queries", + "subcategory": "meta_query", + "difficulty": "hard", + "prompt": "In WP_Meta_Query, which group of compare operators are classified as numeric-only (not available for non-numeric comparisons)?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": ">, >=, <, <=, BETWEEN, NOT BETWEEN" }, + { "key": "B", "text": ">, >=, <, <=, IN, NOT IN" }, + { "key": "C", "text": "BETWEEN, NOT BETWEEN, LIKE, NOT LIKE" }, + { "key": "D", "text": ">, >=, <, <=, REGEXP, NOT REGEXP" } + ], + "correct_answer": "A", + "explanation": "WP_Meta_Query separates operators into two groups: non-numeric (=, !=, LIKE, NOT LIKE, IN, NOT IN, EXISTS, NOT EXISTS, RLIKE, REGEXP, NOT REGEXP) and numeric-only (>, >=, <, <=, BETWEEN, NOT BETWEEN). The numeric operators are validated separately.", + "references": ["https://developer.wordpress.org/reference/classes/wp_meta_query/"] + }, + { + "id": "k-queries-028", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "hard", + "prompt": "What happens when posts_per_page is set to 0 in WP_Query?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It returns an empty result set" }, + { "key": "B", "text": "It is silently forced to 1, returning a single post" }, + { "key": "C", "text": "It behaves the same as -1, returning all posts" }, + { "key": "D", "text": "It falls back to the posts_per_page option from Settings > Reading" } + ], + "correct_answer": "B", + "explanation": "When posts_per_page is 0, WP_Query forces it to 1. The code checks: if 0 === $q['posts_per_page'], set it to 1. This prevents a zero-limit query from being issued.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-029", + "category": "queries", + "subcategory": "wp_query", + "difficulty": "hard", + "prompt": "What object cache group does WP_Query use for storing cached query results in WordPress 6.9?", + "type": "short_answer", + "correct_answer": "post-queries", + "answer_type": "contains", + "explanation": "WP_Query stores cached results using wp_cache_set_salted() with the cache group 'post-queries'. The cache key is generated by the generate_cache_key() method and salted with last_changed timestamps from the 'posts' and optionally 'terms' groups.", + "references": ["https://developer.wordpress.org/reference/classes/wp_query/"] + }, + { + "id": "k-queries-030", + "category": "queries", + "subcategory": "meta_query", + "difficulty": "hard", + "prompt": "In WP_Meta_Query, what MySQL type is 'NUMERIC' aliased to by the get_cast_for_type() method?", + "type": "short_answer", + "correct_answer": "SIGNED", + "answer_type": "exact", + "explanation": "WP_Meta_Query::get_cast_for_type() converts 'NUMERIC' to 'SIGNED' before using it in a CAST() expression. This is because MySQL's CAST does not accept NUMERIC directly; SIGNED is the equivalent integer type.", + "references": ["https://developer.wordpress.org/reference/classes/wp_meta_query/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/rest-api.json b/datasets/suites/wp-core-v1/knowledge/rest-api.json index 1167339..8263885 100644 --- a/datasets/suites/wp-core-v1/knowledge/rest-api.json +++ b/datasets/suites/wp-core-v1/knowledge/rest-api.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-rest_api", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - REST API", "description": "Tests knowledge of WordPress REST API", @@ -11,6 +11,7 @@ { "id": "k-rest-001", "category": "rest-api", + "subcategory": "namespaces", "difficulty": "intermediate", "prompt": "What is the default namespace prefix for WordPress core REST API endpoints?", "type": "short_answer", @@ -21,6 +22,7 @@ { "id": "k-rest-002", "category": "rest-api", + "subcategory": "registration", "difficulty": "intermediate", "prompt": "Which function is used to register a custom REST API route in WordPress?", "type": "multiple_choice", @@ -33,6 +35,434 @@ "correct_answer": "B", "explanation": "register_rest_route() is the function used to register custom REST API endpoints.", "references": ["https://developer.wordpress.org/reference/functions/register_rest_route/"] + }, + { + "id": "k-rest-003", + "category": "rest-api", + "subcategory": "core-infrastructure", + "difficulty": "basic", + "prompt": "What is the default URL prefix used to access the WordPress REST API?", + "type": "short_answer", + "correct_answer": "wp-json", + "answer_type": "exact", + "explanation": "The default REST API URL prefix is 'wp-json', making the base URL something like https://example.com/wp-json/. This is filterable via the 'rest_url_prefix' filter. Note that 'wp/v2' is the core namespace, not the URL prefix.", + "references": ["https://developer.wordpress.org/reference/functions/rest_get_url_prefix/"] + }, + { + "id": "k-rest-004", + "category": "rest-api", + "subcategory": "core-infrastructure", + "difficulty": "basic", + "prompt": "Which PHP constant is defined as true when the WordPress REST API is handling a request?", + "type": "short_answer", + "correct_answer": "REST_REQUEST", + "answer_type": "contains", + "explanation": "WordPress defines the REST_REQUEST constant as true when the REST API is loaded and processing a request. This can be used to conditionally run code only during REST API requests.", + "references": ["https://developer.wordpress.org/reference/functions/rest_api_loaded/"] + }, + { + "id": "k-rest-005", + "category": "rest-api", + "subcategory": "core-infrastructure", + "difficulty": "intermediate", + "prompt": "What is the value of the REST_API_VERSION constant in WordPress?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "1.0" }, + { "key": "B", "text": "2.0" }, + { "key": "C", "text": "3.0" }, + { "key": "D", "text": "2.1" } + ], + "correct_answer": "B", + "explanation": "The REST_API_VERSION constant is set to '2.0' in WordPress core. This has been at version 2.0 since the REST API was merged into core from the WP REST API plugin." + }, + { + "id": "k-rest-006", + "category": "rest-api", + "subcategory": "route-registration", + "difficulty": "basic", + "prompt": "On which WordPress action hook should register_rest_route() be called?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "init" }, + { "key": "B", "text": "wp_loaded" }, + { "key": "C", "text": "rest_api_init" }, + { "key": "D", "text": "plugins_loaded" } + ], + "correct_answer": "C", + "explanation": "REST routes must be registered on the 'rest_api_init' action hook. Registering on 'init' or other hooks will not work because the REST API server has not been initialized yet at those points.", + "references": ["https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/"] + }, + { + "id": "k-rest-007", + "category": "rest-api", + "subcategory": "route-registration", + "difficulty": "intermediate", + "prompt": "Since WordPress 5.5, what happens if you call register_rest_route() without specifying a permission_callback?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "The route silently defaults to requiring administrator capabilities" }, + { "key": "B", "text": "A _doing_it_wrong notice is triggered" }, + { "key": "C", "text": "The route registration fails and returns false" }, + { "key": "D", "text": "The endpoint returns a 403 error for all requests" } + ], + "correct_answer": "B", + "explanation": "Since WordPress 5.5, omitting the permission_callback argument from register_rest_route() triggers a _doing_it_wrong notice. The route still registers and functions, but the notice warns that a permission callback should be explicitly defined.", + "references": ["https://developer.wordpress.org/reference/functions/register_rest_route/"] + }, + { + "id": "k-rest-008", + "category": "rest-api", + "subcategory": "route-registration", + "difficulty": "intermediate", + "prompt": "When creating a public REST API endpoint that should be accessible without authentication, what is the WordPress-recommended value for the permission_callback parameter?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "__return_true" }, + { "key": "B", "text": "__return_false" }, + { "key": "C", "text": "is_user_logged_in" }, + { "key": "D", "text": "null" } + ], + "correct_answer": "A", + "explanation": "The WordPress-recommended approach for public endpoints is to use __return_true as the permission callback. This is a built-in WordPress function that simply returns true. Using null causes the _doing_it_wrong notice introduced in WordPress 5.5.", + "references": ["https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/"] + }, + { + "id": "k-rest-009", + "category": "rest-api", + "subcategory": "server-constants", + "difficulty": "intermediate", + "prompt": "What HTTP methods are included in the WP_REST_Server::EDITABLE constant?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "PUT, PATCH" }, + { "key": "B", "text": "POST, PUT, PATCH" }, + { "key": "C", "text": "POST, PUT" }, + { "key": "D", "text": "PUT, PATCH, DELETE" } + ], + "correct_answer": "B", + "explanation": "WP_REST_Server::EDITABLE is defined as 'POST, PUT, PATCH'. It includes POST along with PUT and PATCH because WordPress uses POST as a fallback method for clients that cannot send PUT or PATCH requests natively.", + "references": ["https://developer.wordpress.org/reference/classes/wp_rest_server/"] + }, + { + "id": "k-rest-010", + "category": "rest-api", + "subcategory": "server-constants", + "difficulty": "hard", + "prompt": "Which of the following correctly lists ALL HTTP methods in the WP_REST_Server::ALLMETHODS constant?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "GET, POST, PUT, DELETE" }, + { "key": "B", "text": "GET, POST, PUT, PATCH, DELETE" }, + { "key": "C", "text": "GET, POST, PUT, PATCH, DELETE, OPTIONS" }, + { "key": "D", "text": "GET, HEAD, POST, PUT, PATCH, DELETE" } + ], + "correct_answer": "B", + "explanation": "WP_REST_Server::ALLMETHODS is defined as 'GET, POST, PUT, PATCH, DELETE'. It does not include OPTIONS or HEAD, even though those are valid HTTP methods that the REST API server handles separately.", + "references": ["https://developer.wordpress.org/reference/classes/wp_rest_server/"] + }, + { + "id": "k-rest-011", + "category": "rest-api", + "subcategory": "pagination", + "difficulty": "basic", + "prompt": "What is the default number of items returned per page by WordPress REST API collection endpoints?", + "type": "short_answer", + "correct_answer": "10", + "answer_type": "exact", + "explanation": "WordPress REST API collection endpoints default to returning 10 items per page. This can be modified with the per_page query parameter, up to a maximum of 100.", + "references": ["https://developer.wordpress.org/reference/classes/wp_rest_controller/get_collection_params/"] + }, + { + "id": "k-rest-012", + "category": "rest-api", + "subcategory": "pagination", + "difficulty": "basic", + "prompt": "Which HTTP response headers does the WordPress REST API use to communicate pagination totals?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "X-Total-Count and X-Total-Pages" }, + { "key": "B", "text": "X-WP-Total and X-WP-TotalPages" }, + { "key": "C", "text": "X-Pagination-Total and X-Pagination-Pages" }, + { "key": "D", "text": "X-WP-Count and X-WP-Pages" } + ], + "correct_answer": "B", + "explanation": "WordPress uses X-WP-Total to indicate the total number of records and X-WP-TotalPages to indicate the total number of pages. These are custom headers prefixed with X-WP- to identify them as WordPress-specific.", + "references": ["https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/"] + }, + { + "id": "k-rest-013", + "category": "rest-api", + "subcategory": "pagination", + "difficulty": "hard", + "prompt": "What is the maximum value allowed for the per_page query parameter in standard WordPress REST API collection endpoints?", + "type": "short_answer", + "correct_answer": "100", + "answer_type": "exact", + "explanation": "The maximum value for per_page in WordPress REST API endpoints is 100. Requesting more than 100 results in a validation error. This limit is defined in the parameter schema in WP_REST_Controller::get_collection_params()." + }, + { + "id": "k-rest-014", + "category": "rest-api", + "subcategory": "special-parameters", + "difficulty": "basic", + "prompt": "What query parameter can be added to a WordPress REST API request to include linked resources (such as author and featured media) directly in the response?", + "type": "short_answer", + "correct_answer": "_embed", + "answer_type": "contains", + "explanation": "The _embed query parameter tells the REST API to embed linked resources (from _links) directly in the response, reducing the need for additional requests. For example, adding ?_embed to a posts request includes the author and featured media objects.", + "references": ["https://developer.wordpress.org/rest-api/using-the-rest-api/linking-and-embedding/"] + }, + { + "id": "k-rest-015", + "category": "rest-api", + "subcategory": "special-parameters", + "difficulty": "intermediate", + "prompt": "What does the _envelope query parameter do in the WordPress REST API?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Encrypts the response payload with a site-specific key" }, + { "key": "B", "text": "Wraps the response in a JSON object containing body, status, and headers keys" }, + { "key": "C", "text": "Compresses the response using gzip encoding" }, + { "key": "D", "text": "Adds a JSONP callback wrapper around the response" } + ], + "correct_answer": "B", + "explanation": "The _envelope parameter wraps the actual response inside a JSON envelope object with body, status, and headers keys. This is useful for environments where reading HTTP headers or status codes directly is not possible, such as some JSONP or restricted clients.", + "references": ["https://developer.wordpress.org/rest-api/using-the-rest-api/global-parameters/"] + }, + { + "id": "k-rest-016", + "category": "rest-api", + "subcategory": "request-response", + "difficulty": "intermediate", + "prompt": "What does the rest_ensure_response() function do?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Validates that a REST response contains no errors before sending" }, + { "key": "B", "text": "Converts any value into a WP_REST_Response object" }, + { "key": "C", "text": "Ensures the REST API server is initialized before dispatching" }, + { "key": "D", "text": "Checks that the response conforms to the endpoint's JSON schema" } + ], + "correct_answer": "B", + "explanation": "rest_ensure_response() takes any value and normalizes it into a WP_REST_Response object. If the value is already a WP_REST_Response, it returns it unchanged. If it is a WP_Error, it converts it to an error response. Otherwise, it wraps the value in a new WP_REST_Response.", + "references": ["https://developer.wordpress.org/reference/functions/rest_ensure_response/"] + }, + { + "id": "k-rest-017", + "category": "rest-api", + "subcategory": "request-response", + "difficulty": "hard", + "prompt": "When a WordPress REST API request includes parameters from multiple sources (JSON body, POST body, query string, URL), what is the priority order from highest to lowest for resolving parameter values?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "GET query string, POST body, JSON body, URL parameters, defaults" }, + { "key": "B", "text": "URL parameters, JSON body, POST body, GET query string, defaults" }, + { "key": "C", "text": "JSON body, POST body, GET query string, URL parameters, defaults" }, + { "key": "D", "text": "POST body, JSON body, GET query string, URL parameters, defaults" } + ], + "correct_answer": "C", + "explanation": "The parameter priority order in WP_REST_Request::get_parameter_order() is: JSON body (highest), POST body, GET query string, URL route parameters, and registered defaults (lowest). This means if the same parameter name exists in the JSON body and the query string, the JSON body value takes precedence." + }, + { + "id": "k-rest-018", + "category": "rest-api", + "subcategory": "authentication", + "difficulty": "basic", + "prompt": "Which of the following are the valid values for the context parameter in WordPress REST API requests?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "view, edit, admin" }, + { "key": "B", "text": "public, private, admin" }, + { "key": "C", "text": "view, edit, embed" }, + { "key": "D", "text": "read, write, admin" } + ], + "correct_answer": "C", + "explanation": "The three valid context values are 'view' (default, for public-facing data), 'edit' (for edit screens, requires appropriate capabilities), and 'embed' (minimal subset of fields for embedding). These control which fields and values are included in the response.", + "references": ["https://developer.wordpress.org/rest-api/using-the-rest-api/global-parameters/"] + }, + { + "id": "k-rest-019", + "category": "rest-api", + "subcategory": "authorization", + "difficulty": "intermediate", + "prompt": "What HTTP status code does rest_authorization_required_code() return when the user is logged in but lacks the required capability?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "401" }, + { "key": "B", "text": "403" }, + { "key": "C", "text": "405" }, + { "key": "D", "text": "422" } + ], + "correct_answer": "B", + "explanation": "rest_authorization_required_code() returns 403 (Forbidden) for logged-in users who lack the required capability, and 401 (Unauthorized) for logged-out users. This distinction helps clients differentiate between 'you need to log in' and 'you don't have permission'.", + "references": ["https://developer.wordpress.org/reference/functions/rest_authorization_required_code/"] + }, + { + "id": "k-rest-020", + "category": "rest-api", + "subcategory": "authentication", + "difficulty": "hard", + "prompt": "Which filter hook allows you to implement custom authentication for the WordPress REST API, and what must it return to indicate an authentication error?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "rest_authentication_errors — return a WP_Error object" }, + { "key": "B", "text": "rest_auth_check — return false" }, + { "key": "C", "text": "rest_cookie_check_errors — return an error code string" }, + { "key": "D", "text": "authenticate — return null" } + ], + "correct_answer": "A", + "explanation": "The rest_authentication_errors filter is the designated hook for implementing custom REST API authentication. Returning a WP_Error object from this filter short-circuits the request with that error. Returning null passes through to allow other auth methods, and returning true indicates successful authentication.", + "references": ["https://developer.wordpress.org/reference/hooks/rest_authentication_errors/"] + }, + { + "id": "k-rest-021", + "category": "rest-api", + "subcategory": "schema-validation", + "difficulty": "intermediate", + "prompt": "Which JSON Schema specification version does the WordPress REST API use for its endpoint schema definitions?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "JSON Schema Draft 7" }, + { "key": "B", "text": "JSON Schema Draft 4" }, + { "key": "C", "text": "JSON Schema Draft 6" }, + { "key": "D", "text": "OpenAPI 3.0 Schema" } + ], + "correct_answer": "B", + "explanation": "The WordPress REST API uses JSON Schema Draft 4 for defining endpoint schemas. All core controllers reference 'http://json-schema.org/draft-04/schema#' in their schema definitions. This determines which schema keywords and validation features are available.", + "references": ["https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/"] + }, + { + "id": "k-rest-022", + "category": "rest-api", + "subcategory": "schema-validation", + "difficulty": "hard", + "prompt": "What characters are allowed in a JSONP callback function name by the WordPress REST API's validation?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Alphanumeric characters, underscores, hyphens, and periods" }, + { "key": "B", "text": "Word characters (alphanumeric plus underscore) and periods only" }, + { "key": "C", "text": "Any characters except parentheses and semicolons" }, + { "key": "D", "text": "Alphanumeric characters and underscores only (no periods)" } + ], + "correct_answer": "B", + "explanation": "The wp_check_jsonp_callback() function uses the regex /[^\\w\\.]/ to reject invalid characters. This means only word characters (a-z, A-Z, 0-9, underscore) and periods are permitted. Periods are allowed to support namespaced callbacks like jQuery.callback.", + "references": ["https://developer.wordpress.org/reference/functions/wp_check_jsonp_callback/"] + }, + { + "id": "k-rest-023", + "category": "rest-api", + "subcategory": "batch-processing", + "difficulty": "intermediate", + "prompt": "What is the default maximum number of requests that can be included in a single WordPress REST API batch request?", + "type": "short_answer", + "correct_answer": "25", + "answer_type": "exact", + "explanation": "The default maximum batch size is 25 requests. This limit is filterable via the rest_get_max_batch_size filter. The batch endpoint is available at POST /wp/batch/v1.", + "references": ["https://developer.wordpress.org/reference/classes/wp_rest_server/serve_batch_request_v1/"] + }, + { + "id": "k-rest-024", + "category": "rest-api", + "subcategory": "batch-processing", + "difficulty": "hard", + "prompt": "How must a REST API route opt in to being callable via the WordPress batch processing endpoint?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Add 'batch' => true to the route registration args" }, + { "key": "B", "text": "Add 'allow_batch' => array( 'v1' => true ) to the route registration args" }, + { "key": "C", "text": "Register the route with WP_REST_Batch_Controller as the base class" }, + { "key": "D", "text": "Add 'batchable' => true to the route registration args" } + ], + "correct_answer": "B", + "explanation": "Routes must explicitly opt in to batch processing by including 'allow_batch' => array( 'v1' => true ) in their route registration arguments. The versioned array structure allows for future batch API versions.", + "references": ["https://developer.wordpress.org/reference/functions/register_rest_route/"] + }, + { + "id": "k-rest-025", + "category": "rest-api", + "subcategory": "embedding-hypermedia", + "difficulty": "intermediate", + "prompt": "What is the CURIE (Compact URI) namespace prefix used in WordPress REST API _links for custom relation types like 'featuredmedia' and 'attachment'?", + "type": "short_answer", + "correct_answer": "wp", + "answer_type": "exact", + "explanation": "WordPress uses the 'wp' CURIE namespace for its custom link relation types. So 'wp:featuredmedia' expands to 'https://api.w.org/featuredmedia'. This follows the HAL (Hypertext Application Language) style used in REST API responses.", + "references": ["https://developer.wordpress.org/rest-api/using-the-rest-api/linking-and-embedding/"] + }, + { + "id": "k-rest-026", + "category": "rest-api", + "subcategory": "embedding-hypermedia", + "difficulty": "hard", + "prompt": "WordPress REST API responses include _links using a hypermedia format. What format standard do these links follow, and what URL template is used for the 'wp' CURIE?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "JSON:API format with template https://wordpress.org/rest/{rel}" }, + { "key": "B", "text": "HAL (Hypertext Application Language) with template https://api.w.org/{rel}" }, + { "key": "C", "text": "Siren hypermedia format with template https://wp.org/api/{rel}" }, + { "key": "D", "text": "Collection+JSON format with template https://api.wordpress.org/{rel}" } + ], + "correct_answer": "B", + "explanation": "WordPress REST API responses use HAL-style _links with CURIEs. The 'wp' CURIE maps to the template 'https://api.w.org/{rel}', where {rel} is replaced with the specific relation type. The api.w.org domain is the canonical short domain for WordPress API relation types.", + "references": ["https://developer.wordpress.org/rest-api/using-the-rest-api/linking-and-embedding/"] + }, + { + "id": "k-rest-027", + "category": "rest-api", + "subcategory": "hooks", + "difficulty": "basic", + "prompt": "Which action hook fires when the WordPress REST API server is initialized and ready for route registration?", + "type": "short_answer", + "correct_answer": "rest_api_init", + "answer_type": "contains", + "explanation": "The rest_api_init action fires when the REST API server is initialized. This is the proper hook to use for registering custom REST routes and performing other REST API setup.", + "references": ["https://developer.wordpress.org/reference/hooks/rest_api_init/"] + }, + { + "id": "k-rest-028", + "category": "rest-api", + "subcategory": "hooks", + "difficulty": "intermediate", + "prompt": "Which filter hook fires AFTER the REST API route callback has executed but BEFORE the response is sent to the client, allowing you to modify the response object?", + "type": "short_answer", + "correct_answer": "rest_post_dispatch", + "answer_type": "contains", + "explanation": "The rest_post_dispatch filter fires after the REST API callback has executed, providing the response object for modification before it is served to the client. rest_pre_dispatch fires before route matching, and rest_pre_serve_request fires at the very end before the HTTP output is sent.", + "references": ["https://developer.wordpress.org/reference/hooks/rest_post_dispatch/"] + }, + { + "id": "k-rest-029", + "category": "rest-api", + "subcategory": "hooks", + "difficulty": "hard", + "prompt": "What is the dynamic action hook name that fires after a post is created or updated via the REST API, where {$this->post_type} is replaced by the post type slug?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "rest_after_save_{post_type}" }, + { "key": "B", "text": "rest_insert_{post_type}" }, + { "key": "C", "text": "rest_update_{post_type}" }, + { "key": "D", "text": "rest_save_{post_type}" } + ], + "correct_answer": "B", + "explanation": "The rest_insert_{post_type} action fires after a post is created or updated via the REST API. For example, rest_insert_post fires for standard posts and rest_insert_page fires for pages. Despite the name containing 'insert', it fires for both creation and updates — the third parameter ($creating) indicates which operation occurred.", + "references": ["https://developer.wordpress.org/reference/hooks/rest_insert_this-post_type/"] + }, + { + "id": "k-rest-030", + "category": "rest-api", + "subcategory": "error-handling", + "difficulty": "intermediate", + "prompt": "What is the structure of a standard WordPress REST API error response body?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "{\"error\": true, \"error_message\": \"...\", \"error_code\": 400}" }, + { "key": "B", "text": "{\"code\": \"error_code\", \"message\": \"...\", \"data\": {\"status\": 400}}" }, + { "key": "C", "text": "{\"status\": \"error\", \"errors\": [{\"code\": \"...\", \"detail\": \"...\"}]}" }, + { "key": "D", "text": "{\"success\": false, \"message\": \"...\", \"statusCode\": 400}" } + ], + "correct_answer": "B", + "explanation": "WordPress REST API error responses follow the format {\"code\": \"error_code\", \"message\": \"Human-readable message\", \"data\": {\"status\": 400}} where 'code' is a machine-readable error identifier, 'message' is human-readable, and 'data.status' contains the HTTP status code.", + "references": ["https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/roles-caps.json b/datasets/suites/wp-core-v1/knowledge/roles-caps.json index 171ccae..5ffb59e 100644 --- a/datasets/suites/wp-core-v1/knowledge/roles-caps.json +++ b/datasets/suites/wp-core-v1/knowledge/roles-caps.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-roles_caps", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Roles & Capabilities", "description": "Tests knowledge of WordPress roles and capabilities", @@ -11,6 +11,7 @@ { "id": "k-roles-001", "category": "roles-caps", + "subcategory": "capabilities", "difficulty": "intermediate", "prompt": "What capability is required by default to install plugins in WordPress?", "type": "multiple_choice", @@ -21,7 +22,221 @@ { "key": "D", "text": "edit_plugins" } ], "correct_answer": "B", - "explanation": "The 'install_plugins' capability controls who can install new plugins." + "explanation": "The 'install_plugins' capability controls who can install new plugins. It is distinct from activate_plugins (activating/deactivating) and update_plugins (updating existing plugins).", + "references": ["https://developer.wordpress.org/reference/functions/map_meta_cap/"] + }, + { + "id": "k-roles-002", + "category": "roles-caps", + "subcategory": "roles", + "difficulty": "basic", + "prompt": "How many default roles does WordPress ship with?", + "type": "short_answer", + "correct_answer": "5", + "answer_type": "exact", + "explanation": "WordPress ships with 5 default roles: Administrator, Editor, Author, Contributor, and Subscriber.", + "references": ["https://developer.wordpress.org/plugins/users/roles-and-capabilities/"] + }, + { + "id": "k-roles-003", + "category": "roles-caps", + "subcategory": "roles", + "difficulty": "basic", + "prompt": "Which capability does a Contributor lack that prevents them from making their posts visible to the public?", + "type": "short_answer", + "correct_answer": "publish_posts", + "answer_type": "contains", + "explanation": "Contributors can edit_posts and delete_posts (their own drafts), but cannot publish_posts. They also lack upload_files and edit_published_posts. Their posts must be published by an Editor or Administrator.", + "references": ["https://developer.wordpress.org/plugins/users/roles-and-capabilities/"] + }, + { + "id": "k-roles-004", + "category": "roles-caps", + "subcategory": "roles", + "difficulty": "intermediate", + "prompt": "Which is the lowest role that has the upload_files capability by default?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Subscriber" }, + { "key": "B", "text": "Contributor" }, + { "key": "C", "text": "Author" }, + { "key": "D", "text": "Editor" } + ], + "correct_answer": "C", + "explanation": "The upload_files capability is granted to Author, Editor, and Administrator. Contributors and Subscribers cannot upload files by default.", + "references": ["https://developer.wordpress.org/plugins/users/roles-and-capabilities/"] + }, + { + "id": "k-roles-005", + "category": "roles-caps", + "subcategory": "meta_caps", + "difficulty": "intermediate", + "prompt": "What is the difference between primitive capabilities and meta capabilities in WordPress?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Primitive caps are stored in roles; meta caps are context-dependent and mapped to primitives via map_meta_cap()" }, + { "key": "B", "text": "Primitive caps apply to posts; meta caps apply to users" }, + { "key": "C", "text": "Primitive caps are built-in; meta caps are created by plugins" }, + { "key": "D", "text": "Primitive caps are checked server-side; meta caps are checked client-side" } + ], + "correct_answer": "A", + "explanation": "Primitive capabilities (like edit_posts, edit_others_posts) are stored directly in role definitions. Meta capabilities (like edit_post, delete_post) are abstract/contextual and require an object ID — map_meta_cap() translates them into the required primitive capabilities based on context.", + "references": ["https://developer.wordpress.org/reference/functions/map_meta_cap/"] + }, + { + "id": "k-roles-006", + "category": "roles-caps", + "subcategory": "meta_caps", + "difficulty": "hard", + "prompt": "When map_meta_cap() processes 'edit_user' and the user is editing their own profile, what capabilities are required?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "edit_users" }, + { "key": "B", "text": "No additional capabilities — any logged-in user can edit their own profile" }, + { "key": "C", "text": "manage_options" }, + { "key": "D", "text": "read and edit_posts" } + ], + "correct_answer": "B", + "explanation": "When $user_id === $args[0] (editing yourself), map_meta_cap() returns an empty $caps array, meaning no special capability is needed. Any authenticated user can edit their own profile.", + "references": ["https://developer.wordpress.org/reference/functions/map_meta_cap/"] + }, + { + "id": "k-roles-007", + "category": "roles-caps", + "subcategory": "meta_caps", + "difficulty": "hard", + "prompt": "What happens when map_meta_cap() returns 'do_not_allow' in the required capabilities array?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "The check still passes for super admins on multisite" }, + { "key": "B", "text": "The capability check always fails, even for super admins — it is explicitly unset from all users' capabilities" }, + { "key": "C", "text": "A _doing_it_wrong notice is triggered" }, + { "key": "D", "text": "The check is deferred to the user_has_cap filter" } + ], + "correct_answer": "B", + "explanation": "do_not_allow is the ultimate denial. In WP_User::has_cap(), it is explicitly unset from $capabilities. Even super admins on multisite fail the check — the code returns false before granting all-caps if do_not_allow is in the required caps.", + "references": ["https://developer.wordpress.org/reference/classes/wp_user/has_cap/"] + }, + { + "id": "k-roles-008", + "category": "roles-caps", + "subcategory": "capabilities", + "difficulty": "hard", + "prompt": "When DISALLOW_UNFILTERED_HTML is defined as true, what happens to the 'unfiltered_html' capability?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It is removed from the Administrator role only" }, + { "key": "B", "text": "It is mapped to do_not_allow for ALL users, including administrators and super admins" }, + { "key": "C", "text": "It only affects non-super-admin users on multisite" }, + { "key": "D", "text": "It triggers a deprecation warning but still allows the capability" } + ], + "correct_answer": "B", + "explanation": "When DISALLOW_UNFILTERED_HTML is defined and true, map_meta_cap() maps unfiltered_html to do_not_allow for everyone. This is stronger than any role check — even super admins cannot bypass it.", + "references": ["https://developer.wordpress.org/reference/functions/map_meta_cap/"] + }, + { + "id": "k-roles-009", + "category": "roles-caps", + "subcategory": "multisite", + "difficulty": "hard", + "prompt": "On a single-site WordPress installation, is_super_admin() returns true based on which capability?", + "type": "short_answer", + "correct_answer": "delete_users", + "answer_type": "contains", + "explanation": "On single-site, is_super_admin() checks if the user has_cap('delete_users'). Since only Administrators have this capability by default, all Administrators are effectively super admins on single-site.", + "references": ["https://developer.wordpress.org/reference/functions/is_super_admin/"] + }, + { + "id": "k-roles-010", + "category": "roles-caps", + "subcategory": "roles", + "difficulty": "basic", + "prompt": "What is the default role assigned to new users who register on a WordPress site?", + "type": "short_answer", + "correct_answer": "subscriber", + "answer_type": "contains", + "explanation": "The default role for new user registrations is Subscriber. This can be changed in Settings > General under 'New User Default Role'.", + "references": ["https://developer.wordpress.org/plugins/users/roles-and-capabilities/"] + }, + { + "id": "k-roles-011", + "category": "roles-caps", + "subcategory": "storage", + "difficulty": "intermediate", + "prompt": "Where are WordPress roles and their capabilities stored in the database?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "In a dedicated wp_roles table" }, + { "key": "B", "text": "In the wp_options table under the {prefix}user_roles option key" }, + { "key": "C", "text": "In the wp_usermeta table for each user" }, + { "key": "D", "text": "In wp-config.php as a defined constant" } + ], + "correct_answer": "B", + "explanation": "Roles are stored as a serialized array in the wp_options table under the key {$table_prefix}user_roles (e.g., wp_user_roles). Each role entry contains a display name and capabilities array.", + "references": ["https://developer.wordpress.org/reference/classes/wp_roles/"] + }, + { + "id": "k-roles-012", + "category": "roles-caps", + "subcategory": "capabilities", + "difficulty": "intermediate", + "prompt": "Which capability is the primary gatekeeper for the WordPress Settings pages?", + "type": "short_answer", + "correct_answer": "manage_options", + "answer_type": "contains", + "explanation": "manage_options is the capability that controls access to the Settings admin pages. Only Administrators have this capability by default.", + "references": ["https://developer.wordpress.org/plugins/users/roles-and-capabilities/"] + }, + { + "id": "k-roles-013", + "category": "roles-caps", + "subcategory": "capabilities", + "difficulty": "intermediate", + "prompt": "The 'exist' capability has special behavior in WP_User::has_cap(). What is it?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It returns true only for users who exist in the database" }, + { "key": "B", "text": "It is hardcoded to true for all users, regardless of their roles" }, + { "key": "C", "text": "It checks if the user session is still active" }, + { "key": "D", "text": "It is an alias for the 'read' capability" } + ], + "correct_answer": "B", + "explanation": "In WP_User::has_cap(), the 'exist' capability is hardcoded: $capabilities['exist'] = true. This means any capability check for 'exist' always passes, for every user.", + "references": ["https://developer.wordpress.org/reference/classes/wp_user/has_cap/"] + }, + { + "id": "k-roles-014", + "category": "roles-caps", + "subcategory": "roles", + "difficulty": "basic", + "prompt": "What capabilities does the Subscriber role have by default?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "read and edit_posts" }, + { "key": "B", "text": "read only" }, + { "key": "C", "text": "read, edit_posts, and upload_files" }, + { "key": "D", "text": "No capabilities at all" } + ], + "correct_answer": "B", + "explanation": "Subscribers only have the 'read' capability (and the legacy level_0). They cannot create, edit, or delete any content.", + "references": ["https://developer.wordpress.org/plugins/users/roles-and-capabilities/"] + }, + { + "id": "k-roles-015", + "category": "roles-caps", + "subcategory": "meta_caps", + "difficulty": "hard", + "prompt": "The user_has_cap filter allows plugins to dynamically grant capabilities. What arguments does it receive?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "$allcaps (all user capabilities), $caps (required primitives), $args (cap name, user ID, object ID), $user (WP_User)" }, + { "key": "B", "text": "$cap (capability name), $user_id, $grant (boolean)" }, + { "key": "C", "text": "$user (WP_User), $cap (capability name), $result (boolean)" }, + { "key": "D", "text": "$allcaps (all user capabilities), $cap (single capability name), $user_id" } + ], + "correct_answer": "A", + "explanation": "The user_has_cap filter passes: $allcaps (full capabilities array), $caps (required primitive caps from map_meta_cap), $args (array with original cap, user ID, and optional object ID), and $user (WP_User object since 3.7.0).", + "references": ["https://developer.wordpress.org/reference/hooks/user_has_cap/"] } ] } diff --git a/datasets/suites/wp-core-v1/knowledge/security.json b/datasets/suites/wp-core-v1/knowledge/security.json index 475e97b..950fcee 100644 --- a/datasets/suites/wp-core-v1/knowledge/security.json +++ b/datasets/suites/wp-core-v1/knowledge/security.json @@ -1,6 +1,6 @@ { "id": "wp-core-knowledge-v1-security", - "version": "1.1.0", + "version": "1.2.0", "metadata": { "name": "WordPress Core Knowledge - Security", "description": "Tests knowledge of WordPress security practices", @@ -11,6 +11,7 @@ { "id": "k-security-001", "category": "security", + "subcategory": "escaping", "difficulty": "basic", "prompt": "Which function should be used to escape HTML output in WordPress?", "type": "short_answer", @@ -21,7 +22,8 @@ { "id": "k-security-002", "category": "security", - "difficulty": "intermediate", + "subcategory": "nonces", + "difficulty": "basic", "prompt": "What function should be used to verify a nonce in WordPress?", "type": "multiple_choice", "choices": [ @@ -33,6 +35,282 @@ "correct_answer": "B", "explanation": "wp_verify_nonce() is the WordPress function to verify a nonce value.", "references": ["https://developer.wordpress.org/reference/functions/wp_verify_nonce/"] + }, + { + "id": "k-security-003", + "category": "security", + "subcategory": "nonces", + "difficulty": "basic", + "prompt": "What is the default name of the hidden nonce field generated by wp_nonce_field()?", + "type": "short_answer", + "correct_answer": "_wpnonce", + "answer_type": "exact", + "explanation": "wp_nonce_field() defaults to the field name '_wpnonce'. It also outputs a _wp_http_referer hidden field by default when $referer is true.", + "references": ["https://developer.wordpress.org/reference/functions/wp_nonce_field/"] + }, + { + "id": "k-security-004", + "category": "security", + "subcategory": "escaping", + "difficulty": "intermediate", + "prompt": "What is the difference between esc_url() and esc_url_raw()?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "esc_url() encodes ampersands and quotes for HTML display; esc_url_raw() does not, making it suitable for database storage and redirects" }, + { "key": "B", "text": "esc_url_raw() skips protocol validation; esc_url() validates against allowed protocols" }, + { "key": "C", "text": "esc_url() only allows http/https; esc_url_raw() allows all protocols" }, + { "key": "D", "text": "esc_url_raw() is deprecated in favor of esc_url()" } + ], + "correct_answer": "A", + "explanation": "esc_url() uses 'display' context, encoding & as & and ' as ' for safe HTML output. esc_url_raw() uses 'db' context, skipping HTML encoding. Both validate protocols identically.", + "references": ["https://developer.wordpress.org/reference/functions/esc_url_raw/"] + }, + { + "id": "k-security-005", + "category": "security", + "subcategory": "nonces", + "difficulty": "intermediate", + "prompt": "What are the possible return values of wp_verify_nonce()?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "true or false" }, + { "key": "B", "text": "1 (generated 0-12 hours ago), 2 (generated 12-24 hours ago), or false (invalid)" }, + { "key": "C", "text": "The nonce string on success, null on failure" }, + { "key": "D", "text": "0 (expired), 1 (valid), or -1 (invalid)" } + ], + "correct_answer": "B", + "explanation": "wp_verify_nonce() returns 1 if the nonce was generated in the current tick (0-12 hours), 2 if from the previous tick (12-24 hours), or false if invalid. The two-tick system provides a grace period.", + "references": ["https://developer.wordpress.org/reference/functions/wp_verify_nonce/"] + }, + { + "id": "k-security-006", + "category": "security", + "subcategory": "nonces", + "difficulty": "intermediate", + "prompt": "What is the default nonce lifetime in WordPress, in seconds?", + "type": "short_answer", + "correct_answer": "86400", + "answer_type": "exact", + "explanation": "The default nonce lifetime is DAY_IN_SECONDS (86400 seconds / 24 hours). This can be changed via the 'nonce_life' filter. The lifetime is split into two ticks of 12 hours each.", + "references": ["https://developer.wordpress.org/reference/functions/wp_nonce_tick/"] + }, + { + "id": "k-security-007", + "category": "security", + "subcategory": "escaping", + "difficulty": "hard", + "prompt": "The esc_attr() function applies a filter named 'attribute_escape', not 'esc_attr'. What filter name does esc_url() use?", + "type": "short_answer", + "correct_answer": "clean_url", + "answer_type": "exact", + "explanation": "WordPress escaping functions use legacy filter names: esc_html uses 'esc_html', esc_attr uses 'attribute_escape', esc_url uses 'clean_url', and esc_js uses 'js_escape'. These names predate the esc_* function naming convention.", + "references": ["https://developer.wordpress.org/reference/functions/esc_url/"] + }, + { + "id": "k-security-008", + "category": "security", + "subcategory": "database", + "difficulty": "intermediate", + "prompt": "What format specifiers does $wpdb->prepare() support?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "%s (string), %d (integer), %f (float), %i (identifier wrapped in backticks)" }, + { "key": "B", "text": "%s (string), %d (integer), %f (float), %b (boolean)" }, + { "key": "C", "text": "%s (string), %d (integer), %f (float) only" }, + { "key": "D", "text": "%s (string), %d (integer), %f (float), %t (table name)" } + ], + "correct_answer": "A", + "explanation": "$wpdb->prepare() supports %s (string, auto-quoted), %d (integer), %f (float), and %i (identifier, wrapped in backticks for table/column names). Numbered placeholders like %1$s are also supported.", + "references": ["https://developer.wordpress.org/reference/classes/wpdb/prepare/"] + }, + { + "id": "k-security-009", + "category": "security", + "subcategory": "passwords", + "difficulty": "intermediate", + "prompt": "Since WordPress 6.8, what hashing algorithm does wp_hash_password() use by default?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "phpass portable hashing ($P$ prefix)" }, + { "key": "B", "text": "bcrypt via password_hash() ($wp prefix)" }, + { "key": "C", "text": "Argon2id via password_hash()" }, + { "key": "D", "text": "SHA-256 with a random salt" } + ], + "correct_answer": "B", + "explanation": "Since WordPress 6.8, wp_hash_password() uses bcrypt (PASSWORD_BCRYPT) with a $wp prefix. The password is pre-hashed with HMAC-SHA384 before bcrypt. Legacy $P$ (phpass) and MD5 hashes are still verified by wp_check_password().", + "references": ["https://developer.wordpress.org/reference/functions/wp_hash_password/"] + }, + { + "id": "k-security-010", + "category": "security", + "subcategory": "kses", + "difficulty": "basic", + "prompt": "What KSES context does wp_kses_post() use to determine allowed HTML tags?", + "type": "short_answer", + "correct_answer": "post", + "answer_type": "exact", + "explanation": "wp_kses_post() calls wp_kses($data, 'post'), which uses the $allowedposttags global. This allows a broad set of HTML tags suitable for post content but excludes script, style, iframe, and form elements.", + "references": ["https://developer.wordpress.org/reference/functions/wp_kses_post/"] + }, + { + "id": "k-security-011", + "category": "security", + "subcategory": "sanitization", + "difficulty": "intermediate", + "prompt": "What is the key difference between sanitize_text_field() and sanitize_textarea_field()?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "sanitize_textarea_field() preserves newlines; sanitize_text_field() strips them" }, + { "key": "B", "text": "sanitize_textarea_field() allows HTML; sanitize_text_field() strips all tags" }, + { "key": "C", "text": "sanitize_text_field() is for single-line inputs; sanitize_textarea_field() is for rich text" }, + { "key": "D", "text": "sanitize_textarea_field() runs wp_kses; sanitize_text_field() does not" } + ], + "correct_answer": "A", + "explanation": "Both functions strip tags and invalid UTF-8, but sanitize_text_field() also replaces newlines, carriage returns, and tabs with spaces. sanitize_textarea_field() preserves newlines, making it suitable for multi-line inputs.", + "references": ["https://developer.wordpress.org/reference/functions/sanitize_textarea_field/"] + }, + { + "id": "k-security-012", + "category": "security", + "subcategory": "validation", + "difficulty": "intermediate", + "prompt": "What does wp_check_filetype() validate — the file's content or its extension?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "It checks file extension only, not actual file content" }, + { "key": "B", "text": "It reads file headers (magic bytes) to determine the real MIME type" }, + { "key": "C", "text": "It checks both extension and content via finfo" }, + { "key": "D", "text": "It validates against a checksum of known safe files" } + ], + "correct_answer": "A", + "explanation": "wp_check_filetype() only checks the file extension against allowed MIME types. For content-based validation, use wp_check_filetype_and_ext() which uses getimagesize() for images and finfo for other files.", + "references": ["https://developer.wordpress.org/reference/functions/wp_check_filetype/"] + }, + { + "id": "k-security-013", + "category": "security", + "subcategory": "redirects", + "difficulty": "intermediate", + "prompt": "What is the key difference between wp_redirect() and wp_safe_redirect()?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "wp_safe_redirect() validates the redirect URL against allowed hosts; wp_redirect() does not" }, + { "key": "B", "text": "wp_safe_redirect() calls exit automatically; wp_redirect() does not" }, + { "key": "C", "text": "wp_safe_redirect() only works for internal URLs; wp_redirect() supports external URLs" }, + { "key": "D", "text": "wp_safe_redirect() uses HTTPS only; wp_redirect() allows HTTP" } + ], + "correct_answer": "A", + "explanation": "wp_safe_redirect() calls wp_validate_redirect() which checks that the destination is an allowed host (same-site by default). If not safe, it falls back to admin_url(). Neither function calls exit — callers must do so explicitly.", + "references": ["https://developer.wordpress.org/reference/functions/wp_safe_redirect/"] + }, + { + "id": "k-security-014", + "category": "security", + "subcategory": "validation", + "difficulty": "hard", + "prompt": "What does is_email() return on success — true or the email string?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "true on success, false on failure" }, + { "key": "B", "text": "The validated email string on success, false on failure" }, + { "key": "C", "text": "A WP_Error on failure, true on success" }, + { "key": "D", "text": "1 on success, 0 on failure" } + ], + "correct_answer": "B", + "explanation": "is_email() returns the email address string on success and false on failure. This is a common source of confusion — many developers assume it returns a boolean.", + "references": ["https://developer.wordpress.org/reference/functions/is_email/"] + }, + { + "id": "k-security-015", + "category": "security", + "subcategory": "nonces", + "difficulty": "hard", + "prompt": "How many characters long is a WordPress nonce?", + "type": "short_answer", + "correct_answer": "10", + "answer_type": "exact", + "explanation": "wp_create_nonce() generates a 10-character nonce by taking substr(wp_hash(...), -12, 10). The hash input includes the tick, action, user ID, and session token.", + "references": ["https://developer.wordpress.org/reference/functions/wp_create_nonce/"] + }, + { + "id": "k-security-016", + "category": "security", + "subcategory": "passwords", + "difficulty": "hard", + "prompt": "What is the maximum password length before wp_hash_password() returns an invalid hash?", + "type": "short_answer", + "correct_answer": "4096", + "answer_type": "exact", + "explanation": "Passwords longer than 4096 characters cause wp_hash_password() to return '*', which is an invalid hash that will never verify. This prevents denial-of-service via extremely long passwords.", + "references": ["https://developer.wordpress.org/reference/functions/wp_hash_password/"] + }, + { + "id": "k-security-017", + "category": "security", + "subcategory": "kses", + "difficulty": "hard", + "prompt": "Which of these protocols is NOT included in wp_allowed_protocols() by default?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "javascript" }, + { "key": "B", "text": "tel" }, + { "key": "C", "text": "webcal" }, + { "key": "D", "text": "xmpp" } + ], + "correct_answer": "A", + "explanation": "The 'javascript' protocol is deliberately excluded from wp_allowed_protocols() to prevent XSS attacks. The default list includes protocols like http, https, ftp, mailto, tel, webcal, xmpp, svn, sms, and others. The list is filterable via the 'kses_allowed_protocols' filter.", + "references": ["https://developer.wordpress.org/reference/functions/wp_allowed_protocols/"] + }, + { + "id": "k-security-018", + "category": "security", + "subcategory": "nonces", + "difficulty": "intermediate", + "prompt": "When check_ajax_referer() is called without specifying a $query_arg, which field names does it check in $_REQUEST?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "_wpnonce only" }, + { "key": "B", "text": "_ajax_nonce first, then _wpnonce as fallback" }, + { "key": "C", "text": "nonce first, then _wpnonce as fallback" }, + { "key": "D", "text": "It requires $query_arg to be specified" } + ], + "correct_answer": "B", + "explanation": "check_ajax_referer() with $query_arg = false checks $_REQUEST['_ajax_nonce'] first, then falls back to $_REQUEST['_wpnonce']. If both are missing or invalid and $stop is true (default), it calls wp_die(-1, 403).", + "references": ["https://developer.wordpress.org/reference/functions/check_ajax_referer/"] + }, + { + "id": "k-security-019", + "category": "security", + "subcategory": "sanitization", + "difficulty": "intermediate", + "prompt": "What characters does sanitize_key() allow?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "Lowercase letters, digits, underscores, and hyphens only" }, + { "key": "B", "text": "Letters (both cases), digits, underscores, and hyphens" }, + { "key": "C", "text": "Lowercase letters, digits, and underscores only" }, + { "key": "D", "text": "Any alphanumeric characters plus periods and underscores" } + ], + "correct_answer": "A", + "explanation": "sanitize_key() lowercases the input first, then strips everything except [a-z0-9_\\-]. The result is always lowercase with only letters, digits, underscores, and hyphens.", + "references": ["https://developer.wordpress.org/reference/functions/sanitize_key/"] + }, + { + "id": "k-security-020", + "category": "security", + "subcategory": "kses", + "difficulty": "hard", + "prompt": "The default KSES context (used for comment text and similar) allows a very restricted set of HTML tags. Which of these is NOT in the $allowedtags list?", + "type": "multiple_choice", + "choices": [ + { "key": "A", "text": "" }, + { "key": "B", "text": "
" }, + { "key": "C", "text": "" }, + { "key": "D", "text": "" } + ], + "correct_answer": "C", + "explanation": "The $allowedtags (default KSES context) only includes: a, abbr, acronym, b, blockquote, cite, code, del, em, i, q, s, strike, strong. The tag is NOT allowed in the default context — it requires the 'post' context.", + "references": ["https://developer.wordpress.org/reference/functions/wp_kses_allowed_html/"] } ] } diff --git a/python/wp_bench/cli.py b/python/wp_bench/cli.py index 7612c95..61596bd 100644 --- a/python/wp_bench/cli.py +++ b/python/wp_bench/cli.py @@ -25,6 +25,7 @@ def run( suite: Optional[str] = typer.Option(None, help="Override suite name"), model_name: Optional[str] = typer.Option(None, help="Override model name (single model mode)"), limit: Optional[int] = typer.Option(None, help="Limit number of tests"), + test_type: Optional[str] = typer.Option(None, help="Run only 'knowledge' or 'execution' tests"), ) -> None: """Run the benchmark end-to-end.""" harness_config = HarnessConfig.from_file(config) if config else HarnessConfig() @@ -33,6 +34,11 @@ def run( harness_config.dataset.name = suite if "/" not in suite else suite if limit is not None: harness_config.run.limit = limit + if test_type is not None: + if test_type not in ("knowledge", "execution"): + console.print(f"[red]Invalid --test-type: {test_type}. Must be 'knowledge' or 'execution'.[/red]") + raise typer.Exit(1) + harness_config.run.test_type = test_type # type: ignore[assignment] # Check if multi-model mode models = harness_config.get_models() @@ -58,13 +64,21 @@ def run( @app.command() -def dry_run(config: Optional[Path] = typer.Option(None, help="Config file")) -> None: +def dry_run( + config: Optional[Path] = typer.Option(None, help="Config file"), + test_type: Optional[str] = typer.Option(None, help="Filter to 'knowledge' or 'execution' tests"), +) -> None: """Load dataset and render prompt statistics without hitting models.""" harness_config = HarnessConfig.from_file(config) if config else HarnessConfig() tests = load_tests(harness_config.dataset) - console.print( - f"Execution tests: {len(tests['execution'])}, Knowledge tests: {len(tests['knowledge'])}" - ) + if test_type == "knowledge": + console.print(f"Knowledge tests: {len(tests['knowledge'])}") + elif test_type == "execution": + console.print(f"Execution tests: {len(tests['execution'])}") + else: + console.print( + f"Execution tests: {len(tests['execution'])}, Knowledge tests: {len(tests['knowledge'])}" + ) if __name__ == "__main__": # pragma: no cover diff --git a/python/wp_bench/config.py b/python/wp_bench/config.py index 8e63144..bd2ecdc 100644 --- a/python/wp_bench/config.py +++ b/python/wp_bench/config.py @@ -2,7 +2,7 @@ from __future__ import annotations from pathlib import Path -from typing import List, Literal, Optional, Union +from typing import List, Literal, Optional from pydantic import BaseModel, Field, HttpUrl, validator @@ -51,6 +51,7 @@ class GraderConfig(BaseModel): class RunConfig(BaseModel): suite: str = "wp-core-v1" + test_type: Optional[Literal["knowledge", "execution"]] = None limit: Optional[int] = None seed: int = 1337 concurrency: int = 5 diff --git a/python/wp_bench/core.py b/python/wp_bench/core.py index ca7d95a..ea8fee6 100644 --- a/python/wp_bench/core.py +++ b/python/wp_bench/core.py @@ -76,10 +76,16 @@ def run(self) -> Dict[str, Any]: SystemExit: If a test fails, prints error details and exits with code 1. """ tests = load_tests(self.config.dataset) - self.environment.setup() + test_type = self.config.run.test_type + run_knowledge = test_type in (None, "knowledge") + run_execution = test_type in (None, "execution") + if run_execution: + self.environment.setup() try: - self._run_knowledge_tests(tests["knowledge"]) - self._run_execution_tests(tests["execution"]) + if run_knowledge: + self._run_knowledge_tests(tests["knowledge"]) + if run_execution: + self._run_execution_tests(tests["execution"]) except TestError as e: print_test_error(e) raise SystemExit(1) from e @@ -315,7 +321,8 @@ def run(self) -> Dict[str, Any]: """ models = self.config.get_models() tests = load_tests(self.config.dataset) - self.environment.setup() + if self.config.run.test_type != "knowledge": + self.environment.setup() try: for model_config in models: @@ -401,8 +408,11 @@ def run(self) -> Dict[str, Any]: Returns: Dict with model config, aggregate scores, and individual results. """ - self._run_knowledge_tests(self.tests["knowledge"]) - self._run_execution_tests(self.tests["execution"]) + test_type = self.config.run.test_type + if test_type in (None, "knowledge"): + self._run_knowledge_tests(self.tests["knowledge"]) + if test_type in (None, "execution"): + self._run_execution_tests(self.tests["execution"]) summary = self.aggregator.finalize() return { "model_config": self.model_config.model_dump(mode="json"), diff --git a/python/wp_bench/output.py b/python/wp_bench/output.py index 05562c3..63ef9b0 100644 --- a/python/wp_bench/output.py +++ b/python/wp_bench/output.py @@ -85,13 +85,16 @@ def print_comparison_table(results: Dict[str, Dict[str, Any]]) -> None: table.add_column("Quality", justify="right") table.add_column("Overall", justify="right", style="bold") + def _fmt_score(value: float | None) -> str: + return f"{value*100:.1f}%" if value is not None else "N/A" + for model_name, result in results.items(): scores = result["scores"] table.add_row( model_name, - f"{scores['knowledge']*100:.1f}%", - f"{scores['correctness']*100:.1f}%", - f"{scores['quality']*100:.1f}%" if scores["quality"] else "N/A", + _fmt_score(scores["knowledge"]), + _fmt_score(scores["correctness"]), + _fmt_score(scores["quality"]), f"{scores['overall']*100:.1f}%", ) diff --git a/python/wp_bench/scoring.py b/python/wp_bench/scoring.py index 18c292f..6ef2759 100644 --- a/python/wp_bench/scoring.py +++ b/python/wp_bench/scoring.py @@ -3,23 +3,25 @@ from dataclasses import dataclass, field from statistics import mean -from typing import Dict, List +from typing import Dict, List, Optional @dataclass class ScoreBreakdown: - knowledge: float = 0.0 - correctness: float = 0.0 - quality: float = 0.0 + knowledge: Optional[float] = None + correctness: Optional[float] = None + quality: Optional[float] = None weights: Dict[str, float] = field( default_factory=lambda: {"knowledge": 0.3, "correctness": 0.4, "quality": 0.3} ) def overall(self) -> float: - total = 0.0 - for key, weight in self.weights.items(): - total += getattr(self, key, 0.0) * weight - return round(total, 4) + active = {k: w for k, w in self.weights.items() if getattr(self, k) is not None} + if not active: + return 0.0 + total_weight = sum(active.values()) + total = sum(getattr(self, k) * w for k, w in active.items()) + return round(total / total_weight, 4) class ScoreAggregator: @@ -44,6 +46,4 @@ def finalize(self) -> ScoreBreakdown: breakdown.correctness = mean(self.correctness_scores) if self.quality_scores: breakdown.quality = mean(self.quality_scores) - else: - breakdown.quality = 0.0 return breakdown diff --git a/runtime/package-lock.json b/runtime/package-lock.json index a7db924..5e96659 100644 --- a/runtime/package-lock.json +++ b/runtime/package-lock.json @@ -554,6 +554,7 @@ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" }