aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
AgeCommit message (Collapse)Author
2026-07-25feat(console): register plugin commands via Application::addnsfisis
get_plugin_commands now yields shared command handles so the discovered commands can go through the same add() path as built-in ones, dropping the placeholder that discarded them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25refactor(json): embed Composer schemas instead of copying them to targetnsfisis
build.rs guessed target/<profile> from OUT_DIR to place res/*.json next to the executable (twice, since test binaries live in deps/), and JsonFile resolved them through current_exe(). That made the binary undistributable on its own. The schemas are now include_str!'d and referenced through a shirabe:///res/ URI that SchemaRetriever resolves, keeping the $ref indirection PHP uses for the phar case. The res/ path segment is required so composer-lock-schema.json's relative "./composer-schema.json" reference still resolves.
2026-07-25perf(advisory): share AnySecurityAdvisory via Rcnsfisis
PHP's SecurityAdvisoryPoolFilter stores advisory *object references* in $securityRemovedVersions, and PoolOptimizer::applyRemovalsToPool hands that array to the new Pool by copy-on-write. Porting AnySecurityAdvisory as a value type turned both of those into deep copies. Measured on `require laravel/framework` (offline, warm cache): 113 distinct advisories were duplicated into 314,309 copies of ~1.08 KiB, retaining 331.9 MiB in the filter loop and another 331.9 MiB when apply_removals_to_pool cloned the whole map. 3.54s -> 2.62s (-26%), peak RSS 975 MB -> 343 MB, which matches the upper bound measured by ablation. Composer runs the same workload in 1.49s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25refactor: replace redundant clones with movesnsfisis
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25feat(tracing): init tracing subscriber from $SHIRABE_TRACINGnsfisis
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25refactor(operation): replace OperationInterface with AnyOperation enumnsfisis
Operations are only ever constructed by the dependency resolver, so a plugin has no way to inject an implementation of its own and the set is closed. Modelling it as an enum, like AnyPackage, removes the OperationInterface trait together with its two parallel downcast mechanisms (as_any() + downcast_ref, and as_*_operation()) and the get_package() default method that panicked on UpdateOperation. The PHP idiom `$op instanceof UpdateOperation ? getTargetPackage() : getPackage()`, written out at six call sites, becomes AnyOperation::get_target_package(). InstallationManager's three blocks that matched on the type string and then recovered the type with expect() collapse into exhaustive matches. SolverOperation keeps only its TYPE constant; the shared getOperationType()/__toString() implementations move to AnyOperation, which also drops the five Self::TYPE.to_string() allocations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25chore: remove unused importsnsfisis
2026-07-24fix(event-dispatcher): invoke Closure listeners instead of always failing ↵nsfisis
is_callable RequireCommand registers an inline listener on InstallerEvents::PRE_OPERATIONS_EXEC to track dependency_resolution_completed, mirroring PHP's `function () use (&$dependencyResolutionCompleted) { ... }`. This is Composer's own code, not a Plugin subscriber, but it went through the shared non-string-callable path, which checked is_callable() against a hardcoded PhpMixed::Null and always failed, breaking every `require` that reaches the install step. Callable::Closure now carries the actual Rc<dyn Fn> instead of being a data-less placeholder, and is invoked directly (Closures are always callable in PHP). The ArrayCallable path used by future Plugin subscribers is untouched. Un-ignoring the two require_command_test cases that cited this bug reveals two separate, pre-existing issues (a missing ext-requirement warning message, and a RefCell re-entrancy panic in ConsoleIO::ask_question); their #[ignore] reasons are updated to describe the real current blocker instead of the now-fixed one.
2026-07-24test(installer-test): revert to loop-based fixture iterationnsfisis
4a5a9556 split each installer/installer-slow fixture into its own #[test] fn (with #[ignore] on known failures) so they could be triaged one at a time, and said explicitly that it should be reverted to a single loop-based test per group once triage was done. Only github-issues-7665.test still fails, and its cause is now understood (see the previous commit): an upstream Composer defect, not a porting bug. Restore the three loop-based tests, skipping that one fixture inline with a TODO(phase-d) comment instead of a per-fixture #[ignore].
2026-07-24docs(installer-test): record confirmed cause for github-issues-7665 ignorensfisis
The ignore reason for slow_github_issues_7665 said "unknown reason, needs further investigation." Investigation since then traced the mismatch to an upstream Composer defect: Problem::getPrettyString's RULE_LEARNED tie-break comparator (getSortableString() <=> getSortableString()) is not transitive, and the values it compares are literal ids that shift with the platform package count, which Installer::createPlatformRepo() derives from the real ambient PHP runtime and which Composer's own test suite never mocks. This fixture is brittle to whichever extensions are installed on the machine generating or running it. Nothing to fix on the Rust side; this is an upstream Composer bug (composer/composer#12111 introduced the comparator).
2026-07-24fix(solver-problems): compare RULE_LEARNED sort keys like PHP's <=>nsfisis
Problem::getPrettyString sorts same-priority reasons by getSortableString(), whose RULE_LEARNED key is a '-'-joined literal id string (e.g. "-95"). PHP's <=> compares two numeric strings numerically, but the port used plain String::cmp (byte-wise), which reverses relative order for same-length negative-number keys. Added shirabe_php_shim::loosely_compare to approximate PHP's <=> for this pattern (numeric compare when both sides parse as numbers, else byte compare) and switched the sort comparator to use it. The diagnosis this replaces (from the commit being amended) blamed Pool package-id assignment order diverging from PHP under COMPOSER_POOL_OPTIMIZER=0. That was disproven this session: direct instrumentation of both PHP and the Rust port confirmed identical relative package-id order, including on the ~335-package github-issues-7665 fixture (ids matched up to a constant +2 offset from a platform-mock package count difference). The sort comparator was the actual bug, not package loading order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24fix(solver-problems): preserve first-occurrence order in extension hintnsfisis
create_extension_hint's --ignore-platform-req suggestion list sorted missing extensions alphabetically before deduping. PHP's array_unique removes duplicates while preserving first-occurrence order instead, which for this call site matches the order problems were reported in (root-require-not-found problems before SAT-conflict problems). Replaced the sort+dedup with the existing order-preserving shirabe_php_shim::array_unique, which was already ported for exactly this PHP semantic but wasn't used here.
2026-07-24fix(installer-test): catch exceptions thrown during composer constructionnsfisis
PHPUnit's expectException/expectExceptionMessage wrap the whole rest of InstallerTest::doTestIntegration, so a fixture's expected exception can legitimately come from FactoryMock::create() itself (root package construction), not just the later install/update run. The Rust port only checked for that at one point, after the run, and unconditionally unwrapped Factory::__create_mock's result — so a fixture like install-self-from-root.test (root package requiring itself, which throws during root package construction) panicked on that unwrap instead of being caught and compared against the expected message. Capture the construction Result and, when an exception was expected, run the same normalize/contains/assert check the later block already uses before returning early, matching PHPUnit's behavior of running no further test-method code once the expected exception has fired.
2026-07-24fix(array-dumper): dump source/dist mirrors as a JSON arraynsfisis
ArrayDumper::dump built the mirrors field as PhpMixed::Array keyed by stringified index ("0", "1", ...), which this codebase's PhpMixed JSON serialization renders as an object. PHP just assigns the plain, sequentially-keyed mirrors array directly, which json_encode renders as a JSON array. Use PhpMixed::List instead, matching the actual shape written to composer.lock.
2026-07-24fix(package): rewrite dist-reference SHA regex without look-aroundnsfisis
Package::set_source_dist_references and LockTransaction's dist-url mirroring both used {(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i, but the regex crate has no look-around support at all and panics compiling it. Per docs/dev/regex-porting.md, rewrote the boundary assertions into capturing groups and switched to Preg::replace_callback, which re-emits the captured delimiters around the replaced reference.
2026-07-24fix(composer-repository): initialize wrapper before delegating loadPackagesnsfisis
For a simple, non-lazy composer repository (no providers-url/ metadata-url/providers-lazy-url), load_packages() delegates to self.inner.load_packages() (PHP: parent::loadPackages()). PHP's ArrayRepository::loadPackages() calls $this->getPackages(), which virtual-dispatches through ComposerRepository::getPackages() down to ComposerRepository::initialize() (the real HTTP/file fetch). Composition doesn't get that dispatch for free: self.inner is a bare ArrayRepository, so self.inner.load_packages() ends up calling ArrayRepository::initialize() — a no-op stub that just sets an empty package list — instead of ComposerRepository::initialize(). The repository's packages.json fetched and parsed successfully, but the result was silently discarded, so the repo always looked empty. get_packages() already carries the identical guard with the same diagnosis in its own comment; load_packages() was just missing it. Documented the same latent gap in find_package/find_packages/count/ has_package, which call into ArrayRepository the same unguarded way but aren't known to be exercised by any test yet.
2026-07-24fix(installer-test): port InstallerTest::setUp's chdir(__DIR__)nsfisis
PHP's InstallerTest::setUp() does chdir(__DIR__) before every test so relative "type": "path" repository URLs in fixtures resolve against composer/tests/Composer/Test; tearDown() restores the previous cwd. This was never ported, only the env-var clears were, so any fixture declaring a relative path repo failed at PathRepository::initialize() with "the `url` supplied for the path (...) repository does not exist" — not a missing PathRepository feature, just an unset cwd. Turned TearDown into a real guard that captures and chdirs on construction and restores on Drop. Since set_current_dir is process-wide state and tests run concurrently by default, added #[serial] to test_installer (the only call site not already inside a #[serial] macro-generated fn) so no two TearDown-holding tests can race on cwd; serial_test's plain #[serial] shares one unnamed lock across the whole binary, so this is sufficient.
2026-07-24fix(pool-builder): use plain getPackages() on locked repositorynsfisis
PoolBuilder::build_pool() and warn_about_non_matching_update_allow_list() called get_canonical_packages() on the locked repository during a partial update, but PHP's PoolBuilder uses the plain getPackages(). get_canonical_packages() unwraps AliasPackage down to its base package, discarding the alias's own version identity. Locker::get_locked_repository() wraps a lock entry with extra.branch-alias in a single CompleteAliasPackage rather than adding a separate base object, so canonicalizing it silently dropped the locked branch-alias version (e.g. 2.2.x-dev), leaving only the raw dev-master version behind. For a package excluded from the partial update's allow-list, that meant its locked branch-alias version could no longer satisfy the root's constraint, producing a spurious solver conflict instead of keeping the package pinned exactly as locked. Fixed the same bug in AuditCommand's --locked package listing, which had the identical get_canonical_packages()/getPackages() mismatch against AuditCommand.php.
2026-07-24fix(dependency-resolver): query real PHP for ext-* version/loaded checksnsfisis
Two shim gaps made the extension-related branches of solver-problem messages wrong: - phpversion($ext) with a non-empty extension can't be known statically (it's shirabe-php-shim's todo!()); Problem::get_missing_package_reason now calls shirabe_php_rpc::phpversion, the same RPC bridge platform::runtime::Runtime::get_extension_version already uses. - extension_loaded's hardcoded allowlist was missing "pcre", a mandatory always-compiled-in PHP extension, so ext-pcre was misreported as "missing from your system" instead of "disabled by your platform config" whenever a platform override disabled it. Also fix XdebugHandler::getAllIniFiles() always returning `[""]` (a php-runtime stub): create_extension_hint()'s early-return guard (`paths[0] empty && len==1`) fired unconditionally, silently dropping the entire "To enable extensions..." hint from every solver-problem message that mentions missing extensions. shirabe-external-packages can't depend on shirabe-php-rpc (shirabe-php-rpc already depends on shirabe-external-packages), so IniHelper::get_all() queries a new get_all_ini_files RPC command directly instead of going through the stub. This exposed that XdebugHandler is never constructed with a name because bin/composer's restart-without-Xdebug bootstrap was never ported to main.rs, making COMPOSER_ORIGINAL_INIS-driven behavior unreachable; documented with a TODO(phase-c) and updated ini_helper_test.rs's ignore reasons (and ignored test_with_no_ini, which only passed before by coincidence with the old stub's constant output) to match.
2026-07-24fix(installer-test): classify EXPECT-LOCK by PHP truthiness, not string matchnsfisis
PHP's readTestFile splits sections with a regex that leaves a section's own trailing newline attached when it is the file's last section (verified against real PHP); for install-without-lock.test and update-without-lock.test, --EXPECT-LOCK-- is that last section, so its raw content is "false\n", not "false". PHP's `$expectLock === 'false'` check therefore also misses, but PHP falls through to JsonFile::parseJson("false\n"), which json_decodes to boolean false anyway, and every downstream check in doTestIntegration branches on PHP truthiness of $expectLock, so the outcome is unaffected either way. The Rust port only mirrored the literal string comparison, so the same "false\n" produced ExpectLock::Json(Value::Bool(false)) instead of ExpectLock::Never, and do_test_integration's Json arm unconditionally read composer.lock, panicking because config.lock=false means no lock file is ever written. Parse EXPECT-LOCK as JSON first and classify by PHP truthiness of the result, matching what doTestIntegration actually checks instead of the raw string.
2026-07-24fix(php-shim): keep literal '>' outside tags in strip_tagsnsfisis
The b'>' match arm in strip_tags's state machine never pushed the character to the output buffer when encountered outside a tag (state 0), unlike every other special-character arm (!, ?, -, and the catch-all) which does push in that state. Every literal '>' not part of an HTML-like tag was silently dropped. This corrupted "=>" into "=" in Composer's operation trace strings (e.g. "Upgrading foo/bar (1.0.0 => 1.1.0)"), which go through strip_tags to remove the <info>/<comment> markup before comparison, un-ignoring 82 integration tests that were asserting on that exact arrow.
2026-07-24fix(locker): return stability-flags as int, not stringnsfisis
Locker::get_stability_flags returned IndexMap<String, String>, converting each value via PhpMixed::as_string(), which only matches the String variant. composer.lock's "stability-flags" values are always JSON integers (BasePackage::STABILITIES), so every flag silently decoded to "" and installer.rs's downstream .parse::<i64>() defaulted it to 0 (stable). This made any locked package pinned via a non-stable stability-flags entry look "unacceptable" during `composer install`, silently dropping it from the solver's pool instead of fixing/requiring it — turning a real dependency conflict into a spurious "lock file needs changes" result. Return i64 directly, matching RootPackageInterface::get_stability_flags and set_lock_data's existing convention for this same PHP array shape.
2026-07-24fix(installer-test): match Locker content-hash encoding to lock hashnsfisis
do_test_integration built the Locker's composer.json content string with serde_json::to_string, which doesn't escape slashes, while the fixture's auto-computed lock "hash" field used JsonFile::encode_with_options with slashes escaped (mirroring PHP's json_encode default). Since virtually every package name contains a "/", this made Locker::isFresh() spuriously report every such lock as stale, unignoring 12 fixtures that depended on the lock being recognized as fresh during `install`.
2026-07-24test(installer): split integration tests into one #[test] per fixturensfisis
Replace the three loop-based test functions (each iterating all fixtures with a shared SKIP_INSTALLER_* skip list) with macro-generated per-fixture test functions, so a single failing or panicking fixture no longer hides the pass/fail status of the rest. Known-failing fixtures keep their TODO(phase-d) reasons via #[ignore] on their own test instead of a shared skip list. All generated tests are #[serial] since they mutate global env vars (COMPOSER_POOL_OPTIMIZER, COMPOSER_FUND) that would otherwise race across parallel test threads. This is an interim structure for triaging the ~131 ignored fixtures one at a time; it should be reverted to a single loop-based test per group (matching Composer's directory-scanned fixtures) once they are resolved.
2026-07-24refactor(filesystem-repository): drop eval() shim, defer installed.php ↵nsfisis
reload to PHP runtime Rust has no PHP interpreter, so eval() can never be ported faithfully. safely_load_installed_versions()'s job of priming Composer\InstalledVersions before plugins run only matters within a single shared PHP process, which the RPC-based plugin architecture does not have; the PHP runtime process can call InstalledVersions::reload() itself instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24docs(symfony-filesystem): mark unported Filesystem gaps with TODO(phase-c)nsfisis
Audited every remaining method in the Symfony Filesystem port against composer/vendor/symfony/filesystem/Filesystem.php and tagged each divergence with a searchable TODO(phase-c): the missing self::$lastError propagation, copy()'s collapsed fopen-failure messages and skipped mtime preservation, exists()'s missing PHP_MAXPATHLEN guard, do_remove()'s unported rename/rollback safety trick and its Unix short-circuit gap, symlink()'s unported Windows path-normalization/copy_on_windows fallback, link_exception()'s unported error-code-1314 message, read_link()'s missing canonicalize=true overload and its Windows PHP<7.4 quirk, and mirror()'s missing getRealPath()/filesCreatedWhileMirroring dedup. Windows-only branches were previously left with plain comments claiming they "never run on Unix" instead of the required TODO marker, which understates them as permanently out of scope rather than unported work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24refactor(symfony-filesystem): remove unused Filesystem API surfacensfisis
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24refactor(symfony-style): remove unused StyleInterface impl on OutputStylensfisis
Real symfony/console OutputStyle is abstract and never defines these methods itself (title/section/table/ask/... stay abstract, deferred to SymfonyStyle). The Rust impl block was a porting artifact never invoked anywhere: OutputStyle is only used as SymfonyStyle's concrete `inner` field, and every StyleInterface call site goes through SymfonyStyle's own full implementation. new_line, which SymfonyStyle::new_line does delegate to, moves to an inherent method to keep that call working.
2026-07-24refactor(symfony-process): remove unused Process API surfacensfisis
Since Process is php-native with no Rust-fidelity obligation (see plugin-class-classification.md), this port only needs to cover what Rust-ported Composer code actually calls. Made the pipes/process_utils modules pub(crate) (nothing outside symfony/process used them) and rebuilt with `--force-warn dead_code` (normally allowed workspace-wide) to find genuinely unreachable methods: Process lost 17 methods, 5 constants, and a private clone helper; ExecutableFinder lost two unused suffix setters; AbstractPipes lost handle_error, whose only caller (a stream_select error-handler registration) was never wired up. Removing several of those setters (set_pty, set_idle_timeout, disable_output/enable_output, set_options) then left the fields they used to write with no remaining writer, so they hold one constant value on every reachable path: pty always false, idle_timeout always None, output_disabled always false, options always {suppress_errors, bypass_shell}. Audited by value (not just call-graph reachability) and removed everything that depended on the now-constant value: - pty: is_pty(), is_pty_supported(), the PTY descriptor branch in UnixPipes::get_descriptors(), and the now-unconstructed Descriptor::Pty variant in shirabe-php-shim (plus its proc_open match arm). - idle_timeout: get_idle_timeout() and check_timeout()'s idle branch; ProcessTimedOutException collapses to the single reachable timeout type (dropped timeout_type/TYPE_GENERAL/TYPE_IDLE/is_general_timeout/ is_idle_timeout/get_exceeded_timeout). - output_disabled: is_output_disabled(), build_callback()'s disabled variant, get_descriptors()'s output_disabled term, and the always-false guard in read_pipes_for_output()/ProcessFailedException (its output section is now unconditional). - options: Drop::drop()'s create_new_console branch can never fire (that key can no longer exist), so it always just stops the process. - has_callback/last_output_time: left write-only once their only readers (the branches above) were gone. - have_read_support: constant true once output_disabled collapsed, so removed from PipesInterface, UnixPipes (incl. its /dev/null null-stream branch), WindowsPipes, and Process::wait()'s dead guard. No behavior change: every removed item/branch had zero callers, or was constant on every reachable call site.
2026-07-23feat(process-executor): stub plugin-facing execute_async_php pathnsfisis
A plugin can reach ProcessExecutor::executeAsync() through the rust-proxy stub, which resolves with a real Symfony Process instance that can't be reconstructed on the Rust side (its state is tied to whichever process calls proc_open(), and it refuses serialization). Add execute_async_php() as a todo!() stub, documented as a dual- instantiation split in plugin-class-classification.md: Rust-internal callers keep using execute_async(), while the plugin path must forward spawning to the PHP child once the RPC channel exists.
2026-07-20perf(composer-repository): avoid redundant clones when checking security ↵nsfisis
advisories get_security_advisories cloned each package's full metadata blob (hundreds of versions for popular packages) just to peek at its "security-advisories" field. Same pattern already fixed in load_async_packages by 2862d2da; move ownership through pattern matches and IndexMap::shift_remove instead. Measured with laravel/laravel create-project: response processing in the security-advisories metadata branch drops ~80% (236ms -> 48ms), and the whole run_security_advisory_filter phase drops from ~420-500ms to ~250-270ms. End-to-end, shirabe now matches or edges out upstream Composer (6.57s vs 6.85s in this run) instead of trailing by ~1.2s.
2026-07-20perf(composer-repository): avoid redundant clones when loading async package ↵nsfisis
metadata load_async_packages cloned each response's version metadata up to three times (spec_list/response, response_arr, versions_mixed, then every per-version field) before building the versions Vec. Move ownership through pattern matches and IndexMap::shift_remove instead. Measured with laravel/laravel create-project: per-batch response processing time in load_async_packages drops ~39% (1.30s -> 0.80s cumulative), narrowing the E2E gap vs upstream Composer from 1.37s to 1.14s on average.
2026-07-20feat(php-rpc): attach worker process state to request I/O errorsnsfisis
A dead worker and a live one hitting a framing bug both surface as a raw socket I/O error (e.g. "Broken pipe"), which doesn't say whether the child crashed, was signaled, or is still running. Query the child's exit status via try_wait() and attach it as anyhow::Context so the panic message shows the root cause directly.
2026-07-20fix(json-file): forward detected indent into write's encode optionsnsfisis
JsonFile::write_with_options() used the caller-supplied JsonEncodeOptions verbatim, ignoring self.indent (set by read()'s detect_indenting), unlike PHP's write() which always passes $this->indent to encode() regardless of the $options argument. Un-ignore test_preserve_indentation_after_read.
2026-07-20fix(search-command): avoid usize overflow panic when truncating descriptionnsfisis
The negated remaining-width subtraction was cast straight to usize, overflowing whenever it went negative (e.g. an abandoned-package warning ate the available width) and panicking on slice indexing. Route through the shim's substr(), which mirrors PHP's negative-length semantics and clamps instead of overflowing.
2026-07-20fix(pool-builder): fix three build_pool bugs found by un-ignoring ↵nsfisis
test_pool_builder build_pool never populated skipped_load for locked packages skipped due to the update allow list (nor their replace targets), so load_package's skipped_load-driven transitive-unlock branch never fired. load_packages_marked_for_loading never recorded loaded packages into loaded_per_repo, so repositories had no way to dedupe already-loaded versions when a constraint got re-expanded, producing duplicate pool entries. unlock_package looked up a locked package's removal index via the position in a Vec snapshot of self.packages.values() instead of its actual IndexMap key, so after earlier removals the wrong entry (or none) got removed, leaving stale locked packages in the pool. Fixing all three lets test_pool_builder run un-ignored.
2026-07-20fix(no-proxy-pattern): stop chr() corrupting IP bytes as lossy UTF-8nsfisis
shirabe_php_shim::chr() returned a Rust String, which lossily re-encodes bytes >= 0x80 as UTF-8 replacement characters. ip_get_mask, ip_get_network, and ip_map_to_6 relied on chr() to build raw in_addr and netmask byte arrays, corrupting IPv4-in-IPv6 mappings and CIDR netmasks. Build the Vec<u8> byte arrays directly instead of round-tripping through String, and un-ignore test_ip_address and test_ip_range now that the underlying bug is fixed. chr()'s only other caller (http_downloader.rs, an ASCII ESC byte in a regex pattern) didn't need the indirection either, so remove the shim function entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20test(proxy-manager): un-ignore tests by serializing env-var accessnsfisis
The ignored tests raced on the process-wide HTTP_PROXY/etc env vars and the ProxyManager::INSTANCE mutex, causing spurious PoisonError panics when run in parallel. Adding #[serial_test::serial], the same annotation test_instantiation already used, fixes the race with no other changes needed.
2026-07-20fix(php-shim): use PhpMixed::to_bool() for PHP truthy castsnsfisis
Several call sites coerced PhpMixed to bool via `.as_bool()` (which only matches a literal Bool variant) where the corresponding PHP code does a plain `(bool)` cast or truthy check (isset()/array_key_exists() + implicit bool conversion). This silently dropped truthy non-bool values (e.g. String("true"), String("1"), Int(1)) to their unwrap_or default instead of PHP's actual truthy result. Switched these sites to PhpMixed::to_bool(), which implements PHP's full truthy-cast rules.
2026-07-20test(ignore): document root causes for unannotated #[ignore] testsnsfisis
74 tests carried a bare #[ignore] with no explanation. Re-ran each: 25 now pass and had the attribute removed; the remaining 49 got a concise reason (todo!() stubs, regex-crate PCRE gaps, PhpMixed type mismatches, config bool-coercion bugs, missing skipped_load wiring in PoolBuilder, etc.) so future work can find and fix them by grep. No production code or test logic/assertions were changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20fix(advisory): treat int-keyed ignore entries as plain IDsnsfisis
parseIgnoreWithApply distinguishes ['CVE-123' => 'reason'] from [0 => 'CVE-123'] by checking is_int($key) in PHP. AuditConfig only matched on the value's type, so a canonical-int-keyed string value was mistaken for an id => reason pair. Expose canonical_int_key from the php-shim to replicate PHP's key-canonicalization rule and unignore test_mixed_formats.
2026-07-20fix(diagnose-command): stop holding a Config borrow across http callsnsfisis
execute() held &config.borrow() across check_http/check_composer_repo/ check_composer_audit, which reach HttpDownloader -> CurlDownloader:: download; that method does self.config.borrow_mut() on the same Config RefCell, panicking with "RefCell already borrowed" once the phpinfo panic that previously masked this was fixed. Switch those three helpers to take the Rc<RefCell<Config>> handle (matching check_version's existing pattern) and borrow only where a field is actually read, so no borrow spans the downstream network call. Un-ignore the now-passing run_diagnose smoke test and test_cmd_fail; test_cmd_success stays ignored, now for two separate reasons: it needs real network access (as the PHP original does), and shirabe_php_shim::OPENSSL_VERSION_NUMBER is a hardcoded stub (0) that always trips check_platform's TLSv1.1/1.2 support check regardless of the real linked OpenSSL, forcing a non-zero exit code.
2026-07-20feat(php-rpc): implement phpinfo() capture over RPCnsfisis
DiagnoseCommand::check_platform needed phpinfo() output but shirabe-php-shim's ob_start()/phpinfo()/ob_get_clean() are unmodeled todo!()s. Add a phpinfo dispatch entry to the PHP worker (capturing output the same way extension_info already does) and a get_phpinfo() wrapper, then switch check_platform to call it directly. This unblocks the phpinfo-related panic in diagnose; the ignored tests now hit a separate RefCell double-borrow bug in check_http, so their ignore reasons are updated to point at that instead.
2026-07-20test(fund-command): match PHP tester options for deterministic displaynsfisis
capture_stderr_separately routed the tester through ConsoleOutput, which detects color/hyperlink support from the real process STDOUT before the stream is swapped to the memory buffer. When STDOUT is a real tty, decoration leaks into the captured output. The PHP original never passes this option and get_error_output() was unused here, so drop it to match and keep the assertion deterministic.
2026-07-20test(show-command): ignore test_self over UTC-only date() timezone gapnsfisis
The shim date() renders in UTC only (no timezone database) while PHP's date() uses the system default timezone, so get_relative_time misses the "today" match and prints "this week" whenever the local date differs from the UTC date (daily 00:00-09:00 JST on this machine). Verified by running the test with and without TZ=UTC at 07:40 JST. Mark the gap with a TODO(phase-c) in the shim; fixing it needs a timezone database (a new crate), which is a user decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(cli-tests): detail run_diagnose ignore reasonnsfisis
Measured the actual panic site: DiagnoseCommand::check_platform reaches the todo!() ob_start()/ob_get_clean() shims while capturing phpinfo(), the same root cause already recorded in tests/command/diagnose_command_test.rs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(require-command): drop input borrows before determine_requirementsnsfisis
The Ref temporaries created by input.borrow() inside the argument expressions of the determine_requirements call lived until the end of the whole call statement, so ConsoleIO::ask_question's borrow_mut() on the same shared input RefCell panicked with "RefCell already borrowed" when the command prompted for packages. Hoist the argument computations into locals so no borrow is held across the call, and un-ignore the run_require CLI test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(zip): detail why the zip-extension test stays ignorednsfisis
PHP only runs this test when the zip extension is not loaded; the Rust port has unconditional zip support, so that precondition cannot exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20test(repository-manager): un-ignore test_repo_creationnsfisis
The ignore reason was stale: create_repository_by_class now dispatches every repository class this test registers, and the test passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20fix(github-driver): defer get_branches until tags search missesnsfisis
PHP's `?:` chain in getComposerInformation short-circuits, so getBranches() only runs when the tags search is falsy. The eager port issued an extra git/refs/heads API request that PHP never makes. Un-ignore test_public_repository_archived, which this fixes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>