mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
## Why Buzz server components need one typed feature-flag contract without forcing the open-source build to depend on LaunchDarkly. A staging or production relay must not be able to choose the wrong provider through runtime configuration. ## What - Adds typed boolean and signed `i64` flags with static, environment, and compile-gated LaunchDarkly evaluators. - Defines an executable relay artifact contract that compiles exactly one provider. - Rejects zero or multiple relay provider features at compile time. - Runs the LaunchDarkly feature tests and the provider compile matrix in the unit CI lane and fallback test script. ## How `buzz-feature-flags` stays provider-neutral. The future relay composition root will expose one build path per artifact feature: `static-feature-flags`, `environment-feature-flags`, or `launchdarkly-feature-flags`. Runtime configuration carries only settings for the selected provider. It never carries a provider name, fallback, or precedence rule. The crate still has no relay consumer. A locked compile fixture proves each singleton builds and every zero or multi-provider combination fails until the relay adopts this contract. This removes the proposed runtime `ProviderMode` and leaves one provider, one construction path, and one evaluator per artifact. ## Risk This PR still has no relay or app consumer, so it changes no user-visible behavior. Risk is limited to the new feature-flag crate, its compile contract, and its unit-test lane. ## Testing An external Rust consumer at `30cf968bfe85e82980fda7e63f5bebe649ca939f` exercised six environment-evaluator scenarios and 59 assertions. The compile-time correction adds no live consumer to exercise manually. ## Bigger picture When `buzz-relay` adopts this crate, its build manifests must map each distribution to one provider feature. That consumer PR must verify live Relay Proxy reachability, secret ingestion, lifecycle ownership, and user-visible behavior. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
use std::sync::Arc;
|
|
|
|
use buzz_core::{CommunityId, PublicKey};
|
|
use buzz_feature_flags::{
|
|
BooleanFlag, EnvironmentDiagnostic, EnvironmentEvaluator, EvaluationContext, FlagEvaluator,
|
|
IntegerFlag,
|
|
};
|
|
|
|
const ENV_NAMESPACE: &str = "BUZZ_FEATURE_FLAG_";
|
|
|
|
fn community(id: &str) -> CommunityId {
|
|
CommunityId::from_uuid(id.parse().expect("valid community UUID"))
|
|
}
|
|
|
|
fn actor(hex: &str) -> PublicKey {
|
|
PublicKey::from_hex(hex).expect("valid pubkey")
|
|
}
|
|
|
|
#[test]
|
|
fn parses_booleans_case_insensitive_with_trim() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([
|
|
(
|
|
format!("{ENV_NAMESPACE}RELAY_BOOL_TRUE"),
|
|
" TrUe \t".to_owned(),
|
|
),
|
|
(
|
|
format!("{ENV_NAMESPACE}RELAY_BOOL_FALSE"),
|
|
"\n false ".to_owned(),
|
|
),
|
|
]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.bool.true", false), &context));
|
|
assert!(!evaluator.evaluate_bool(BooleanFlag::new("relay.bool.false", true), &context));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_signed_negative_integers_with_trim() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([(
|
|
format!("{ENV_NAMESPACE}DB_EVENTS_QUERY_VERSION"),
|
|
" -17 ".to_owned(),
|
|
)]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert_eq!(
|
|
evaluator.evaluate_int(IntegerFlag::new("db.events.query-version", 1), &context),
|
|
-17
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_flag_values_fall_back_to_declared_defaults() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([] as [(String, String); 0]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.bool.missing", true), &context));
|
|
assert_eq!(
|
|
evaluator.evaluate_int(IntegerFlag::new("relay.int.missing", -9), &context),
|
|
-9
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_and_empty_values_fall_back_to_declared_defaults() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([
|
|
(
|
|
format!("{ENV_NAMESPACE}RELAY_BOOL_INVALID"),
|
|
"maybe".to_owned(),
|
|
),
|
|
(format!("{ENV_NAMESPACE}RELAY_BOOL_EMPTY"), " ".to_owned()),
|
|
(
|
|
format!("{ENV_NAMESPACE}RELAY_INT_INVALID"),
|
|
"12.5".to_owned(),
|
|
),
|
|
]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(!evaluator.evaluate_bool(BooleanFlag::new("relay.bool.invalid", false), &context));
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.bool.empty", true), &context));
|
|
assert_eq!(
|
|
evaluator.evaluate_int(IntegerFlag::new("relay.int.invalid", 88), &context),
|
|
88
|
|
);
|
|
assert_eq!(
|
|
evaluator.evaluate_int(IntegerFlag::new("!!!", -4), &context),
|
|
-4,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn evaluation_context_is_ignored_for_process_wide_values() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([(
|
|
format!("{ENV_NAMESPACE}RELAY_PROCESS_WIDE"),
|
|
"true".to_owned(),
|
|
)]);
|
|
let community_a = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa");
|
|
let community_b = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb");
|
|
let actor_a = actor("c4f0623bdc8c4f7ecab9f7457f501f3e8f4efcf8f8f6ef6f4d76f42f5bb6f2cb");
|
|
let actor_b = actor("f1e2d3c4b5a697887766554433221100ffeeddccbbaa99887766554433221100");
|
|
|
|
assert!(evaluator.evaluate_bool(
|
|
BooleanFlag::new("relay.process-wide", false),
|
|
&EvaluationContext::for_actor(community_a, actor_a),
|
|
));
|
|
assert!(evaluator.evaluate_bool(
|
|
BooleanFlag::new("relay.process-wide", false),
|
|
&EvaluationContext::for_actor(community_b, actor_b),
|
|
));
|
|
assert!(evaluator.evaluate_bool(
|
|
BooleanFlag::new("relay.process-wide", false),
|
|
&EvaluationContext::for_community(community_a),
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn normalization_replaces_non_alnum_runs_and_trims_separators() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([(
|
|
format!("{ENV_NAMESPACE}RELAY_FOO_BAR_BAZ_9"),
|
|
"true".to_owned(),
|
|
)]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(evaluator.evaluate_bool(
|
|
BooleanFlag::new("___relay..foo---bar***baz??9___", false),
|
|
&context,
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn colliding_flag_keys_share_normalized_environment_name() {
|
|
let evaluator =
|
|
EnvironmentEvaluator::from_pairs([(format!("{ENV_NAMESPACE}A_B"), "true".to_owned())]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("a-b", false), &context));
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("a_b", false), &context));
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_is_immutable_after_construction() {
|
|
let mut source = vec![(format!("{ENV_NAMESPACE}SNAPSHOT_BOOL"), "true".to_owned())];
|
|
|
|
let evaluator = EnvironmentEvaluator::from_pairs(
|
|
source
|
|
.iter()
|
|
.map(|(key, value)| (key.as_str(), value.as_str())),
|
|
);
|
|
|
|
source[0].1 = "false".to_owned();
|
|
source.push((format!("{ENV_NAMESPACE}SNAPSHOT_INT"), "99".to_owned()));
|
|
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("snapshot.bool", false), &context));
|
|
assert_eq!(
|
|
evaluator.evaluate_int(IntegerFlag::new("snapshot.int", -1), &context),
|
|
-1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn debug_does_not_expose_environment_contents() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([(
|
|
format!("{ENV_NAMESPACE}SECRET_FLAG"),
|
|
"super-secret-value".to_owned(),
|
|
)]);
|
|
|
|
let debug = format!("{evaluator:?}");
|
|
assert!(!debug.contains("super-secret-value"));
|
|
assert!(!debug.contains("SECRET_FLAG"));
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_present_values_default_and_diagnose_once_per_key_and_type() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([
|
|
(
|
|
format!("{ENV_NAMESPACE}RELAY_INVALID_BOOL"),
|
|
"raw-secret-bool".to_owned(),
|
|
),
|
|
(
|
|
format!("{ENV_NAMESPACE}RELAY_INVALID_INT"),
|
|
"raw-secret-int".to_owned(),
|
|
),
|
|
("DATABASE_URL".to_owned(), "unrelated-secret".to_owned()),
|
|
]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
let bool_flag = BooleanFlag::new("relay.invalid-bool", true);
|
|
let int_flag = IntegerFlag::new("relay.invalid-int", -12);
|
|
|
|
assert!(evaluator.evaluate_bool(bool_flag, &context));
|
|
assert!(evaluator.evaluate_bool(bool_flag, &context));
|
|
assert_eq!(evaluator.evaluate_int(int_flag, &context), -12);
|
|
assert_eq!(evaluator.evaluate_int(int_flag, &context), -12);
|
|
|
|
let diagnostics = evaluator.take_diagnostics();
|
|
assert_eq!(
|
|
diagnostics,
|
|
vec![
|
|
EnvironmentDiagnostic::InvalidBooleanValue {
|
|
flag_key: "relay.invalid-bool",
|
|
},
|
|
EnvironmentDiagnostic::InvalidIntegerValue {
|
|
flag_key: "relay.invalid-int",
|
|
},
|
|
]
|
|
);
|
|
|
|
let rendered = format!("{diagnostics:?}");
|
|
assert!(!rendered.contains("raw-secret-bool"));
|
|
assert!(!rendered.contains("raw-secret-int"));
|
|
assert!(!rendered.contains("unrelated-secret"));
|
|
assert!(!rendered.contains("DATABASE_URL"));
|
|
|
|
assert!(evaluator.evaluate_bool(bool_flag, &context));
|
|
assert_eq!(evaluator.evaluate_int(int_flag, &context), -12);
|
|
assert!(evaluator.take_diagnostics().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn trait_object_evaluation_reports_through_retained_concrete_owner() {
|
|
let owner = Arc::new(EnvironmentEvaluator::from_pairs([(
|
|
format!("{ENV_NAMESPACE}RELAY_INVALID"),
|
|
"not-a-boolean".to_owned(),
|
|
)]));
|
|
let evaluator: Arc<dyn FlagEvaluator> = owner.clone();
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.invalid", true), &context));
|
|
assert_eq!(
|
|
owner.take_diagnostics(),
|
|
vec![EnvironmentDiagnostic::InvalidBooleanValue {
|
|
flag_key: "relay.invalid",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn same_flag_key_reports_distinct_boolean_and_integer_diagnostics() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([(
|
|
format!("{ENV_NAMESPACE}RELAY_SHARED_KEY"),
|
|
"invalid-for-both-types".to_owned(),
|
|
)]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(!evaluator.evaluate_bool(BooleanFlag::new("relay.shared-key", false), &context));
|
|
assert_eq!(
|
|
evaluator.evaluate_int(IntegerFlag::new("relay.shared-key", -4), &context),
|
|
-4
|
|
);
|
|
assert_eq!(
|
|
evaluator.take_diagnostics(),
|
|
vec![
|
|
EnvironmentDiagnostic::InvalidBooleanValue {
|
|
flag_key: "relay.shared-key",
|
|
},
|
|
EnvironmentDiagnostic::InvalidIntegerValue {
|
|
flag_key: "relay.shared-key",
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cloned_evaluators_share_dedup_before_and_after_drain() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([(
|
|
format!("{ENV_NAMESPACE}RELAY_CLONED"),
|
|
"not-a-boolean".to_owned(),
|
|
)]);
|
|
let clone = evaluator.clone();
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
let flag = BooleanFlag::new("relay.cloned", false);
|
|
|
|
assert!(!evaluator.evaluate_bool(flag, &context));
|
|
assert!(!clone.evaluate_bool(flag, &context));
|
|
assert_eq!(
|
|
clone.take_diagnostics(),
|
|
vec![EnvironmentDiagnostic::InvalidBooleanValue {
|
|
flag_key: "relay.cloned",
|
|
}]
|
|
);
|
|
assert!(evaluator.take_diagnostics().is_empty());
|
|
|
|
assert!(!evaluator.evaluate_bool(flag, &context));
|
|
assert!(!clone.evaluate_bool(flag, &context));
|
|
assert!(evaluator.take_diagnostics().is_empty());
|
|
assert!(clone.take_diagnostics().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn missing_values_default_without_diagnostics() {
|
|
let evaluator = EnvironmentEvaluator::from_pairs([] as [(String, String); 0]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
|
|
assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.missing-bool", true), &context));
|
|
assert_eq!(
|
|
evaluator.evaluate_int(IntegerFlag::new("relay.missing-int", -4), &context),
|
|
-4
|
|
);
|
|
assert!(evaluator.take_diagnostics().is_empty());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn namespaced_non_unicode_value_defaults_and_diagnoses_once() {
|
|
use std::{ffi::OsString, os::unix::ffi::OsStringExt};
|
|
|
|
let evaluator = EnvironmentEvaluator::from_pairs([(
|
|
OsString::from(format!("{ENV_NAMESPACE}RELAY_NON_UNICODE")),
|
|
OsString::from_vec(vec![0x80]),
|
|
)]);
|
|
let context =
|
|
EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
|
|
let flag = BooleanFlag::new("relay.non-unicode", false);
|
|
|
|
assert!(!evaluator.evaluate_bool(flag, &context));
|
|
assert!(!evaluator.evaluate_bool(flag, &context));
|
|
assert_eq!(
|
|
evaluator.take_diagnostics(),
|
|
vec![EnvironmentDiagnostic::InvalidBooleanValue {
|
|
flag_key: "relay.non-unicode",
|
|
}]
|
|
);
|
|
assert!(!evaluator.evaluate_bool(flag, &context));
|
|
assert!(evaluator.take_diagnostics().is_empty());
|
|
}
|