From 3bf8db5f4b13faf7e0c7266965b7c3887e4f1afc Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 30 Aug 2026 00:19:00 -0400 Subject: [PATCH] Compact routine capacity advisories --- inc/Abilities/WorkspaceAbilities.php | 8 ++-- inc/Cli/Commands/WorkspaceCommand.php | 19 +++++---- inc/Cli/WorkspaceCompactOutput.php | 12 ++++++ .../WorkspaceRepositoryLifecycle.php | 4 +- inc/Workspace/WorktreeDiskBudget.php | 42 ++++++++++++++++++- tests/workspace-capacity-advisory.php | 16 +++++++ tests/workspace-command-startup-bounds.php | 12 ++++-- tests/workspace-list-cli-format-contract.php | 10 ++++- tests/workspace-show-cli-format-contract.php | 16 ++++++- tests/worktree-disk-budget.php | 4 ++ 10 files changed, 122 insertions(+), 21 deletions(-) diff --git a/inc/Abilities/WorkspaceAbilities.php b/inc/Abilities/WorkspaceAbilities.php index b877e35b..ab0ebce7 100644 --- a/inc/Abilities/WorkspaceAbilities.php +++ b/inc/Abilities/WorkspaceAbilities.php @@ -167,7 +167,7 @@ private function registerAbilities(): void { 'summary' => array( 'type' => 'object' ), 'workspace_capacity' => array( 'type' => 'object', - 'description' => 'One command-level, lossless workspace capacity envelope with a stable diagnostic ID, advisory fingerprint, evidence reference, and recovery actions.', + 'description' => 'Compact command-level capacity status with typed triggers and an evidence reference. Full capacity evidence remains on workspace hygiene.', ), 'repos' => array( 'type' => 'array', @@ -272,7 +272,7 @@ private function registerAbilities(): void { 'dirty' => array( 'type' => 'integer' ), 'workspace_capacity' => array( 'type' => 'object', - 'description' => 'Complete workspace capacity envelope including byte and inode total/used/free values and percentages, probe, status, warnings, reasons, thresholds, and remediation commands.', + 'description' => 'Compact workspace capacity status and evidence reference for routine reads. Blocking states retain complete evidence; hygiene owns the full dossier.', ), 'primary_freshness' => self::primaryFreshnessSchema(), ), @@ -3511,7 +3511,7 @@ public static function listRepos( array $input ): array|\WP_Error { if ( is_wp_error( $result ) ) { return $result; } - $result['workspace_capacity'] = WorktreeDiskBudget::inspect($workspace->get_path()); + $result['workspace_capacity'] = WorktreeDiskBudget::for_routine_read( WorktreeDiskBudget::inspect( $workspace->get_path() ) ); return $result; } @@ -3593,7 +3593,7 @@ public static function showRepo( array $input ): array|\WP_Error { $result = ( new RemoteWorkspaceBackend() )->show( $handle ); if ( ! self::shouldFallbackToLocalWorkspace( $result ) ) { if ( is_array( $result ) && is_dir( $workspace->get_path() ) ) { - $result['workspace_capacity'] = WorktreeDiskBudget::inspect( $workspace->get_path() ); + $result['workspace_capacity'] = WorktreeDiskBudget::for_routine_read( WorktreeDiskBudget::inspect( $workspace->get_path() ) ); } return $result; } diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index ba80d81c..a414686a 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -759,7 +759,7 @@ public function path( array $args, array $assoc_args ): void { * : Include per-row Git remote, branch, and primary freshness probes. * * [--full] - * : Render full disk/inode capacity evidence and recovery details instead of one compact advisory. + * : Expand complete capacity evidence when the command already produced it. Routine list output stays compact; use workspace hygiene for the full dossier. * * [--format=] * : Output format. @@ -840,7 +840,11 @@ public function list_repos( array $args, array $assoc_args ): void { } if ( 'json' === ( $assoc_args['format'] ?? 'table' ) ) { - $this->renderer()->json( ! empty( $assoc_args['envelope'] ) ? $result : (array) ( $result['repos'] ?? array() ) ); + $payload = ! empty( $assoc_args['envelope'] ) ? $result : (array) ( $result['repos'] ?? array() ); + if ( ! empty( $assoc_args['envelope'] ) && empty( $assoc_args['full'] ) ) { + $payload = WorkspaceCompactOutput::workspace_read_result($payload); + } + $this->renderer()->json($payload); return; } @@ -1073,7 +1077,7 @@ private function render_workspace_list_summary( array $result, array $assoc_args $format = (string) ( $assoc_args['format'] ?? 'table' ); if ( 'json' === $format ) { - $this->renderer()->json( $summary ); + $this->renderer()->json(empty($assoc_args['full']) ? WorkspaceCompactOutput::workspace_read_result($summary) : $summary); return; } if ( 'csv' === $format || 'yaml' === $format ) { @@ -3243,7 +3247,7 @@ private function inventory_prune_missing( array $assoc_args ): void { * : Repository directory name. * * [--full] - * : Render full disk/inode capacity evidence and recovery details instead of one compact advisory. + * : Expand complete capacity evidence when the command already produced it. Routine show output stays compact; use workspace hygiene for the full dossier. * * [--refresh] * : Fetch the tracked remote under a bounded timeout before classifying primary freshness. @@ -3296,7 +3300,7 @@ public function show( array $args, array $assoc_args ): void { } if ( 'json' === $format ) { - $this->renderer()->json( $result ); + $this->renderer()->json(empty($assoc_args['full']) ? WorkspaceCompactOutput::workspace_read_result($result) : $result); return; } @@ -3341,8 +3345,9 @@ private function render_workspace_capacity_advisory( array $capacity, bool $full if ( array() === $capacity || array() === (array) ( $capacity['trigger_reasons'] ?? array() ) ) { return; } - $blocking = empty( $capacity['creation_allowed'] ) || ! empty( $capacity['force_override_required'] ); - if ( ! $full && ! $blocking ) { + $blocking = empty( $capacity['creation_allowed'] ) || ! empty( $capacity['force_override_required'] ); + $has_full_evidence = array_key_exists( 'filesystem_free_bytes', $capacity ) && array_key_exists( 'warn_free_bytes', $capacity ); + if ( ! $blocking && ( ! $full || ! $has_full_evidence ) ) { $advisory = \DataMachineCode\Workspace\WorktreeDiskBudget::format_advisory( $capacity ); if ( '' !== $advisory ) { WP_CLI::warning( $advisory ); diff --git a/inc/Cli/WorkspaceCompactOutput.php b/inc/Cli/WorkspaceCompactOutput.php index 1f31efda..36058adc 100644 --- a/inc/Cli/WorkspaceCompactOutput.php +++ b/inc/Cli/WorkspaceCompactOutput.php @@ -50,6 +50,15 @@ public static function worktree_add_result( array $result ): array { ); } + /** Project routine workspace reads without embedding the full capacity dossier. */ + public static function workspace_read_result( array $result ): array { + if ( isset($result['workspace_capacity']) && is_array($result['workspace_capacity']) ) { + $result['workspace_capacity'] = self::worktree_capacity_summary($result['workspace_capacity']); + } + + return $result; + } + private static function worktree_capacity_summary( array $capacity ): array { if ( array() === $capacity ) { return array(); @@ -74,7 +83,10 @@ static function ( string $code ): array { 'diagnostic_id' => $capacity['diagnostic_id'] ?? null, 'advisory_fingerprint' => $capacity['advisory_fingerprint'] ?? null, 'evidence_reference' => $capacity['evidence_reference'] ?? null, + 'evidence_command' => $capacity['evidence_command'] ?? null, 'status' => $capacity['status'] ?? null, + 'worktree_count' => $capacity['worktree_count'] ?? null, + 'emergency_triggered' => isset($capacity['emergency_triggered']) ? (bool) $capacity['emergency_triggered'] : null, 'force_override' => isset( $capacity['force_override'] ) ? (bool) $capacity['force_override'] : null, 'creation_allowed' => array_key_exists('creation_allowed', $capacity) ? (bool) $capacity['creation_allowed'] : ( 'refused' !== ( $capacity['status'] ?? '' ) ), 'force_override_required' => array_key_exists('force_override_required', $capacity) ? (bool) $capacity['force_override_required'] : $has_blocking_trigger, diff --git a/inc/Workspace/WorkspaceRepositoryLifecycle.php b/inc/Workspace/WorkspaceRepositoryLifecycle.php index 170507a7..f5f8ede5 100644 --- a/inc/Workspace/WorkspaceRepositoryLifecycle.php +++ b/inc/Workspace/WorkspaceRepositoryLifecycle.php @@ -1036,7 +1036,7 @@ public function show_repo( string $handle, bool $refresh = false ): array|\WP_Er 'remote' => '' !== (string) ( $context_policy['repo'] ?? '' ) ? GitHubRemote::cloneUrl( (string) $context_policy['repo'] ) : null, 'commit' => null, 'dirty' => 0, - 'workspace_capacity' => WorktreeDiskBudget::inspect($this->workspace_path), + 'workspace_capacity' => WorktreeDiskBudget::for_routine_read(WorktreeDiskBudget::inspect($this->workspace_path)), 'workspace_policy' => WorkspaceAliasResolver::policy_attestation($handle), ); } @@ -1076,7 +1076,7 @@ public function show_repo( string $handle, bool $refresh = false ): array|\WP_Er $remote_freshness_ms = (int) round(( microtime(true) - $remote_freshness_started ) * 1000); $capacity_started = microtime(true); - $capacity = WorktreeDiskBudget::inspect($this->workspace_path); + $capacity = WorktreeDiskBudget::for_routine_read(WorktreeDiskBudget::inspect($this->workspace_path)); $capacity_ms = (int) round(( microtime(true) - $capacity_started ) * 1000); $result = array( 'success' => true, diff --git a/inc/Workspace/WorktreeDiskBudget.php b/inc/Workspace/WorktreeDiskBudget.php index 7114e5d7..547f54de 100644 --- a/inc/Workspace/WorktreeDiskBudget.php +++ b/inc/Workspace/WorktreeDiskBudget.php @@ -362,7 +362,7 @@ public static function evaluate( array $metrics, array $thresholds = array(), bo 'creation_allowed' => ! $refused, 'admission_exception' => $admission_exception, 'warnings' => $warnings, - 'emergency_triggered' => array() !== $trigger_reasons, + 'emergency_triggered' => array() !== $has_blocking_trigger, 'trigger_reasons' => $trigger_reasons, 'typed_trigger_reasons' => $typed_trigger_reasons, 'cleanup_dry_run_command' => 'studio wp datamachine-code workspace worktree cleanup --dry-run', @@ -400,6 +400,46 @@ public static function evaluate( array $metrics, array $thresholds = array(), bo return $budget; } + /** + * Project a compact status for routine list/show reads. + * + * Blocking and emergency payloads stay complete so immediate remediation remains available. + * + * @param array $budget Full capacity evidence. + * @return array + */ + public static function for_routine_read( array $budget ): array { + if ( ! empty( $budget['emergency_triggered'] ) || empty( $budget['creation_allowed'] ) || ! empty( $budget['force_override_required'] ) ) { + return $budget; + } + + return self::compact_status( $budget ); + } + + /** + * Compact status and evidence reference for routine workspace reads. + * + * @param array $budget Full or compact capacity evidence. + * @return array + */ + public static function compact_status( array $budget ): array { + return array( + 'workspace_path' => (string) ( $budget['workspace_path'] ?? '' ), + 'worktree_count' => isset( $budget['worktree_count'] ) && is_numeric( $budget['worktree_count'] ) ? (int) $budget['worktree_count'] : 0, + 'status' => (string) ( $budget['status'] ?? 'unknown' ), + 'creation_allowed' => ! empty( $budget['creation_allowed'] ), + 'force_override_required' => ! empty( $budget['force_override_required'] ), + 'force_override_applied' => ! empty( $budget['force_override_applied'] ), + 'emergency_triggered' => ! empty( $budget['emergency_triggered'] ), + 'trigger_reasons' => array_values( array_map( 'strval', (array) ( $budget['trigger_reasons'] ?? array() ) ) ), + 'typed_trigger_reasons' => array_values( (array) ( $budget['typed_trigger_reasons'] ?? array() ) ), + 'diagnostic_id' => (string) ( $budget['diagnostic_id'] ?? self::DIAGNOSTIC_ID ), + 'advisory_fingerprint' => isset( $budget['advisory_fingerprint'] ) ? (string) $budget['advisory_fingerprint'] : null, + 'evidence_reference' => isset( $budget['evidence_reference'] ) ? (string) $budget['evidence_reference'] : null, + 'evidence_command' => (string) ( $budget['evidence_command'] ?? 'studio wp datamachine-code workspace hygiene --format=json' ), + ); + } + /** Build a state-level fingerprint so unchanged advisories can be suppressed safely. */ private static function advisory_fingerprint( array $budget ): string { $thresholds = array(); diff --git a/tests/workspace-capacity-advisory.php b/tests/workspace-capacity-advisory.php index 8a952ce1..26f1720b 100644 --- a/tests/workspace-capacity-advisory.php +++ b/tests/workspace-capacity-advisory.php @@ -33,10 +33,26 @@ function capacity_advisory_assert( bool $condition, string $message ): void { $measurement_warning = WorktreeDiskBudget::evaluate(array_merge($metrics, array( 'free_bytes' => null ))); capacity_advisory_assert('workspace_capacity' === ($warning['diagnostic_id'] ?? null), 'Capacity evidence must expose a stable diagnostic ID.'); +capacity_advisory_assert(false === ($warning['emergency_triggered'] ?? true), 'Healthy disk with advisory worktree count must not set emergency_triggered.'); +capacity_advisory_assert('advisory' === ($warning['typed_trigger_reasons'][0]['severity'] ?? null), 'Worktree-count pressure must stay typed advisory.'); +capacity_advisory_assert(true === ($blocked['emergency_triggered'] ?? false), 'Refusal floors must reserve emergency_triggered for blocking thresholds.'); capacity_advisory_assert(($warning['advisory_fingerprint'] ?? null) === ($same_warning['advisory_fingerprint'] ?? null), 'Unchanged warning state must retain a suppressible fingerprint.'); capacity_advisory_assert(($warning['advisory_fingerprint'] ?? null) !== ($new_threshold['advisory_fingerprint'] ?? null), 'A changed active threshold must produce a new fingerprint.'); capacity_advisory_assert(($warning['advisory_fingerprint'] ?? null) !== ($blocked['advisory_fingerprint'] ?? null), 'A blocking state change must produce a new fingerprint.'); capacity_advisory_assert(str_starts_with((string) ($warning['evidence_reference'] ?? ''), 'workspace_capacity@'), 'Capacity evidence must expose a compact reference.'); + +$healthy = WorktreeDiskBudget::evaluate(array_merge($metrics, array( 'worktree_count' => 12 ))); +$advisory_read = WorktreeDiskBudget::for_routine_read($warning); +$healthy_read = WorktreeDiskBudget::for_routine_read($healthy); +$blocked_read = WorktreeDiskBudget::for_routine_read($blocked); +capacity_advisory_assert(false === ($healthy['emergency_triggered'] ?? true) && 'ok' === ($healthy['status'] ?? null), 'Healthy capacity must remain non-emergency.'); +capacity_advisory_assert($advisory_read === WorktreeDiskBudget::compact_status($warning), 'Routine list/show must compact healthy-disk advisory count status.'); +capacity_advisory_assert(! isset($advisory_read['filesystem_free_bytes']) && ! isset($advisory_read['cleanup_recommendations']) && ! isset($advisory_read['emergency_cleanup_command']) && ! isset($advisory_read['recovery_actions']), 'Routine advisory reads must omit the capacity dossier and cleanup plans.'); +capacity_advisory_assert('studio wp datamachine-code workspace hygiene --format=json' === ($advisory_read['evidence_command'] ?? null), 'Routine reads must retain the hygiene evidence command.'); +capacity_advisory_assert(false === ($advisory_read['emergency_triggered'] ?? true) && 'advisory' === ($advisory_read['typed_trigger_reasons'][0]['severity'] ?? null), 'Compact advisory count status must keep typed advisory severity.'); +capacity_advisory_assert(! isset($healthy_read['filesystem_free_bytes']) && 'ok' === ($healthy_read['status'] ?? null), 'Healthy routine reads must stay compact.'); +capacity_advisory_assert(isset($blocked_read['filesystem_free_bytes']) && isset($blocked_read['cleanup_recommendations']) && true === ($blocked_read['emergency_triggered'] ?? false), 'Blocking routine reads must retain complete emergency evidence.'); +capacity_advisory_assert(! str_contains(WorktreeDiskBudget::format_advisory($advisory_read), 'workspace worktree prune'), 'Compact routine advisory must not embed cleanup plans.'); capacity_advisory_assert(4 === count((array) ($warning['recovery_actions'] ?? array())), 'Worktree-count warnings must add bounded cleanup and registration previews.'); capacity_advisory_assert('studio wp datamachine-code workspace worktree cleanup-eligible-drain --limit=25 --format=json' === ($warning['recovery_actions'][2]['command'] ?? null), 'Worktree-count warnings must point to bounded cleanup preview.'); capacity_advisory_assert('studio wp datamachine-code workspace worktree prune --dry-run --format=json' === ($warning['recovery_actions'][3]['command'] ?? null), 'Worktree-count warnings must point to the non-mutating prune preview.'); diff --git a/tests/workspace-command-startup-bounds.php b/tests/workspace-command-startup-bounds.php index 07d48571..ff8af66c 100644 --- a/tests/workspace-command-startup-bounds.php +++ b/tests/workspace-command-startup-bounds.php @@ -218,9 +218,10 @@ function startup_bounds_remove_tree( string $path ): void { startup_bounds_assert(! is_wp_error($produced_show), 'WorkspaceAbilities::showRepo did not produce the bounded local result.'); $produced_capacity = $produced_show['workspace_capacity'] ?? null; startup_bounds_assert(is_array($produced_capacity), 'WorkspaceAbilities::showRepo did not emit workspace_capacity.'); - foreach ( array( 'workspace_path', 'filesystem_free_bytes', 'filesystem_total_bytes', 'worktree_count', 'status', 'warnings', 'trigger_reasons', 'typed_trigger_reasons', 'creation_allowed', 'diagnostic_id', 'advisory_fingerprint', 'evidence_reference', 'recovery_actions' ) as $field ) { + foreach ( array( 'workspace_path', 'worktree_count', 'status', 'trigger_reasons', 'typed_trigger_reasons', 'creation_allowed', 'emergency_triggered', 'diagnostic_id', 'advisory_fingerprint', 'evidence_reference', 'evidence_command' ) as $field ) { startup_bounds_assert(array_key_exists($field, $produced_capacity), sprintf('WorkspaceAbilities::showRepo emitted an incomplete workspace_capacity: missing %s.', $field)); } + startup_bounds_assert(! array_key_exists('filesystem_free_bytes', $produced_capacity) && ! array_key_exists('cleanup_recommendations', $produced_capacity) && ! array_key_exists('recovery_actions', $produced_capacity), 'Routine show must omit the full capacity dossier.'); startup_bounds_assert($workspace === $produced_capacity['workspace_path'], 'WorkspaceAbilities::showRepo emitted capacity for the wrong workspace.'); startup_bounds_assert(176 === $produced_capacity['worktree_count'], 'WorkspaceAbilities::showRepo did not preserve the large-workspace capacity count.'); $unrelated_probes = array_filter( @@ -241,6 +242,8 @@ function startup_bounds_remove_tree( string $path ): void { 'Targeted show timing profile did not retain every required phase.' ); startup_bounds_assert('warning' === $produced_capacity['status'], 'WorkspaceAbilities::showRepo did not preserve the fixture capacity status.'); + startup_bounds_assert(false === ($produced_capacity['emergency_triggered'] ?? true), 'Routine show must not label advisory worktree count as an emergency.'); + startup_bounds_assert('advisory' === ($produced_capacity['typed_trigger_reasons'][0]['severity'] ?? null), 'Routine show must keep worktree-count pressure typed advisory.'); startup_bounds_assert(in_array('worktree_count_warning_threshold', $produced_capacity['trigger_reasons'], true), 'WorkspaceAbilities::showRepo did not emit the worktree capacity trigger.'); $produced_reasons = \DataMachineCode\Workspace\WorktreeDiskBudget::format_trigger_reasons($produced_capacity); $capacity_renderer = new \ReflectionMethod($command, 'render_workspace_capacity_advisory'); @@ -262,15 +265,16 @@ function startup_bounds_remove_tree( string $path ): void { startup_bounds_assert($warning_elapsed < 3.0, sprintf('Warning targeted show exceeded its startup bound: %.3fs.', $warning_elapsed)); startup_bounds_assert(! str_contains($warning_output, '--force'), 'Warning targeted show suggested bypassing capacity protection.'); startup_bounds_assert(str_contains($warning_output, 'workspace hygiene --format=json'), 'Warning targeted show did not emit the generic hygiene next step.'); - startup_bounds_assert(str_contains($warning_output, 'cleanup-eligible-drain --limit=25') && str_contains($warning_output, 'worktree prune --dry-run'), 'Worktree-count warning did not expose bounded, preview-only remediation.'); + startup_bounds_assert(! str_contains($warning_output, 'cleanup-eligible-drain') && ! str_contains($warning_output, 'worktree prune --dry-run'), 'Routine show must not embed cleanup plans in the compact advisory.'); startup_bounds_assert(! str_contains($warning_output, '--apply'), 'Worktree-count warning suggested applying cleanup before review.'); startup_bounds_assert(! str_contains($warning_output, 'workspace worktree locks'), 'Warning targeted show inferred stale locks without observed lane state.'); WP_CLI::$output = array(); $capacity_renderer->invoke($command, $produced_capacity, true); $full_warning_output = implode("\n", WP_CLI::$output); - startup_bounds_assert(str_contains($full_warning_output, 'Disk budget: ') && str_contains($full_warning_output, 'Recovery for the listed capacity warning(s)'), 'Workspace show --full did not retain complete warning evidence and recovery.'); - startup_bounds_assert(str_contains($full_warning_output, 'workspace hygiene --include-sizes --size-limit=100'), 'Workspace show --full did not retain bounded size inspection.'); + startup_bounds_assert(1 === count(array_filter(WP_CLI::$output, static fn ( string $line ): bool => str_starts_with($line, 'Capacity advisory ['))), 'Workspace show --full must keep compact advisory output when only compact evidence is present.'); + startup_bounds_assert(! str_contains($full_warning_output, 'Disk budget: ') && ! str_contains($full_warning_output, 'Recovery for the listed capacity warning(s)'), 'Workspace show --full must not invent a capacity dossier from compact status.'); + startup_bounds_assert(str_contains($full_warning_output, 'workspace hygiene --format=json'), 'Workspace show --full must still point at hygiene for full evidence.'); $GLOBALS['dmc_test_disk_free_bytes'] = (float) ( 5 * 1024 * 1024 * 1024 ); WP_CLI::$output = array(); diff --git a/tests/workspace-list-cli-format-contract.php b/tests/workspace-list-cli-format-contract.php index e1be72df..73283049 100644 --- a/tests/workspace-list-cli-format-contract.php +++ b/tests/workspace-list-cli-format-contract.php @@ -58,6 +58,7 @@ public function execute( array $input ): array { return $this->result; } function wp_get_ability( string $name ): ?WorkspaceListAbility { return $GLOBALS['dmc_workspace_list_ability']; } require_once dirname(__DIR__) . '/inc/Cli/CliResponseRenderer.php'; + require_once dirname(__DIR__) . '/inc/Cli/WorkspaceCompactOutput.php'; require_once dirname(__DIR__) . '/inc/Cli/Commands/WorkspaceCommand.php'; require_once dirname(__DIR__) . '/inc/Abilities/WorkspaceAbilities.php'; @@ -129,14 +130,19 @@ function cli_format_reset(): void { $command->list_repos(array(), array( 'format' => 'json', 'envelope' => true )); $envelope_json = json_decode(WP_CLI::$lines[0] ?? '', true); cli_format_assert(100 === ($envelope_json['total'] ?? null) && 'cursor-2' === ($envelope_json['next_cursor'] ?? null), 'Envelope JSON must explicitly expose pagination metadata.'); - cli_format_assert('abc' === ($envelope_json['workspace_capacity']['advisory_fingerprint'] ?? null) && ! isset($envelope_json['repos'][0]['workspace_capacity']), 'Envelope JSON must retain one deduplicated command-level capacity evidence object.'); + cli_format_assert('abc' === ($envelope_json['workspace_capacity']['advisory_fingerprint'] ?? null) && ! isset($envelope_json['workspace_capacity']['advisory']) && ! isset($envelope_json['repos'][0]['workspace_capacity']), 'Envelope JSON must retain one compact command-level capacity evidence object.'); cli_format_reset(); $command->list_repos(array(), array( 'summary' => true, 'format' => 'json' )); $summary_json = json_decode(WP_CLI::$lines[0] ?? '', true); cli_format_assert(100 === ($summary_json['total'] ?? null) && ! isset($summary_json['repos'][0]['name']), 'Summary JSON must serialize the full aggregate summary, not the current page.'); cli_format_assert(2 === ($summary_json['returned'] ?? null) && 'cursor-2' === ($summary_json['next_cursor'] ?? null), 'Summary JSON must retain page continuation metadata.'); - cli_format_assert('abc' === ($summary_json['workspace_capacity']['advisory_fingerprint'] ?? null), 'Summary JSON must retain lossless command-level capacity evidence.'); + cli_format_assert('abc' === ($summary_json['workspace_capacity']['advisory_fingerprint'] ?? null) && ! isset($summary_json['workspace_capacity']['advisory']), 'Summary JSON must retain compact command-level capacity evidence.'); + + cli_format_reset(); + $command->list_repos(array(), array( 'summary' => true, 'format' => 'json', 'full' => true )); + $full_summary_json = json_decode(WP_CLI::$lines[0] ?? '', true); + cli_format_assert('Capacity advisory [workspace_capacity@abc]: admission allowed.' === ($full_summary_json['workspace_capacity']['advisory'] ?? null), 'Summary --full JSON must retain complete capacity evidence.'); cli_format_reset(); $command->list_repos(array(), array( 'summary' => true )); diff --git a/tests/workspace-show-cli-format-contract.php b/tests/workspace-show-cli-format-contract.php index e8b6f1a1..9ed52055 100644 --- a/tests/workspace-show-cli-format-contract.php +++ b/tests/workspace-show-cli-format-contract.php @@ -56,6 +56,7 @@ public static function halt( int $status ): never { throw new Workspace_Show_Cli define( 'ABSPATH', __DIR__ . '/fixtures/' ); require_once dirname( __DIR__ ) . '/inc/Cli/CliResponseRenderer.php'; + require_once dirname( __DIR__ ) . '/inc/Cli/WorkspaceCompactOutput.php'; require_once dirname( __DIR__ ) . '/inc/Cli/Commands/WorkspaceCommand.php'; use DataMachineCode\Abilities\WorkspaceAbilities; @@ -118,9 +119,12 @@ function workspace_show_cli_assert( bool $condition, string $message ): void { 'status' => 'warning', 'creation_allowed' => true, 'force_override_required' => false, + 'emergency_triggered' => false, 'trigger_reasons' => array( 'worktree_count_warning_threshold' ), 'advisory' => 'Capacity advisory [workspace_capacity@abc]: admission allowed.', 'summary' => 'full capacity summary', + 'filesystem_free_bytes' => 182 * 1073741824, + 'warn_free_bytes' => 20 * 1073741824, ); $command->show( array( 'example' ), array() ); workspace_show_cli_assert('Dirty: no' === WP_CLI::$logs[5] && 'Capacity advisory [workspace_capacity@abc]: admission allowed.' === WP_CLI::$logs[6] && 7 === count(WP_CLI::$logs), 'Default workspace show must lead with repository state and emit exactly one compact advisory.'); @@ -144,7 +148,17 @@ function workspace_show_cli_assert( bool $condition, string $message ): void { WP_CLI::$logs = array(); $command->show( array( 'example' ), array( 'format' => 'json' ) ); $payload = json_decode( WP_CLI::$lines[0] ?? '', true ); - workspace_show_cli_assert( WorkspaceAbilities::$result === $payload, 'Workspace show JSON did not retain the lossless ability result.' ); + workspace_show_cli_assert( + 'refused' === ($payload['workspace_capacity']['status'] ?? null) + && false === ($payload['workspace_capacity']['creation_allowed'] ?? true) + && ! isset($payload['workspace_capacity']['summary']), + 'Workspace show JSON did not compact routine capacity evidence.' + ); + + WP_CLI::$lines = array(); + $command->show( array( 'example' ), array( 'format' => 'json', 'full' => true ) ); + $full_payload = json_decode( WP_CLI::$lines[0] ?? '', true ); + workspace_show_cli_assert( WorkspaceAbilities::$result === $full_payload, 'Workspace show --full JSON did not retain the lossless ability result.' ); WP_CLI::$lines = array(); WorkspaceAbilities::$result = new WP_Error( 'workspace_not_found', 'Repository "missing" not found.', array( 'name' => 'missing' ) ); diff --git a/tests/worktree-disk-budget.php b/tests/worktree-disk-budget.php index 1656bb4d..60b50124 100644 --- a/tests/worktree-disk-budget.php +++ b/tests/worktree-disk-budget.php @@ -36,6 +36,7 @@ function assert_true( bool $condition, string $message ): void { ); assert_true('refused' === $budget['status'], 'low free space should refuse worktree creation'); + assert_true(true === $budget['emergency_triggered'], 'byte refusal must remain an emergency threshold'); assert_true('independent_filesystem_bytes_and_inodes' === $budget['safety_basis'], 'response should identify independent byte and inode safeguards'); assert_true(str_contains($budget['warnings'][0] ?? '', 'Projected free filesystem space'), 'threshold messaging should identify projected filesystem free space'); assert_true(98 * $gib === $budget['filesystem_used_bytes'], 'filesystem used bytes should be explicit'); @@ -82,6 +83,7 @@ function assert_true( bool $condition, string $message ): void { ); assert_true('ok' === $healthy['status'], 'healthy free space should pass the worktree disk budget gate'); + assert_true(false === $healthy['emergency_triggered'], 'healthy free space must not report an emergency'); assert_true(array() === $healthy['warnings'], 'healthy free space should not emit disk budget warnings'); assert_true(array_key_exists('workspace_size_bytes', $healthy) && null === $healthy['workspace_size_bytes'], 'legacy workspace size field should remain present when diagnostics are unavailable'); @@ -173,6 +175,7 @@ function assert_true( bool $condition, string $message ): void { $inode_thresholds ); assert_true('warning' === $worktree_count_warning['status'], 'worktree count above its threshold should warn with healthy byte and inode capacity'); + assert_true(false === $worktree_count_warning['emergency_triggered'], 'count-only advisory pressure must not trigger emergency cleanup'); assert_true(array( 'worktree_count_warning_threshold' ) === $worktree_count_warning['trigger_reasons'], 'worktree count warning should retain its stable reason code'); assert_true(true === $worktree_count_warning['creation_allowed'], 'count-only pressure must allow creation'); assert_true(false === $worktree_count_warning['force_override_required'], 'count-only pressure must not require force'); @@ -303,6 +306,7 @@ function assert_true( bool $condition, string $message ): void { assert_true(false === $combined_pressure['creation_allowed'], 'combined blocking pressure must deny creation'); assert_true(true === $combined_pressure['force_override_required'], 'combined byte or inode pressure must require force'); assert_true(array( 'blocking', 'advisory', 'blocking' ) === array_column($combined_pressure['typed_trigger_reasons'], 'severity'), 'combined pressure must preserve each trigger severity in stable reason order'); + assert_true(true === $combined_pressure['emergency_triggered'], 'blocking pressure must trigger emergency cleanup'); $combined_summary = WorktreeDiskBudget::format_summary($combined_pressure); assert_true(str_contains($combined_summary, 'Admission: blocked; force override required=yes; advisory triggers=worktree_count_warning_threshold; blocking triggers=projected_free_bytes_absolute_refusal_floor,projected_free_inodes_absolute_refusal_floor.'), 'human summary must distinguish advisory count pressure from byte and inode refusal'); $legacy_summary = WorktreeDiskBudget::format_summary(array( 'status' => 'warning', 'trigger_reasons' => array( 'worktree_count_warning_threshold' ) ));