From 81c16f43eb6d996f36724bc2c51999938f2a66fd Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 24 Jul 2026 16:55:38 -0400 Subject: [PATCH] fix(workspace): keep help and targeted reads bounded --- data-machine-code.php | 51 ++++- inc/Abilities/WorkspaceAbilities.php | 15 +- inc/Cli/Commands/WorkspaceCommand.php | 11 +- inc/Workspace/WorkspaceCoreUtilities.php | 12 +- .../WorkspaceRepositoryLifecycle.php | 57 +++-- inc/Workspace/WorkspaceTargetInspector.php | 95 ++++++++ inc/Workspace/workspace-target-probe.php | 66 ++++++ tests/workspace-command-startup-bounds.php | 213 ++++++++++++++++++ 8 files changed, 480 insertions(+), 40 deletions(-) create mode 100644 inc/Workspace/WorkspaceTargetInspector.php create mode 100644 inc/Workspace/workspace-target-probe.php create mode 100644 tests/workspace-command-startup-bounds.php diff --git a/data-machine-code.php b/data-machine-code.php index 194b215..f9ba3b6 100644 --- a/data-machine-code.php +++ b/data-machine-code.php @@ -21,6 +21,47 @@ define( 'DATAMACHINE_CODE_PATH', plugin_dir_path( __FILE__ ) ); define( 'DATAMACHINE_CODE_URL', plugin_dir_url( __FILE__ ) ); +/** + * Classify DMC CLI requests which must not initialize runtime services. + * + * WP-CLI loads WordPress before it dispatches nested help, so command methods + * cannot protect help from plugin bootstrap side effects. Inspect only DMC's + * own argv namespace here and leave every unrelated WP-CLI request unchanged. + * + * @param array|null $argv Raw process arguments. + */ +function datamachine_code_is_side_effect_free_cli_request( ?array $argv = null ): bool { + if ( ! defined('WP_CLI') || ! WP_CLI ) { + return false; + } + + $tokens = array_values(array_map('strval', $argv ?? ( is_array($GLOBALS['argv'] ?? null) ? $GLOBALS['argv'] : array() ))); + if ( ! in_array('datamachine-code', $tokens, true) ) { + return false; + } + + if ( in_array('--help', $tokens, true) || in_array('-h', $tokens, true) ) { + return true; + } + + $help_index = array_search('help', $tokens, true); + $dmc_index = array_search('datamachine-code', $tokens, true); + return false !== $help_index && false !== $dmc_index && $help_index < $dmc_index; +} + +/** + * Whether this request is a targeted, read-only workspace command. + */ +function datamachine_code_is_targeted_workspace_read_cli_request( ?array $argv = null ): bool { + if ( ! defined('WP_CLI') || ! WP_CLI ) { + return false; + } + + $tokens = array_values(array_map('strval', $argv ?? ( is_array($GLOBALS['argv'] ?? null) ? $GLOBALS['argv'] : array() ))); + $workspace_index = array_search('workspace', $tokens, true); + return false !== $workspace_index && 'show' === ( $tokens[ $workspace_index + 1 ] ?? '' ) && in_array('datamachine-code', array_slice($tokens, 0, $workspace_index), true); +} + // PSR-4 Autoloading. require_once __DIR__ . '/vendor/autoload.php'; @@ -41,7 +82,7 @@ function datamachine_code_install_schema(): void { * Keep schema current for already-active installs after deploy/update. */ function datamachine_code_maybe_upgrade_schema(): void { - if ( function_exists('wp_installing') && wp_installing() ) { + if ( datamachine_code_is_side_effect_free_cli_request() || datamachine_code_is_targeted_workspace_read_cli_request() || ( function_exists('wp_installing') && wp_installing() ) ) { return; } @@ -75,7 +116,7 @@ function datamachine_code_has_datamachine_integration(): bool { function datamachine_code_register_bundle_artifacts(): void { static $registered = false; - if ( $registered ) { + if ( $registered || datamachine_code_is_side_effect_free_cli_request() ) { return; } @@ -90,7 +131,7 @@ function datamachine_code_register_bundle_artifacts(): void { function datamachine_code_register_datamachine_integrations(): void { static $registered = false; - if ( $registered || ! datamachine_code_has_datamachine_integration() ) { + if ( $registered || datamachine_code_is_side_effect_free_cli_request() || ! datamachine_code_has_datamachine_integration() ) { return; } @@ -117,7 +158,7 @@ function datamachine_code_register_datamachine_integrations(): void { function datamachine_code_bootstrap() { static $bootstrapped = false; - if ( $bootstrapped ) { + if ( $bootstrapped || datamachine_code_is_side_effect_free_cli_request() || datamachine_code_is_targeted_workspace_read_cli_request() ) { return; } @@ -321,7 +362,7 @@ function datamachine_code_register_cli_commands() { * Only load when Data Machine core's AI engine is available. */ function datamachine_code_load_chat_tools() { - if ( ! class_exists('DataMachine\Engine\AI\Tools\BaseTool') ) { + if ( datamachine_code_is_side_effect_free_cli_request() || datamachine_code_is_targeted_workspace_read_cli_request() || ! class_exists('DataMachine\Engine\AI\Tools\BaseTool') ) { return; } diff --git a/inc/Abilities/WorkspaceAbilities.php b/inc/Abilities/WorkspaceAbilities.php index 3049865..f7cabe5 100644 --- a/inc/Abilities/WorkspaceAbilities.php +++ b/inc/Abilities/WorkspaceAbilities.php @@ -2888,21 +2888,22 @@ public static function getCapabilities( array $input ): array { // phpcs:ignor */ public static function showRepo( array $input ): array|\WP_Error { $workspace = new Workspace(); - if ( RemoteWorkspaceBackend::should_handle() ) { - $local_result = self::showLocalWorkspaceHandleIfPresent($workspace, (string) ( $input['name'] ?? '' )); - if ( null !== $local_result ) { - return $local_result; - } + $handle = (string) ( $input['name'] ?? '' ); + $local = $workspace->show_repo($handle); + if ( ! is_wp_error($local) && empty($local['is_context']) ) { + return $local; } + // Registered remote state remains authoritative for local misses, bounded + // local failures, and context aliases whose checkout is not mounted. if ( RemoteWorkspaceBackend::should_handle() ) { - $result = ( new RemoteWorkspaceBackend() )->show($input['name'] ?? ''); + $result = ( new RemoteWorkspaceBackend() )->show($handle); if ( ! self::shouldFallbackToLocalWorkspace($result) ) { return $result; } } - return $workspace->show_repo($input['name'] ?? ''); + return $local; } /** diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index cb37ec8..39218a3 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -17,6 +17,7 @@ use WP_CLI; use DataMachine\Cli\BaseCommand; +use DataMachineCode\Abilities\WorkspaceAbilities; use DataMachineCode\Cli\CliResponseRenderer; use DataMachineCode\Cli\CliRepeatableOptionParser; use DataMachineCode\Cli\WorkspaceCompactOutput; @@ -2510,13 +2511,9 @@ public function show( array $args, array $assoc_args ): void { return; } - $ability = wp_get_ability('datamachine-code/workspace-show'); - if ( ! $ability ) { - WP_CLI::error('Workspace show ability not available.'); - return; - } - - $result = $ability->execute(array( 'name' => $args[0] )); + // This targeted read is also the lightweight startup path used before the + // Abilities API runtime has been bootstrapped. + $result = WorkspaceAbilities::showRepo(array( 'name' => $args[0] )); if ( is_wp_error($result) ) { WP_CLI::error($result->get_error_message()); diff --git a/inc/Workspace/WorkspaceCoreUtilities.php b/inc/Workspace/WorkspaceCoreUtilities.php index 9383f9e..4153e1d 100644 --- a/inc/Workspace/WorkspaceCoreUtilities.php +++ b/inc/Workspace/WorkspaceCoreUtilities.php @@ -692,7 +692,17 @@ private function build_primary_freshness_report( string $repo_path, string $hand ); } - $header = strtok( (string) ( $status_result['output'] ?? '' ), "\n"); + return $this->build_primary_freshness_report_from_status_output((string) ( $status_result['output'] ?? '' ), $handle); + } + + /** + * Build primary freshness metadata from an already-bounded local status probe. + * + * @return array + */ + private function build_primary_freshness_report_from_status_output( string $status_output, string $handle ): array { + + $header = strtok($status_output, "\n"); $header = false === $header ? '' : trim($header); $branch = null; $detached = false; diff --git a/inc/Workspace/WorkspaceRepositoryLifecycle.php b/inc/Workspace/WorkspaceRepositoryLifecycle.php index 4e40d37..e304b7b 100644 --- a/inc/Workspace/WorkspaceRepositoryLifecycle.php +++ b/inc/Workspace/WorkspaceRepositoryLifecycle.php @@ -712,13 +712,28 @@ public function remove_repo( string $handle ): array|\WP_Error { * @return array{success: bool, name?: string, path?: string, branch?: string, remote?: string, commit?: string, dirty?: int}|\WP_Error */ public function show_repo( string $handle ): array|\WP_Error { - $context_policy = WorkspaceAliasResolver::context_policy_for($handle); + $requested_handle = $handle; + $context_policy = null; + $parsed = $this->parse_handle($handle); + $repo_path = $this->workspace_path . '/' . $parsed['dir_name']; + $inspection = WorkspaceTargetInspector::inspect($repo_path, $parsed['dir_name']); + if ( is_wp_error($inspection) ) { + return $inspection; + } + + if ( empty($inspection['exists']) ) { + $context_policy = WorkspaceAliasResolver::context_policy_for($handle); + } if ( null !== $context_policy ) { $target = (string) ( $context_policy['target'] ?? $handle ); $parsed = $this->parse_handle($target); $repo_path = $this->workspace_path . '/' . $parsed['dir_name']; $ref = (string) ( $context_policy['ref'] ?? '' ); - if ( ! is_dir($repo_path) ) { + $inspection = WorkspaceTargetInspector::inspect($repo_path, $handle); + if ( is_wp_error($inspection) ) { + return $inspection; + } + if ( empty($inspection['exists']) ) { return array( 'success' => true, 'name' => (string) $context_policy['alias'], @@ -736,23 +751,23 @@ public function show_repo( string $handle ): array|\WP_Error { $handle = $target; } - $resolved_handle = $this->resolve_primary_repo_name($handle); - if ( ! is_wp_error($resolved_handle) ) { - $handle = $resolved_handle; + if ( empty($inspection['exists']) && null === $context_policy ) { + $resolved_handle = $this->resolve_primary_repo_name($handle); + if ( ! is_wp_error($resolved_handle) && $resolved_handle !== $handle ) { + $handle = $resolved_handle; + $parsed = $this->parse_handle($handle); + $repo_path = $this->workspace_path . '/' . $parsed['dir_name']; + $inspection = WorkspaceTargetInspector::inspect($repo_path, $parsed['dir_name']); + if ( is_wp_error($inspection) ) { + return $inspection; + } + } } - $parsed = $this->parse_handle($handle); - $repo_path = $this->workspace_path . '/' . $parsed['dir_name']; - - if ( ! is_dir($repo_path) ) { - return new \WP_Error('repo_not_found', sprintf('Workspace handle "%s" not found.', $parsed['dir_name']), array( 'status' => 404 )); + if ( empty($inspection['exists']) ) { + return new \WP_Error('repo_not_found', sprintf('Workspace handle "%s" not found.', $requested_handle), array( 'status' => 404 )); } - $branch = GitRunner::current_branch($repo_path); - $remote = GitRunner::remote_url($repo_path); - $commit = GitRunner::latest_commit_summary($repo_path); - $status = GitRunner::dirty_count($repo_path); - $result = array( 'success' => true, 'name' => null !== $context_policy ? (string) $context_policy['alias'] : $parsed['dir_name'], @@ -760,11 +775,13 @@ public function show_repo( string $handle ): array|\WP_Error { 'is_worktree' => $parsed['is_worktree'], 'is_context' => null !== $context_policy, 'path' => $repo_path, - 'branch' => $branch, - 'remote' => $remote, - 'commit' => $commit, - 'dirty' => $status, - 'primary_freshness' => ! $parsed['is_worktree'] ? $this->build_primary_freshness_report($repo_path, $parsed['dir_name']) : null, + 'branch' => $inspection['branch'] ?? null, + 'remote' => $inspection['remote'] ?? null, + 'commit' => $inspection['commit'] ?? null, + 'dirty' => (int) ( $inspection['dirty'] ?? 0 ), + 'primary_freshness' => ! $parsed['is_worktree'] && is_string($inspection['branch_status'] ?? null) + ? $this->build_primary_freshness_report_from_status_output((string) $inspection['branch_status'], $parsed['dir_name']) + : null, ); if ( null !== $context_policy ) { $result['workspace_policy'] = WorkspaceAliasResolver::policy_attestation($handle); diff --git a/inc/Workspace/WorkspaceTargetInspector.php b/inc/Workspace/WorkspaceTargetInspector.php new file mode 100644 index 0000000..11c34e2 --- /dev/null +++ b/inc/Workspace/WorkspaceTargetInspector.php @@ -0,0 +1,95 @@ +|\WP_Error + */ + public static function inspect( string $path, string $handle ): array|\WP_Error { + $timeout = function_exists('apply_filters') + ? (int) apply_filters('datamachine_code_workspace_target_lookup_timeout_seconds', self::DEFAULT_TIMEOUT_SECONDS, $handle) + : self::DEFAULT_TIMEOUT_SECONDS; + $timeout = max(1, $timeout); + + $diagnostic = array( + 'phase' => 'filesystem', + 'resource' => $path, + 'probe_owner' => 'workspace-show', + 'timeout_seconds' => $timeout, + ); + if ( function_exists('do_action') ) { + do_action('datamachine_code_workspace_lookup_waiting', $diagnostic); + } + + $probe_path = __DIR__ . '/workspace-target-probe.php'; + $filesystem_probe = ''; + $git_command = 'git'; + if ( function_exists('apply_filters') ) { + $filesystem_probe = (string) apply_filters('datamachine_code_workspace_target_filesystem_probe_command', '', $handle, $path); + $git_command = (string) apply_filters('datamachine_code_workspace_target_git_command', 'git', $handle, $path); + } + $command = CommandSpec::from_argv(array( PHP_BINARY, $probe_path, $path, $filesystem_probe, $git_command )); + if ( is_wp_error($command) ) { + return $command; + } + + $result = ProcessRunner::run( + $command, + array( + 'timeout_seconds' => $timeout, + 'separate_streams' => true, + 'output_cap_bytes' => 32768, + 'error_code' => 'workspace_target_lookup_failed', + ) + ); + if ( is_wp_error($result) ) { + $data = is_array($result->get_error_data()) ? $result->get_error_data() : array(); + $stderr = (string) ( $data['stderr'] ?? '' ); + if ( preg_match_all('/^DMC_BOUNDARY:([a-z_]+):([^\r\n]+)/m', $stderr, $matches) && ! empty($matches[1]) ) { + $diagnostic['phase'] = (string) end($matches[1]); + $diagnostic['operation'] = (string) end($matches[2]); + } + + if ( isset($data['timeout']) ) { + return new \WP_Error( + 'workspace_target_lookup_timeout', + sprintf('Workspace lookup for "%s" timed out during the %s probe after %d second(s).', $handle, $diagnostic['phase'], $timeout), + array_merge($data, $diagnostic, array( 'status' => 504 )) + ); + } + + return new \WP_Error( + 'workspace_target_lookup_failed', + sprintf('Workspace lookup for "%s" failed during the %s probe.', $handle, $diagnostic['phase']), + array_merge($data, $diagnostic, array( 'status' => 500 )) + ); + } + + $decoded = json_decode((string) ( $result['stdout'] ?? '' ), true); + if ( ! is_array($decoded) ) { + return new \WP_Error( + 'workspace_target_lookup_invalid_response', + sprintf('Workspace lookup for "%s" returned an invalid probe response.', $handle), + array_merge($diagnostic, array( 'status' => 500 )) + ); + } + + return $decoded; + } +} diff --git a/inc/Workspace/workspace-target-probe.php b/inc/Workspace/workspace-target-probe.php new file mode 100644 index 0000000..30210e5 --- /dev/null +++ b/inc/Workspace/workspace-target-probe.php @@ -0,0 +1,66 @@ + false ), JSON_UNESCAPED_SLASHES)); + exit(0); +} + +/** @return string|null */ +$git_probe = static function ( string $operation, string $args ) use ( $path, $git_command ): ?string { + fwrite(STDERR, 'DMC_BOUNDARY:git:' . $operation . "\n"); + $command = $git_command . ' --no-optional-locks -C ' . escapeshellarg($path) . ' ' . $args . ' 2>&1'; + $output = array(); + $exit = 0; + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Isolated, outer-deadline-controlled read-only probe. + exec($command, $output, $exit); + if ( 0 !== $exit ) { + return null; + } + + return trim(implode("\n", $output)); +}; + +$branch = $git_probe('branch', 'rev-parse --abbrev-ref HEAD'); +$remote = $git_probe('remote', 'config --get ' . escapeshellarg('remote.origin.url')); +$commit = $git_probe('commit', 'log -1 --format=' . escapeshellarg('%h %s')); +$branch_status = $git_probe('status', 'status --porcelain=v1 --branch'); +$status_lines = null === $branch_status ? array() : array_filter(array_map('trim', explode("\n", $branch_status))); +$dirty = count(array_filter($status_lines, static fn ( string $line ): bool => ! str_starts_with($line, '## '))); + +fwrite( + STDOUT, + json_encode( + array( + 'exists' => true, + 'branch' => '' !== (string) $branch ? $branch : null, + 'remote' => '' !== (string) $remote ? $remote : null, + 'commit' => '' !== (string) $commit ? $commit : null, + 'dirty' => $dirty, + 'branch_status' => $branch_status, + ), + JSON_UNESCAPED_SLASHES + ) +); diff --git a/tests/workspace-command-startup-bounds.php b/tests/workspace-command-startup-bounds.php new file mode 100644 index 0000000..9f3fdac --- /dev/null +++ b/tests/workspace-command-startup-bounds.php @@ -0,0 +1,213 @@ +code; } + public function get_error_message(): string { return $this->message; } + public function get_error_data(): mixed { return $this->data; } + } + + final class WP_CLI { + /** @var array */ + public static array $commands = array(); + /** @var list */ + public static array $output = array(); + + public static function add_command( string $name, string $class ): void { + self::$commands[ $name ] = $class; + } + + public static function log( string $message ): void { self::$output[] = $message; } + public static function warning( string $message ): void { self::$output[] = $message; } + public static function success( string $message ): void { self::$output[] = $message; } + public static function error( string $message ): void { throw new \RuntimeException($message); } + } + + /** @var array> */ + $GLOBALS['dmc_test_actions'] = array(); + $GLOBALS['dmc_test_get_option_calls'] = 0; + $GLOBALS['dmc_test_mutation_calls'] = 0; + $GLOBALS['dmc_test_options'] = array(); + $GLOBALS['dmc_test_filters'] = array(); + + function startup_bounds_assert( bool $condition, string $message ): void { + if ( ! $condition ) { + throw new \RuntimeException($message); + } + } + + function is_wp_error( mixed $value ): bool { return $value instanceof WP_Error; } + function plugin_dir_path( string $file ): string { return dirname($file) . '/'; } + function plugin_dir_url( string $file ): string { return 'https://example.test/' . basename(dirname($file)) . '/'; } + function register_activation_hook( string $file, callable|string $callback ): void {} + function wp_installing(): bool { return false; } + function add_action( string $hook, callable|string|array $callback, int $priority = 10, int $accepted_args = 1 ): void { + $GLOBALS['dmc_test_actions'][ $hook ][] = array( 'priority' => $priority, 'callback' => $callback ); + } + function add_filter( string $hook, callable|string|array $callback, int $priority = 10, int $accepted_args = 1 ): void { + add_action($hook, $callback, $priority, $accepted_args); + } + function apply_filters( string $hook, mixed $value, mixed ...$args ): mixed { + if ( 'datamachine_code_workspace_target_lookup_timeout_seconds' === $hook ) { + return 1; + } + if ( array_key_exists($hook, $GLOBALS['dmc_test_filters']) ) { + return $GLOBALS['dmc_test_filters'][ $hook ]; + } + return $value; + } + function do_action( string $hook, mixed ...$args ): void { + $callbacks = $GLOBALS['dmc_test_actions'][ $hook ] ?? array(); + usort($callbacks, static fn ( array $left, array $right ): int => $left['priority'] <=> $right['priority']); + foreach ( $callbacks as $entry ) { + call_user_func_array($entry['callback'], $args); + } + } + function did_action( string $hook ): int { return 0; } + function get_option( string $key, mixed $default = false ): mixed { + ++$GLOBALS['dmc_test_get_option_calls']; + return $GLOBALS['dmc_test_options'][ $key ] ?? $default; + } + function update_option( string $key, mixed $value, bool $autoload = false ): bool { + ++$GLOBALS['dmc_test_mutation_calls']; + $GLOBALS['dmc_test_options'][ $key ] = $value; + return true; + } + function __( string $text, string $domain = '' ): string { return $text; } + + function startup_bounds_remove_tree( string $path ): void { + if ( ! is_dir($path) ) { + return; + } + foreach ( scandir($path) ?: array() as $entry ) { + if ( '.' === $entry || '..' === $entry ) { + continue; + } + $child = $path . '/' . $entry; + is_dir($child) ? startup_bounds_remove_tree($child) : unlink($child); + } + rmdir($path); + } + + $root = dirname(__DIR__); + $workspace = sys_get_temp_dir() . '/dmc-startup-bounds-' . bin2hex(random_bytes(6)); + mkdir($workspace, 0777, true); + mkdir($workspace . '/target', 0777, true); + for ( $index = 0; $index < 600; ++$index ) { + mkdir($workspace . '/unrelated-' . str_pad((string) $index, 4, '0', STR_PAD_LEFT)); + } + + define('WP_CLI', true); + define('WPINC', 'wp-includes'); + define('ABSPATH', $root . '/tests/fixtures/'); + define('DATAMACHINE_WORKSPACE_PATH', $workspace); + $GLOBALS['argv'] = array( 'wp', 'datamachine-code', 'workspace', 'worktree', 'add', '--help' ); + + require_once $root . '/data-machine-code.php'; + do_action('plugins_loaded'); + + startup_bounds_assert(isset(WP_CLI::$commands['datamachine-code workspace']), 'Nested help did not register the workspace command for WP-CLI dispatch.'); + startup_bounds_assert(0 === $GLOBALS['dmc_test_get_option_calls'], 'Nested help initialized database-backed discovery.'); + startup_bounds_assert(0 === $GLOBALS['dmc_test_mutation_calls'], 'Nested help mutated schema or registry state.'); + foreach ( array( + 'DataMachineCode\\Abilities\\WorkspaceAbilities', + 'DataMachineCode\\Abilities\\GitHubAbilities', + 'DataMachineCode\\Storage\\WorktreeInventoryRepository', + 'DataMachineCode\\Workspace\\Workspace', + 'DataMachineCode\\Workspace\\WorkspaceMutationLock', + 'DataMachineCode\\Support\\GitRunner', + 'DataMachineCode\\Workspace\\RemoteWorkspaceBackend', + ) as $service_class ) { + startup_bounds_assert(! class_exists($service_class, false), sprintf('Nested help initialized %s.', $service_class)); + } + + // Dispatch the registered command exactly as WP-CLI does. Targeted show must + // not depend on the full Abilities API bootstrap that was skipped above. + $GLOBALS['argv'] = array( 'wp', 'datamachine-code', 'workspace', 'show', 'target' ); + $before_entries = scandir($workspace); + $started = microtime(true); + $command_class = WP_CLI::$commands['datamachine-code workspace']; + $command = new $command_class(); + $command->show(array( 'target' ), array()); + $elapsed = microtime(true) - $started; + startup_bounds_assert(in_array('Path: ' . $workspace . '/target', WP_CLI::$output, true), 'Real workspace show dispatch returned the wrong path.'); + startup_bounds_assert($elapsed < 3.0, sprintf('Targeted show exceeded its startup bound: %.3fs.', $elapsed)); + startup_bounds_assert(0 === $GLOBALS['dmc_test_get_option_calls'], 'Existing local targeted show consulted registry or remote backend state.'); + startup_bounds_assert($before_entries === scandir($workspace), 'Targeted show changed workspace state.'); + + $stall_probe = $workspace . '/stall-boundary.php'; + file_put_contents( + $stall_probe, + ' 'datamachine_code_workspace_target_filesystem_probe_command', + 'git' => 'datamachine_code_workspace_target_git_command', + ) as $phase => $filter ) { + $GLOBALS['dmc_test_filters'][ $filter ] = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($stall_probe); + $started = microtime(true); + $error = \DataMachineCode\Workspace\WorkspaceTargetInspector::inspect($workspace . '/target', 'target'); + $elapsed = microtime(true) - $started; + startup_bounds_assert(is_wp_error($error), sprintf('Stalled %s dependency did not return an error.', $phase)); + startup_bounds_assert('workspace_target_lookup_timeout' === $error->get_error_code(), sprintf('Stalled %s dependency returned an untyped error.', $phase)); + $data = $error->get_error_data(); + startup_bounds_assert($phase === ( $data['phase'] ?? null ), sprintf('Stalled %s diagnostic lost its blocked phase.', $phase)); + startup_bounds_assert($workspace . '/target' === ( $data['resource'] ?? null ), sprintf('Stalled %s diagnostic lost its resource.', $phase)); + startup_bounds_assert('workspace-show' === ( $data['probe_owner'] ?? null ), sprintf('Stalled %s diagnostic lost its probe owner.', $phase)); + startup_bounds_assert(('filesystem' === $phase ? 'is_dir' : 'branch') === ( $data['operation'] ?? null ), sprintf('Stalled %s diagnostic lost its operation.', $phase)); + startup_bounds_assert($elapsed < 2.5, sprintf('Stalled %s dependency exceeded its bound: %.3fs.', $phase, $elapsed)); + unset($GLOBALS['dmc_test_filters'][ $filter ]); + } + startup_bounds_assert($state_before_failures === scandir($workspace), 'A failed startup probe left partial worktree state.'); + + // Exercise the actual option/database and remote-backend boundaries. A local + // miss and a context alias must still resolve registered remote state. + $GLOBALS['dmc_test_options']['datamachine_code_remote_workspace_state'] = array( + 'repos' => array( + 'remote-target' => array( 'repo' => 'owner/remote-target', 'url' => 'https://github.com/owner/remote-target.git' ), + 'timeout-target' => array( 'repo' => 'owner/timeout-target', 'url' => 'https://github.com/owner/timeout-target.git' ), + ), + 'repo_names' => array( + 'owner/remote-target' => 'remote-target', + 'owner/timeout-target' => 'timeout-target', + ), + 'worktrees' => array(), + ); + $database_calls = $GLOBALS['dmc_test_get_option_calls']; + $remote = \DataMachineCode\Abilities\WorkspaceAbilities::showRepo(array( 'name' => 'remote-target' )); + startup_bounds_assert(! is_wp_error($remote) && 'github_api' === ( $remote['backend'] ?? null ), 'Local miss did not preserve registered remote workspace behavior.'); + startup_bounds_assert($GLOBALS['dmc_test_get_option_calls'] > $database_calls, 'Remote show did not cross the database option boundary.'); + + mkdir($workspace . '/timeout-target'); + $GLOBALS['dmc_test_filters']['datamachine_code_workspace_target_git_command'] = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($stall_probe); + $timed_out_local = \DataMachineCode\Abilities\WorkspaceAbilities::showRepo(array( 'name' => 'timeout-target' )); + startup_bounds_assert(! is_wp_error($timed_out_local) && 'github_api' === ( $timed_out_local['backend'] ?? null ), 'Bounded local timeout did not preserve registered remote workspace behavior.'); + unset($GLOBALS['dmc_test_filters']['datamachine_code_workspace_target_git_command']); + + $GLOBALS['dmc_test_filters']['datamachine_code_context_repositories'] = array( + 'remote-context' => array( 'alias' => 'remote-context', 'repo' => 'owner/remote-target', 'target' => 'remote-target', 'ref' => 'main' ), + ); + $context = \DataMachineCode\Abilities\WorkspaceAbilities::showRepo(array( 'name' => 'remote-context' )); + startup_bounds_assert(! is_wp_error($context) && 'github_api' === ( $context['backend'] ?? null ), 'Context alias did not preserve registered remote workspace behavior.'); + startup_bounds_assert(! empty($context['is_context']), 'Remote context alias lost its context policy.'); + unset($GLOBALS['dmc_test_filters']['datamachine_code_context_repositories']); + + startup_bounds_assert(! is_dir($workspace . '/.locks'), 'Read/help startup acquired a mutation lock.'); + startup_bounds_assert(0 === $GLOBALS['dmc_test_mutation_calls'], 'A failed startup probe left partial registry state.'); + + startup_bounds_remove_tree($workspace); + echo "workspace-command-startup-bounds: ok\n"; +}