@@ -421,3 +421,75 @@ async fn live_policy_update_from_empty_network_policies() {
421421
422422 guard. cleanup ( ) . await ;
423423}
424+
425+ /// Regression for #2159: a sparse initial policy that the supervisor enriches
426+ /// with baseline filesystem paths during startup must have its resulting
427+ /// revision acknowledged as loaded, not left `Pending`.
428+ ///
429+ /// Reproduction (matches the maintainer's triage): create a sandbox with the
430+ /// network-only `examples/policy-advisor/sandbox-policy.yaml`. The supervisor
431+ /// adds baseline filesystem paths, syncs the enriched policy back to the
432+ /// gateway (creating revision 2, superseding revision 1), builds the OPA engine
433+ /// and then acknowledges revision 2 as loaded. Before the fix, revision 2
434+ /// stayed `Pending` even though the sandbox was `Ready` and the policy was
435+ /// effective.
436+ ///
437+ /// NOTE: This exercises the Docker-backed supervisor built from this branch.
438+ /// The exact `policy list` status wording ("Loaded"/"Superseded") may differ by
439+ /// CLI version; the assertions below key on the effective version reaching 2 and
440+ /// no revision remaining `Pending` once the acknowledgement lands.
441+ #[ tokio:: test]
442+ async fn initial_sparse_policy_is_acknowledged_as_loaded ( ) {
443+ // Repo-relative path to the sparse network-only policy fixture.
444+ let sparse_policy = concat ! (
445+ env!( "CARGO_MANIFEST_DIR" ) ,
446+ "/../../examples/policy-advisor/sandbox-policy.yaml"
447+ ) ;
448+
449+ let mut guard = SandboxGuard :: create_keep_with_args (
450+ & [ "--name" , "e2e-2159-sparse-enrich" , "--policy" , sparse_policy, "--no-tty" ] ,
451+ & [ "sh" , "-c" , "echo Ready && sleep infinity" ] ,
452+ "Ready" ,
453+ )
454+ . await
455+ . expect ( "create keep sandbox with sparse policy" ) ;
456+
457+ // The enriched revision (2) is synced during startup; the acknowledgement
458+ // (LOADED) is delivered by the supervisor's poll loop shortly after Ready.
459+ // Poll until the effective policy is version 2 and no revision is Pending.
460+ let mut acknowledged = false ;
461+ let mut last_list = String :: new ( ) ;
462+ for _ in 0 ..30 {
463+ let get = run_cli ( & [ "policy" , "get" , & guard. name ] ) . await ;
464+ let version = get. success . then ( || extract_version ( & get. output ) ) . flatten ( ) ;
465+
466+ let list = run_cli ( & [ "policy" , "list" , & guard. name ] ) . await ;
467+ last_list = list. output . clone ( ) ;
468+ let pending = list. output . to_lowercase ( ) . contains ( "pending" ) ;
469+
470+ if version == Some ( 2 ) && list. success && !pending {
471+ acknowledged = true ;
472+ break ;
473+ }
474+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 2 ) ) . await ;
475+ }
476+
477+ assert ! (
478+ acknowledged,
479+ "enriched initial policy should reach revision 2 with no Pending revision.\n \
480+ last `policy list` output:\n {last_list}"
481+ ) ;
482+
483+ // Both the superseded original (1) and the loaded enriched revision (2)
484+ // must appear in the revision history.
485+ assert ! (
486+ list_output_contains_version( & last_list, 2 ) ,
487+ "policy list should contain revision 2:\n {last_list}"
488+ ) ;
489+ assert ! (
490+ list_output_contains_version( & last_list, 1 ) ,
491+ "policy list should contain revision 1:\n {last_list}"
492+ ) ;
493+
494+ guard. cleanup ( ) . await ;
495+ }
0 commit comments