const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); // A fresh WordPress install leaves `permalink_structure` empty, so WP_Rewrite is snapshotted // with no post/page rules and every single post resolves to 404 until someone re-saves // Settings → Permalinks. RewriteBootstrap must refresh that snapshot early enough to matter. test('permalink structure is bootstrapped before the main query is parsed', () => { const bootstrap = read('src/Core/RewriteBootstrap.php'); const functions = read('functions.php'); assert.match(bootstrap, /namespace Jio\\Core;/); assert.match(bootstrap, /class RewriteBootstrap/); assert.match(bootstrap, /const DEFAULT_STRUCTURE = '\/%postname%\/';/); // The hook that makes the current request correct: after_setup_theme still runs inside // wp-settings.php, before wp-blog-header.php parses the query. assert.match(bootstrap, /add_action\('after_setup_theme', \[\$this, 'ensure_permalink_structure'\], 1\)/); assert.match(bootstrap, /add_action\('after_switch_theme', \[\$this, 'flush_on_activate'\]\)/); assert.match(bootstrap, /add_action\('wp_loaded', \[\$this, 'flush_when_version_changes'\], 20\)/); // set_permalink_structure is what wp-admin/options-permalink.php uses: it persists the // option AND refreshes the in-memory WP_Rewrite state for the current request. assert.match(bootstrap, /\$wp_rewrite->set_permalink_structure\(self::DEFAULT_STRUCTURE\)/); assert.match(bootstrap, /\$wp_rewrite instanceof \\WP_Rewrite/); // Version bumps must not land on `init`: that runs before every add_rewrite_rule() // callback, and core defers pre-wp_loaded flushes anyway. assert.doesNotMatch(bootstrap, /add_action\('init'/); assert.match(bootstrap, /get_option\('jio_rewrite_flushed_version'/); // A structure the administrator chose is never overridden. const ensure = bootstrap.slice( bootstrap.indexOf('public function ensure_permalink_structure'), bootstrap.indexOf('public function flush_on_activate'), ); assert.match(ensure, /get_option\('permalink_structure'\)[\s\S]*?!== ''[\s\S]*?return;/); }); test('functions.php delegates rewrite flushing to RewriteBootstrap', () => { const functions = read('functions.php'); assert.match(functions, /\\Jio\\Core\\RewriteBootstrap::get_instance\(\)->init\(\);/); // The old duplicated sites are gone. assert.doesNotMatch(functions, /delete_option\('rewrite_rules'\)/); assert.doesNotMatch(functions, /jio_flushed_version/); assert.doesNotMatch(functions, /add_action\('after_switch_theme', function \(\) \{\s*flush_rewrite_rules\(\);/); // RewriteBootstrap must be initialized before Setup, whose init registers the rewrite // callbacks on the same request. assert.ok( functions.indexOf('\\Jio\\Core\\RewriteBootstrap::get_instance()->init();') < functions.indexOf('\\Jio\\Core\\Setup::get_instance()->init();'), 'RewriteBootstrap must initialize before Setup', ); }); test('user routing no longer writes permalink_structure itself', () => { const routing = read('inc/modules/user-parts/routing.php'); // Single owner: the late init-time write is what made the first request 404 while leaving // a non-empty but broken rewrite_rules option behind. (The read-only author_link fallback // below is a separate, legitimate query-arg path.) assert.doesNotMatch(routing, /update_option\('permalink_structure'/); const flushCallback = routing.slice( routing.indexOf('// 统一 flush 检查'), routing.indexOf('if ($need_flush)'), ); assert.doesNotMatch(flushCallback, /permalink_structure/); // Its own message-routing version stamp stays — that is a separate self-heal concern. assert.match(routing, /message-routing:' \. jio_get_messages_rewrite_slug\(\)/); assert.match(routing, /flush_rewrite_rules\(true\)/); }); test('fresh install gets a usable structure, chosen structures are left alone', () => { const result = spawnSync(process.env.PHP_BINARY || 'php', [ path.join(__dirname, 'rewrite-bootstrap-harness.php'), ], { cwd: root, encoding: 'utf8' }); assert.equal(result.status, 0, result.stderr || result.stdout); const data = JSON.parse(result.stdout); // Hook wiring. assert.deepEqual(data.hooks, [ { hook: 'after_setup_theme', priority: 1, method: 'ensure_permalink_structure' }, { hook: 'after_switch_theme', priority: 10, method: 'flush_on_activate' }, { hook: 'wp_loaded', priority: 20, method: 'flush_when_version_changes' }, ]); // Fresh install: option written AND the in-memory WP_Rewrite snapshot refreshed. assert.equal(data.fresh_install.option, '/%postname%/'); assert.deepEqual(data.fresh_install.set_permalink, ['/%postname%/']); // An admin's own structure wins, untouched. assert.equal(data.chosen_structure.option, '/%category%/%postname%.html'); assert.deepEqual(data.chosen_structure.set_permalink, []); // Version stamp flushes exactly once, never per request. assert.equal(data.version_flush.flush_count, 1); assert.equal(data.version_flush.stamp, '9.9.9-test'); assert.equal(data.version_flush.repeat_flush, 0); });