Skip to content

Commit 0f6c8ba

Browse files
Testclaude
andcommitted
test(flows): cover multi-flow entry-attrs propagation end-to-end
The flows-entry-attrs fix (commit 17baad6) shipped suite + release guards for the single-flow degenerate case, but the multi-flow modes (ad-hoc / meta / mixed) were only validated by a manual QA round — the legacy multi-flow tests used attribute-free fixtures and could not observe needs/admission loss. Add system tests (ad-hoc cross-union `needs`, meta-flow expansion, mixed topological order) on attribute-carrying fixtures, plus a @group release test for a multi-flow run against the .phar. Verified the guards bite: dropping the reconstructed dependency graph turns all three red. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6a02848 commit 0f6c8ba

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

tests/System/Commands/FlowsCommandTest.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,103 @@ public function single_flow_exclude_files_skips_job_like_flow_command()
518518
);
519519
}
520520

521+
// ─── Multi-flow modes must propagate entry-attrs end-to-end ──────
522+
// The bug spanned every mode, but the legacy multi-flow tests use
523+
// attribute-free fixtures (job_a/b/c → script true), so they could not
524+
// observe the loss. These exercise needs across the merged union (ad-hoc),
525+
// through meta-flow expansion, and the topological ordering in mixed runs.
526+
527+
/**
528+
* Fixture with several normal flows whose entries carry `needs`, plus a
529+
* meta-flow `ci`. `compile` fails so `pkg` (in a *different* flow than the
530+
* ones it merges with) must propagate the skip across the union.
531+
*/
532+
private function buildMultiFlowAttrsFixture(): void
533+
{
534+
$this->configurationFileBuilder
535+
->enableV3Mode()
536+
->setV3Hooks(['pre-commit' => ['qa']])
537+
->setV3Flows([
538+
'options' => ['processes' => 1, 'fail-fast' => false],
539+
'build' => ['jobs' => [
540+
'compile',
541+
['job' => 'pkg', 'needs' => ['compile']],
542+
]
543+
],
544+
'qa' => ['jobs' => [
545+
'prep',
546+
['job' => 'tests', 'needs' => ['prep']],
547+
]
548+
],
549+
'deploy' => ['jobs' => [
550+
['job' => 'ship', 'needs' => ['verify']],
551+
'verify',
552+
]
553+
],
554+
'ci' => ['flows' => ['build', 'qa']],
555+
])
556+
->setV3Jobs([
557+
'compile' => ['type' => 'custom', 'script' => 'exit 1'],
558+
'pkg' => ['type' => 'custom', 'script' => 'echo pkg'],
559+
'prep' => ['type' => 'custom', 'script' => 'echo prep'],
560+
'tests' => ['type' => 'custom', 'script' => 'echo tests'],
561+
'ship' => ['type' => 'custom', 'script' => 'echo ship'],
562+
'verify' => ['type' => 'custom', 'script' => 'echo verify'],
563+
])
564+
->buildInFileSystem();
565+
}
566+
567+
/** @test ad-hoc: `needs` propagates across the merged union of two flows */
568+
public function ad_hoc_multiflow_propagates_cross_flow_needs()
569+
{
570+
$this->buildMultiFlowAttrsFixture();
571+
572+
$output = $this->runJson("flows build qa --format=json --config=$this->configPath");
573+
574+
$this->assertSame(['build', 'qa'], $output['flows']);
575+
$jobs = $this->indexJobs($output['jobs']);
576+
$this->assertFalse($jobs['compile']['success'], 'compile must fail');
577+
$this->assertTrue($jobs['pkg']['skipped'], 'pkg (flow build) must propagate its failed `needs`');
578+
$this->assertSame('needs compile failed', $jobs['pkg']['skipReason']);
579+
$this->assertSame(['compile'], $jobs['pkg']['needs']);
580+
$this->assertFalse($jobs['tests']['skipped'], 'tests (flow qa) runs — its dependency prep passed');
581+
$this->assertSame(['prep'], $jobs['tests']['needs']);
582+
}
583+
584+
/** @test declarative: a meta-flow propagates `needs` through expansion */
585+
public function declarative_multiflow_propagates_needs_through_expansion()
586+
{
587+
$this->buildMultiFlowAttrsFixture();
588+
589+
$output = $this->runJson("flows ci --format=json --config=$this->configPath");
590+
591+
$this->assertSame(['build', 'qa'], $output['flows']);
592+
$jobs = $this->indexJobs($output['jobs']);
593+
$this->assertTrue($jobs['pkg']['skipped'], 'pkg must propagate via needs after meta-flow expansion');
594+
$this->assertSame('needs compile failed', $jobs['pkg']['skipReason']);
595+
}
596+
597+
/** @test mixed: the merged union is ordered topologically across flows */
598+
public function mixed_multiflow_orders_merged_jobs_topologically()
599+
{
600+
$this->buildMultiFlowAttrsFixture();
601+
602+
$output = $this->runJson("flows ci deploy --format=json --config=$this->configPath");
603+
604+
$this->assertSame(['build', 'qa', 'deploy'], $output['flows']);
605+
$names = array_column($output['jobs'], 'name');
606+
// `ship` declares `needs: [verify]` *before* verify in the flow; the
607+
// reconstructed graph must still emit verify first.
608+
$this->assertLessThan(
609+
array_search('ship', $names, true),
610+
array_search('verify', $names, true),
611+
'verify must precede ship (topological order across the aggregate)'
612+
);
613+
$jobs = $this->indexJobs($output['jobs']);
614+
$this->assertSame(['verify'], $jobs['ship']['needs']);
615+
$this->assertFalse($jobs['ship']['skipped'], 'ship runs — verify passed');
616+
}
617+
521618
/**
522619
* Index a JSON v2 `jobs` array by job name.
523620
*

tests/System/Release/FlowEntryAttrsReleaseTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,63 @@ public function phar_flows_command_skips_job_by_exclude_files(): void
453453
);
454454
}
455455

456+
/**
457+
* Multi-flow run: `needs` must propagate across the merged union of two
458+
* flows. `compile` (flow build) fails ⇒ `pkg` (also flow build, but merged
459+
* alongside flow qa) is skipped. Guards the multi-flow path end-to-end in
460+
* the `.phar`, which the single-flow release tests above do not exercise.
461+
*
462+
* @test
463+
*/
464+
public function phar_flows_command_propagates_needs_in_multi_flow_run(): void
465+
{
466+
$this->configurationFileBuilder
467+
->setV3Flows([
468+
'options' => ['processes' => 1, 'fail-fast' => false],
469+
'build' => ['jobs' => [
470+
'compile',
471+
['job' => 'pkg', 'needs' => ['compile']],
472+
]
473+
],
474+
'qa' => ['jobs' => [
475+
'prep',
476+
['job' => 'tests', 'needs' => ['prep']],
477+
]
478+
],
479+
])
480+
->setV3Jobs([
481+
'compile' => ['type' => 'custom', 'script' => 'exit 1'],
482+
'pkg' => ['type' => 'custom', 'script' => 'echo pkg'],
483+
'prep' => ['type' => 'custom', 'script' => 'echo prep'],
484+
'tests' => ['type' => 'custom', 'script' => 'echo tests'],
485+
]);
486+
487+
file_put_contents($this->configPath, $this->configurationFileBuilder->buildV3Php());
488+
489+
$cmd = sprintf(
490+
'%s flows build qa --format=json --config=%s 2>/dev/null',
491+
$this->githooks,
492+
$this->configPath
493+
);
494+
495+
passthru($cmd, $exitCode);
496+
497+
$this->assertSame(1, $exitCode);
498+
$decoded = json_decode($this->getActualOutput(), true);
499+
$this->assertIsArray($decoded);
500+
$this->assertSame(['build', 'qa'], $decoded['flows']);
501+
502+
$byName = $this->indexJobs($decoded['jobs']);
503+
$this->assertFalse($byName['compile']['success']);
504+
$this->assertTrue(
505+
$byName['pkg']['skipped'],
506+
'`flows build qa` must propagate the cross-union needs skip'
507+
);
508+
$this->assertSame('needs compile failed', $byName['pkg']['skipReason']);
509+
$this->assertSame(['compile'], $byName['pkg']['needs']);
510+
$this->assertFalse($byName['tests']['skipped'], 'tests runs — prep passed');
511+
}
512+
456513
/**
457514
* @param array<int, array<string, mixed>> $jobs
458515
* @return array<string, array<string, mixed>>

0 commit comments

Comments
 (0)