aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests
AgeCommit message (Collapse)Author
2026-08-06test(version-selector): drop four stale #[ignore] attributesnsfisis
The reason string blamed shirabe_php_shim::runtime::constant(), which PlatformRepository stopped reaching once its constant lookups went through the RuntimeInterface seam. All four pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06feat(plugin): let links and release dates cross the RPC boundarynsfisis
The link getters and setters and the release date accessors were explicit errors for every non-empty value, because an immutable value has no entity to point a handle at. They now cross as materialized values: the descriptor names the real class and the constructor arguments, and each side builds a genuine instance of its own. The semver constraint a link holds is encoded structurally rather than re-parsed from its string form, so the pretty strings and the conjunctive flag survive the crossing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06chore: drop @param/@return tags that only restate Rust typesnsfisis
The ported docblocks copied @param and @return straight from the PHP source. When such a tag carries nothing but a type and an argument name, the Rust signature already states it, so the line is noise. Tags whose text adds prose beyond the type are kept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06refactor(php-shim): take filesystem paths as impl AsRef<Path>nsfisis
The shim's filesystem entry points took `&str` even though each one resolves to a local path through `std::fs` or a syscall, so callers holding a `PathBuf` had to stringify it at the call site. They now take `impl AsRef<Path>`, the form `file_exists`, `is_dir`, `unlink` and the rest of the already-converted set use. `Phar`, `PharData` and `ZipArchive` keep their archive path as a `PathBuf`. `PharData::compress` names the compressed sibling by appending the suffix to the file name rather than formatting the path into a `String`. Arguments PHP resolves through a stream wrapper (`fopen`, `file_put_contents`, `include`) still take `&str`, as do the byte-string operations (`dirname`, `basename`, `pathinfo`) and archive-internal entry names, which are `/`-joined logical names rather than OS paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06test(plugin): cover composer/installers beyond a fresh installnsfisis
A single install leaves the installer contract half tested: it never reaches `update`, never runs over an already-installed tree, and never reaches the plugin's own `uninstall()` override — the one chaining onto the promise `LibraryInstaller::uninstall` returns. Nor does it touch the configuration surface real projects use, `installer-paths` and `installer-name`, which the plugin reads back through the package proxy. All three now compare command output as well as the resulting tree against upstream Composer. Progress bar frames are dropped from that comparison: Shirabe renders them differently for every install, including projects with no plugin at all, so they say nothing about the plugin under test. With those covered the plugin joins the verified list in the README.
2026-08-06test(plugin): compare a composer/installers run against upstream Composernsfisis
The fixture project pins composer/installers 2.3.0 and requires two packages of framework-specific types, so the plugin's LibraryInstaller subclass decides where they land. Upstream Composer and Shirabe install the same project and the whole resulting tree is compared. The plugin tarball is fetched by `fixtures/e2e-installers/fetch` into a git-ignored directory and pinned by a digest over the extracted files, so the third-party source never enters this repository and the test skips itself while the directory is absent. The tree is staged and only moved into place once verified, so an unverified tree is never observable under the name the test looks for.
2026-08-06test(plugin): compare a plugin-provided installer against upstream Composernsfisis
The fixture plugin registers an InstallerInterface implementation of its own and installs a package of a custom type with it, recording every contract call it receives. A fresh install produces a byte-identical project tree on both implementations, trace included. A second test pins the divergence a re-run and a `remove` expose: the two implementations consult getInstaller at different points, so the trace order — and its length once `remove` re-creates the Composer instance — differs. It is written in full and marked ignored rather than trimmed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06feat(plugin): run plugin-provided installers through the RPC workernsfisis
A plugin can now hand an InstallerInterface implementation to InstallationManager::addInstaller across the wire, and a legacy composer-installer package is loaded as one; both are backed by a PhpInstallerProxy forwarding the whole installer contract to the entity in the PHP worker. An installer returning a real promise is an explicit error until promises can cross the boundary. InstallationManager takes installers as shared handles instead of boxes, so the object identity removeInstaller and PluginManager's registeredPlugins compare against survives registration, and holds them in a RefCell: Installer::run keeps a shared borrow of the manager for the whole run, and a plugin activated inside it registers its installer from there. The type cache keys on the installer itself, like upstream, so re-entrant registration cannot leave a stale index behind. InstallerInterface::supports is fallible for the same reason getCapabilities and getCommands are: it answers over RPC. Cloning a proxy stub clones the Rust-side entity and rebinds the copy to the fresh handle. Previously only the classes declaring __clone got a throwing body, and the rest let two stubs share (and twice release) one handle. Package entities answer with AnyPackage::dup, which already carries BasePackage::__clone and the RootAliasPackage override; the others are an explicit error. The package proxy covers the whole PackageInterface surface; only the link maps and the release date still lack a wire image for their value objects. PluginManager gains a test-only seam for the reported Plugin API version, and the three PluginInstallerTest cases that need it are ported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05test(plugin): pin a composer-normalize fixture and compare its listingnsfisis
The pinned plugin and its real dependency tree (nine packages, fetched by commit and verified by tree hash into the git-ignored ext/) install under both implementations, and the list/help renderings of the plugin-provided normalize command must match upstream byte for byte. The execution comparison is written but ignored: NormalizeCommand builds a second, in-process Composer instance, and the worker's proxy stubs reject native construction of the classes that path instantiates. Stale comments about the missing worker-side application are updated to the current facts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05test(plugin): compare plugin-provided command execution against upstreamnsfisis
A Shirabe-authored CommandProvider fixture plugin (greet) runs under both implementations: exit codes, the command's own output, help/list rendering, alias resolution, a validation failure, and a file recording the shared object graph the command observed (root package, strict application FQCN, and the exit code of the built-in about command it invoked) must match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05feat(plugin): discover and list plugin-provided commandsnsfisis
Port Application::getPluginCommands: resolve the local composer with plugins force-enabled, fall back to Factory::createGlobal, and collect commands from CommandProvider capability adapters. PhpCommandProxy now mirrors name/description/aliases/hidden over RPC so `list` output matches upstream; input definitions remain TODO(plugin). PhpClass::php_class_name returns an owned String because PHP-backed proxies only know their class at runtime (the override-skip warning prints get_class). CommandProvider::getCommands hands out shared Rc<RefCell> handles since the commands are stored in the application. Factory::createGlobal now propagates createConfig errors instead of swallowing them, and Application::getComposer only catches the exception classes upstream catches, so a ParsingException reaches doRun's GithubActionError path as in Composer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): instantiate plugin capabilities through the PHP RPC workernsfisis
getPluginCapability was a no-op returning None. Now it runs the real flow: class_exists in the worker, new $capabilityClass($ctorArgs) with the plugin's own phandle spliced in as $ctorArgs['plugin'], both instanceof checks answered by is_a in the child, and a per-interface Rust adapter over the resulting entity (CommandProvider and the plain Capability marker; anything else is an explicit error). getPluginCapabilities now propagates errors instead of swallowing them. Capable::get_capabilities widens from IndexMap<String, String> to IndexMap<String, PhpMixed>: upstream casts the return with (array) and validates only the queried key, so the narrow type both rejected maps Composer accepts and made the invalidImplementationClassNames data provider unrepresentable. The old code also returned ' 0 ' (trimmed to the falsy '0') as a valid class name where upstream throws; the rewritten validation follows upstream's empty/is_string/trim sequence. CommandProvider::get_commands returns BaseCommand adapters whose name is read back over RPC after the PHP constructor ran configure(); executing one still needs the PHP-side Symfony Application and stays an explicit error. The is_array / instanceof BaseCommand checks that upstream's Application::getPluginCommands performs on the raw getCommands value live in the adapter, because Vec<Box<dyn BaseCommand>> asserts every element up front. Ports testCommandProviderCapability (plugin-v8 end to end against the real worker) and testQueryingWithInvalidCapabilityClassNameThrows (all eight provider cases); the two tests that pass a PHPUnit mock plugin into PHP stay ignored — a Rust-native mock has no PHP-side entity to cross the boundary as $ctorArgs['plugin']. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04docs: state code facts instead of porting-phase progressnsfisis
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04test(plugin): compare a real-plugin install against upstream Composernsfisis
Run upstream Composer and Shirabe over pristine copies of a fixture project at the same path and require composer.lock, the whole vendor tree (including the plugin-generated GeneratedConfig.php), the plugin's IO lines and the exit code to match byte for byte. The plugin under test (phpstan/extension-installer 1.4.3) is downloaded by fixtures/e2e/fetch — pinned to an upstream commit and hash-verified — into a git-ignored directory rather than committed; the test skips while it is absent, like the other real-PHP prerequisites. Its dependencies are minimal stand-ins resolved from local repositories, so test runs stay offline. A zip dist would exercise the known lossy-string byte-precision debt in RemoteFilesystem, so the fixture serves the plugin through a path dist until that is resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04test(plugin): tolerate the _composer_tmp rename in the removal testnsfisis
The PHP worker is shared across the test binary, so whichever subscriber test runs first owns the bare class name and every later install registers the plugin under a _composer_tmpN rename (as upstream does). The exact-name lookup made the removal test depend on lock acquisition order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): remove subscribed listeners when a plugin is removednsfisis
Wire removePlugin to EventDispatcher::removeListener now that subscriber listeners carry the plugin's P-table handle: PHP's $candidate[0] === $listener identity check maps to phandle equality on Callable::PhpMethod. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): dispatch plugin event subscribers through the RPC workernsfisis
Wires the addPlugin subscriber branch end to end: EventSubscriberInterface and Capable become fallible and dyn-compatible (their sole implementor is the PHP plugin proxy, which answers getSubscribedEvents over RPC), listeners register as Callable::PhpMethod and are invoked with a per-call event handle, and the R table now drops entries when a child-side stub destructs. The R table keeps its IndexMap with monotonically increasing handles, so released handles are never reused and no generation counter is needed. Upstream has no subscriber-plugin test, so the path is covered by a Shirabe-owned fixture exercising all three getSubscribedEvents shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04fix(plugin): resolve review findings in the plugin activation flownsfisis
InstallationManager::execute now takes &self (the mock recorder moved into a RefCell) and its callers hold only shared borrows: plugin registration inside a batch re-enters the same manager handle through Composer::getInstallationManager()->getInstallPath(), which panicked on the RefCell re-borrow under the &mut shape — the same re-entrancy the repository side already fixed, unreachable from the ported tests because they call PluginInstaller::install directly like PHPUnit does. The worker-side InstalledVersions mirror now matches the full tail of FilesystemRepository::write: unconditional reload plus the reflection-based selfDir/installedIsLocalDir restore. The previous class_exists(false) guard rested on a lazy-load assumption that does not hold in the worker (its real ClassLoader only knows the Composer checkout's vendor dir, so a later lazy load would read the checkout's installed.php, not the project's); the mirror is now skipped only when the class is not autoloadable at all, i.e. no plugin runtime and hence no observer code. Boot-time seeding stays TODO(plugin). Also from the review: registered_plugins entries are removed only after the deactivate/uninstall loop (PHP unsets last, and a throw must leave the entry observable); extra.class keeps associative-array values and fails loudly on non-strings instead of silently dropping them; the two discarded write() results now propagate (they carry the reload-push failure); register_package's allow-plugins skip message is DEBUG like the addPlugin side; the loader-eviction divergence of REGISTERED_LOADERS and the lossy UTF-8 spots carry searchable markers; the test-only proxy downcast follows the __ naming rule; the R-table dispatch clones the entity out instead of holding the table borrow across the handler; the IO/PartialComposer stubs turn a plugin-side `new NullIO()` into an explicit error instead of an ArgumentCountError; and the empty() emulation covers float 0.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04feat(plugin): activate plugins through the PHP RPC workernsfisis
Implement the remainder of PluginManager::registerPackage: the plugin autoload map is built by the ported createLoader/parseAutoloads and served to the worker over the existing reverse-RPC autoloader, files entries go through a composerRequire-equivalent glue call, and already-defined classes take the upstream _composer_tmp rename/eval path. Instantiation uses the new NewObject/CallPhpMethod lanes backed by a P table in the worker; PhpPluginProxy adapts the resulting handle to PluginInterface, with $composer/$io exposed to plugin callbacks via an R table (unsupported methods stay explicit errors). Hand-written proxy stubs cover Composer, PartialComposer and the IO hierarchy, and the stub autoloader is re-prepended after loading the Composer PHP runtime so its vendor autoloader cannot shadow proxied FQCNs. FilesystemRepository::write now mirrors InstalledVersions::reload into a running worker (class_exists-guarded, so an unloaded class keeps its upstream lazy-load behavior), removing the previously undefined observation window. The installer pipeline passes the installed repository as a shared handle instead of a long-lived `&mut dyn`: plugin registration runs inside InstallationManager::execute and re-enters the same local repository through the RepositoryManager, which would panic on the RefCell re-borrow under the old shape. PluginInterface lifecycle methods now take an owned ComposerHandle (plugins retain $composer past the call) and return anyhow::Result (PHP plugin code may throw); the plugin list uses shared ownership so the identity comparison of removePlugin survives the dual storage in registeredPlugins, matching PHP reference semantics. Ports the activate/upgrade/uninstall tests of PluginInstallerTest, serialized across the shared worker process whose persistent class table is exactly what exercises the rename path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03feat(event-dispatcher): run composer.json PHP scripts through the RPC workernsfisis
Implement the two script execution paths that previously stopped at todo!(): a Class::method listener is invoked as CallStaticMethod with the event crossing the boundary as a proxy-stub handle, and a Command-class listener runs inside a throwaway bare Symfony Application hosted by the worker via a generated snippet, its BufferedOutput written back through the dispatcher's IO. makeAutoloader is ported for real (canonical-package hash, setDevMode, buildPackageMap/parseAutoloads/createLoader), and the class_exists/is_callable/is_a/defined guards now query the worker, whose script autoloader resolves classes by asking the Rust-side ClassLoader over the reverse channel. EventInterface gains as_any (the IOInterface downcast pattern) so the concrete event type is reachable behind the trait object. Application::do_run now registers ScriptAliasCommand entries as typed commands, unblocking the run-script --list/alias tests; the dev-mode-to-generator test is ported with local mockall mocks. The remaining ignored tests carry re-verified reasons: the listener methods live on the PHPUnit test class itself (unloadable in the worker), or the test needs live import of a user PHP Command class into the Application. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03test(json): serialize the tests sharing the tabs2.json scratch filensfisis
test_preserve_indentation_after_read and test_overwrites_indentation_by_default copy to and delete the same fixture path; PHPUnit runs them serially, the parallel Rust harness let them race and flake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(repository): resolve remaining late-binding hazards from the auditnsfisis
Three fixes for the ComposerRepository/FilesystemRepository/PlatformRepository hazards where inner-composition delegation skipped PHP's late-bound virtual dispatch: - ComposerRepository::has_package now builds its packageMap through the late-bound getPackages() equivalent, so lazy-providers repos surface the LogicException and available-packages repos load their package list, as in PHP, instead of silently answering false from the raw array. - RepositoryInterface::get_repo_name returns anyhow::Result<String>: PHP's getRepoName() counts through the late-bound initialize(), which is fallible in file-reading subclasses. FilesystemRepository and PackageRepository now run that initialization instead of freezing the inner array repository to an empty state (which also made a later write() truncate installed.json). Supporting changes keep the initialization chain callable from &self: JsonFile::read takes &self (indent moved into a RefCell), FilesystemRepository dev_mode became a Cell, and WritableArrayRepository dev_package_names a RefCell. - PlatformRepository::new routes constructor packages through its own add_package so the override handling and full platform initialization run as they do via PHP's parent constructor; the inner find_package/add_package delegations inside add_package (and ComposerRepository::add_package) gained the same is_initialized guard, since the constructor path would otherwise freeze the repository. Same defect class as 7db937af, 97b5211a and 3e367f78. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02test(bootstrap): port the remaining env setup from tests/bootstrap.phpnsfisis
Add NO_COLOR=1, the COMPOSER/COMPOSER_VENDOR_DIR/COMPOSER_BIN_DIR clears and the timezone normalization, and make bootstrap() Once-guarded so additional call sites stay safe. Wiring it into every test binary still needs a libtest setup hook (ctor crate or fixture-level calls), recorded in the TODO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(json): propagate JsonFile::encode errors instead of unwrappingnsfisis
PHP's JsonFile::encode throws a RuntimeException when json_encode fails; the port swallowed that into an .unwrap() marked TODO(phase-c). Return anyhow::Result from encode/encode_with_options and propagate at every call site (print_table and list_repositories become Result-returning to carry it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02test(completion): port CommandCompletionTester and CompletionFunctionalTestnsfisis
Adds the Symfony console test helper (Tester/CommandCompletionTester) and ports every CompletionFunctionalTest data-provider entry as an individual test. The tests reproduce the PHP environment by chdir'ing into the vendored composer/ checkout (its composer.json/lock provide the installed packages, scripts and package properties the expectations reference); the Packagist-backed entries query the live repository exactly like the PHP test does. Only `exec ` is ignored: its expectations require the dev checkout's fully installed vendor/bin, which the vendored checkout does not ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(io): propagate ask/select errors instead of panickingnsfisis
IOInterface::ask/select now return anyhow::Result<PhpMixed>, and ConsoleIO::ask_question forwards QuestionHelper errors (validator failures, MissingInputException) instead of collapsing them with .expect(). In PHP these exceptions propagate from QuestionHelper through ConsoleIO to the caller, so callers such as UpdateCommand's interactive package selection must be able to observe them; the MissingInputException is wrapped with its concrete type preserved so Application's ExceptionInterface downcast keeps working. All call sites now propagate with `?` (Perforce::query_p4_user becomes Result-returning: PHP declares it void but exceptions still escape), and the previously ignored test_interactive_mode_throws_if_no_package_entered passes. ask_confirmation/ask_and_hide_answer still collapse errors; extending propagation to them is left as TODO(phase-c) pending a decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(repository): run lazy initialization in count/has_packagensfisis
PHP's ArrayRepository::count()/hasPackage() call $this->initialize(), which late-binds to the concrete repository class and lazily loads its packages. The Rust pass-throughs skipped that: they ran ArrayRepository's stub initialize instead, returning 0/false and marking the repository initialized with an empty package list, which made ensure_initialized() skip the real initialization forever after. Take &mut self in RepositoryInterface::count/has_package so the lazy repositories (Filesystem, Platform, Composer) can guard with their real initialize, and return Result from has_package since that initialization can fail (PHP propagates the exception). InstallerInterface::is_installed and InstallationManager::is_package_installed/mark_alias_installed propagate the same way, which also resolves the TODO(phase-d) markers on Package/Path/Artifact/Vcs repositories about initialization errors being swallowed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02refactor(php-shim): drop pack/unpack in favor of direct byte handlingnsfisis
The only callers were trivial fixed-format uses: reading the first four hash bytes as a native int, splitting in_addr byte strings, and building a constant ZIP EOCD record. Each site now does the byte manipulation directly, so the general-purpose shims are no longer needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(diagnose-command): pass the self-audit when installed.json is absentnsfisis
A native binary never ships vendor/composer/installed.json, so Composer's "non-standard Composer installation" warning fired on every diagnose run and forced exit 1. The self-audit itself stays: a Composer source snapshot is planned to be embedded together with the plugin API implementation, which will make it functional; until then the missing file reports success, marked with TODO(phase-c). Also un-ignore diagnose_command_test::test_cmd_success: the other half of its ignore reason ("requires real network access") is no blocker — the PHP original runs its live packagist/github checks unguarded too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02fix(symfony-finder): make Glob::to_regex regex-crate compatiblensfisis
Glob::toRegex emits PCRE-only constructs — the (?=[^\.]) look-ahead for the strict-leading-dot rule, the possessive [^/]++ in /**/ segments, and, via BaseExcludeFilter, the (?=$|/) dir-boundary look-ahead — which the regex crate cannot compile, so `archive` and every ArchivableFilesFinder path panicked. Rewrite the port to tokenize the glob (mirroring the PHP loop's dispatch) and resolve every no-dot constraint by recursive union expansion. The dir boundary must take part in that expansion (a trailing `*` matching zero characters drops the constraint onto the boundary itself), so BaseExcludeFilter now uses the new Glob::to_regex_dir_boundary instead of string surgery. Equivalence was verified against PHP 8.5.8 (vendored Glob.php + preg_match) over 66,176 glob x flag x subject combinations with zero divergence. Un-ignores the five archiver tests blocked on this and updates GitExcludeFilterTest's expected pattern text, an explicitly authorized exception to the no-test-modification rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01feat(symfony-console): implement interactive question/style helpersnsfisis
Resolve the remaining todo!()s in SymfonyStyle, OutputStyle, QuestionHelper and SymfonyQuestionHelper: * Wire up the virtual dispatch PHP performs for the protected writePrompt()/writeError() overrides, following the codebase's established inheritance idiom (Command, ArchiveDownloader): the base class becomes a trait (QuestionHelperInterface, named after the QuestionInterface precedent) whose provided methods ask/do_ask/ validate_attempts carry the template logic and late-bind the write_prompt/write_error hooks through Self, with inner()/inner_mut() reaching the base-class state. SymfonyQuestionHelper overrides the hooks as plain trait-impl methods, mirroring PHP's protected-method overriding, so SymfonyStyle-driven questions now render the Symfony Style Guide prompt. * Type definition_list input as an enum (string|array|TableSeparator) because PhpMixed intentionally cannot carry objects; the InvalidArgumentException branch (a LogicException) becomes unrepresentable. horizontal_table now takes typed Cells/Rows. * Propagate the MissingInputException thrown inside autocomplete() through a Result instead of aborting. * Implement as_console_output_interface via Ref::filter_map on ConsoleOutput, the interface's only implementor. * Port progressIterate eagerly, following ProgressBar::iterate. * Map __FILE__ to current_exe(): a native binary never runs from a phar, so the hiddeninput.exe relocation branch correctly never fires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01feat(php-shim): implement Phar/PharData and the zlib/bzip2 functionsnsfisis
Adopt the tar, flate2, and bzip2 crates to fill in the phar.rs and compress.rs todos: PharData tar/zip reading, building, and whole-archive compression, plus a native .phar reader that follows the php.net file-format manual and verifies hash-based signatures. Callers now propagate the constructor/extract errors PHP throws, and fwrite accepts byte strings so gzread no longer needs lossy UTF-8. The native .phar writing API stays todo!() (no call sites; Composer's Compiler is not ported) and OPENSSL phar signatures are accepted unverified (TODO(phase-c)). This unblocks Tar::getComposerJson and the tar/phar/gzip downloaders; tar_test (7), artifact_repository_test (2), and phar_archiver_test zip (1) are un-ignored. The archive command itself still panics because ArchiveManager::archive always generates glob excludes whose look-ahead regexes the regex crate cannot compile; converting those patterns to regex-compatible ones is a separate, still-undecided work item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26feat(diagnose-command): query the real PHP runtime in one RPC callnsfisis
diagnose used to read hardcoded shim stubs, so it described a fictional runtime: OPENSSL_VERSION_NUMBER was always 0 and tripped the TLSv1.1/1.2 check, PHP_BINARY and OPENSSL_VERSION_TEXT were empty, and the extension, function and ini probes answered from a fixed table. The PHP worker gained a `diagnose` entry that returns every fact the command needs as one PHP array, cached in a OnceLock so the several call sites share a single round trip. Reading it back needed array support in the serialize() parser, which in turn lets get_loaded_extensions and get_all_ini_files return real lists instead of comma-joined strings. Also fixes the openssl_version message, which dropped strstr()'s before_needle argument during the port, and check_connectivity's allow_url_fopen test, which did not follow PHP string truthiness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25fix(require-command): end input borrow before asking for confirmationnsfisis
The `fixed` option was read as a temporary inside the argument list of update_requirements_after_resolution(), so the Ref lived until the end of the enclosing let statement — i.e. across the whole call. When the resolved version looks like a feature branch, that call asks for confirmation, and ConsoleIO takes the same input RefCell mutably, panicking with "RefCell already borrowed". Hoisting the read into its own statement ends the borrow before the call. The expected output of the un-ignored test transcribed PHP's string concatenation operator (`[y,n]? '.'`, used to keep the trailing space visible) as a literal `.`; it now matches RequireCommandTest.php. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25fix(package-discovery): pass IO so platform requirement warnings surfacensfisis
findBestVersionForPackage is the only findBestCandidate() caller that PHP hands $this->getIO() to; the port passed None, so VersionSelector silently skipped every "Cannot use <pkg> as it requires <ext> which is missing from your platform" warning. require/update/create-project therefore dropped a candidate without telling the user why. Un-ignores require_command_test::test_require, whose first data-provider case asserts exactly that warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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-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-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.