@@ -345,6 +345,51 @@ func TestFlowCompileProjectsHyphenatedEntryIdentity(t *testing.T) {
345345 }
346346}
347347
348+ func TestFlowCompileRejectsDependencyLockProjectionOverlap (t * testing.T ) {
349+ // control-law: compile-inputs-cannot-be-replaced-or-retired-by-their-own-projection
350+ if runtime .GOOS == "windows" {
351+ t .Skip ("shell fixture is Unix-only" )
352+ }
353+ repository , err := filepath .EvalSymlinks (t .TempDir ())
354+ if err != nil {
355+ t .Fatal (err )
356+ }
357+ documentRaw , err := json .Marshal (productDeliveryDocument ("product-delivery" ))
358+ if err != nil {
359+ t .Fatal (err )
360+ }
361+ sourcePath := ".boatstack/flows/product-delivery.flow.ts"
362+ artifactPath := ".boatstack/flows/product-delivery.flow.ir.json"
363+ writeFixture (t , repository , ".git/keep" , nil )
364+ writeFixture (t , repository , sourcePath , []byte ("declarative source" ))
365+ writeFixture (t , repository , "package-lock.json" , []byte ("lock" ))
366+ writeFixture (t , repository , "raw-ir.json" , documentRaw )
367+ frontend := filepath .Join (repository , "frontend.sh" )
368+ if err := os .WriteFile (frontend , []byte ("#!/bin/sh\n cat >/dev/null\n cat '" + filepath .Join (repository , "raw-ir.json" )+ "'\n " ), 0o700 ); err != nil {
369+ t .Fatal (err )
370+ }
371+ options := flowCommandOptions {repository : repository , source : sourcePath , lock : "package-lock.json" , frontend : frontend }
372+ if err := compileFlow (context .Background (), options ); err != nil {
373+ t .Fatal (err )
374+ }
375+ before , err := os .ReadFile (filepath .Join (repository , filepath .FromSlash (artifactPath )))
376+ if err != nil {
377+ t .Fatal (err )
378+ }
379+ options .lock = artifactPath
380+ err = compileFlow (context .Background (), options )
381+ if err == nil || ! strings .Contains (err .Error (), "FLOW_COMPILE_INPUT_OVERLAP" ) {
382+ t .Fatalf ("overlapping lock result = %v" , err )
383+ }
384+ after , err := os .ReadFile (filepath .Join (repository , filepath .FromSlash (artifactPath )))
385+ if err != nil || ! bytes .Equal (after , before ) {
386+ t .Fatalf ("overlapping compile changed artifact: %v" , err )
387+ }
388+ if err := checkFlow (context .Background (), flowCommandOptions {repository : repository }); err != nil {
389+ t .Fatalf ("preserved artifact no longer checks: %v" , err )
390+ }
391+ }
392+
348393func TestFlowCompileRefusesUnmanagedGeneratedSkill (t * testing.T ) {
349394 // control-law: first-compile-cannot-adopt-or-overwrite-unmanaged-skill-bytes
350395 if runtime .GOOS == "windows" {
@@ -693,7 +738,6 @@ func TestFlowEntryBindsStableRunAndResumesManagedPlan(t *testing.T) {
693738 }
694739 writeFixture (t , repository , ".boatstack/plans/delivery-one.source" , []byte ("exact plan" ))
695740 writeFixture (t , repository , ".boatstack/plans/inbox/unrelated.md" , []byte ("other plan" ))
696- writeFixture (t , repository , ".boatstack/plans/delivery-one.source" , []byte ("approved amendment" ))
697741 resumed , err := bindFlowEntry (context .Background (), commandOptions {
698742 repository : repository , programID : "product-delivery" , entryID : "run" , runID : initial .runID , host : "codex" ,
699743 deliveryID : initial .deliveryID , objectiveKind : initial .objectiveKind , objectiveID : initial .objectiveID , transitionID : "plan.create" ,
@@ -713,6 +757,28 @@ func TestFlowEntryBindsStableRunAndResumesManagedPlan(t *testing.T) {
713757 }
714758}
715759
760+ func TestFlowEntryRejectsSelectedPlanContentSubstitution (t * testing.T ) {
761+ // control-law: one-flow-run-binds-the-exact-selected-plan-bytes
762+ repository := flowRepository (t )
763+ planPath := ".boatstack/plans/inbox/delivery-one.md"
764+ writeFixture (t , repository , planPath , []byte ("plan A" ))
765+ initial , err := bindFlowEntry (context .Background (), commandOptions {repository : repository , programID : "product-delivery" , entryID : "run" , host : "codex" })
766+ if err != nil {
767+ t .Fatal (err )
768+ }
769+ writeFixture (t , repository , planPath , []byte ("plan B" ))
770+ _ , err = bindFlowEntry (context .Background (), commandOptions {
771+ repository : repository , programID : "product-delivery" , entryID : "run" , runID : initial .runID , host : "codex" ,
772+ deliveryID : initial .deliveryID , objectiveKind : initial .objectiveKind , objectiveID : initial .objectiveID , transitionID : "plan.create" ,
773+ })
774+ if err == nil || ! strings .Contains (err .Error (), "FLOW_RUN_MISMATCH" ) {
775+ t .Fatalf ("plan substitution result = %v" , err )
776+ }
777+ if _ , statErr := os .Stat (filepath .Join (repository , ".boatstack" , "plans" , "delivery-one.source" )); ! os .IsNotExist (statErr ) {
778+ t .Fatalf ("plan substitution produced a managed source: %v" , statErr )
779+ }
780+ }
781+
716782func TestFlowEntryPreservesSelectedPlanFilenameBeforeMaterialization (t * testing.T ) {
717783 // control-law: an-admitted-plan-filename-remains-resolvable-for-the-same-run
718784 repository := flowRepository (t )
0 commit comments