mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(detection-core): replay fact-derived relations
This commit is contained in:
@@ -7,7 +7,7 @@ from dataclasses import dataclass, replace
|
||||
import pytest
|
||||
from detection_core_fixtures import InMemoryContentResolver, InMemoryPopulationCustody, custody_contract, stored_artifact
|
||||
|
||||
from openultrasast.core import canonical, provenance
|
||||
from openultrasast.core import canonical, evidence, provenance
|
||||
from openultrasast.core.evidence import NeutralOutcome
|
||||
from openultrasast.core.factapply import FactApplicationError, apply_fact
|
||||
from openultrasast.core.factmatch import (
|
||||
@@ -34,7 +34,9 @@ from openultrasast.core.factpack import (
|
||||
derive_review_evidence_digest,
|
||||
derive_review_independence_policy_digest,
|
||||
)
|
||||
from openultrasast.core.factvalidate import FactRelationValidationError, validate_fact_relations
|
||||
from openultrasast.core.family import (
|
||||
MISSING_REVIEWED_DEPENDENCY,
|
||||
DecisionRule,
|
||||
FamilyContract,
|
||||
FieldSpec,
|
||||
@@ -42,15 +44,20 @@ from openultrasast.core.family import (
|
||||
RelationOriginPolicy,
|
||||
ResidualQuestion,
|
||||
RoleSpec,
|
||||
derive_security_object_id,
|
||||
family_contract_digest,
|
||||
)
|
||||
from openultrasast.core.semantics import (
|
||||
EvidenceProvenance,
|
||||
FieldKind,
|
||||
FieldValue,
|
||||
ProvenanceKind,
|
||||
RelationKind,
|
||||
RelationStatus,
|
||||
SemanticNode,
|
||||
SemanticRelation,
|
||||
SourceLocation,
|
||||
make_relation,
|
||||
)
|
||||
|
||||
|
||||
@@ -68,21 +75,47 @@ class ApplyCase:
|
||||
population_digest: str
|
||||
|
||||
|
||||
def _contract() -> FamilyContract:
|
||||
class MultiPopulationCustody(InMemoryPopulationCustody):
|
||||
def __init__(
|
||||
self,
|
||||
logs: tuple[provenance.PopulationCustodyLog, ...],
|
||||
policy: provenance.PopulationCustodyPolicy,
|
||||
) -> None:
|
||||
self._logs = {log.population_digest: log for log in logs}
|
||||
self._multi_policy = policy
|
||||
|
||||
def current_log(self, population_digest: str) -> provenance.PopulationCustodyLog:
|
||||
try:
|
||||
return self._logs[population_digest]
|
||||
except KeyError as exc:
|
||||
raise provenance.PopulationCustodyError("population custody log not found") from exc
|
||||
|
||||
def append(self, request: provenance.PopulationUseRequest, custodian: str) -> provenance.PopulationUseReceipt:
|
||||
prior = self.current_log(request.population_digest)
|
||||
receipt = provenance.record_population_use(request, prior, custodian, self._multi_policy)
|
||||
self._logs[request.population_digest] = provenance.append_population_use(prior, receipt, self._multi_policy)
|
||||
return receipt
|
||||
|
||||
|
||||
def _contract(*, fact_only: bool = False) -> FamilyContract:
|
||||
origin = RelationOriginPolicy.EXCLUDED_DEPENDENCY_FACT_ONLY if fact_only else RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT
|
||||
return FamilyContract(
|
||||
"reference-family",
|
||||
"1",
|
||||
"family.v2",
|
||||
evidence.EVIDENCE_PACKET_SCHEMA,
|
||||
("python",),
|
||||
ObjectSpec("resource", (FieldSpec("name", FieldKind.TEXT, True),), ("name",)),
|
||||
(
|
||||
RoleSpec("flow", (), (RelationKind.CALL,), RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT),
|
||||
RoleSpec("flow", (), (RelationKind.CALL,), origin),
|
||||
RoleSpec("source", (FieldSpec("name", FieldKind.TEXT, False),), (), RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT),
|
||||
RoleSpec("target", (FieldSpec("name", FieldKind.TEXT, False),), (), RelationOriginPolicy.PRODUCT_OR_REVIEWED_FACT),
|
||||
),
|
||||
(DecisionRule("unknown", NeutralOutcome.INDETERMINATE, (), (RelationStatus.UNRESOLVED,)),),
|
||||
(ResidualQuestion("classify", ("flow",), ("unknown",)),),
|
||||
("missing",),
|
||||
(
|
||||
DecisionRule("safe", NeutralOutcome.SATISFIED, ("flow",), (RelationStatus.COMPLETE,)),
|
||||
DecisionRule("unknown", NeutralOutcome.INDETERMINATE, (), (RelationStatus.INCOMPLETE, RelationStatus.UNRESOLVED)),
|
||||
),
|
||||
(ResidualQuestion("classify", ("flow",), ("safe", "unknown")),),
|
||||
("missing", MISSING_REVIEWED_DEPENDENCY),
|
||||
)
|
||||
|
||||
|
||||
@@ -94,8 +127,9 @@ def _append_use(
|
||||
*,
|
||||
labels_accessed: bool,
|
||||
arm_id: str,
|
||||
population_digest: str | None = None,
|
||||
) -> provenance.LineageUse:
|
||||
current = custody.current_log(canonical.content_digest("population"))
|
||||
current = custody.current_log(population_digest or canonical.content_digest("population"))
|
||||
request = provenance.PopulationUseRequest(
|
||||
current.population_digest,
|
||||
arm_id,
|
||||
@@ -152,8 +186,9 @@ def _case(
|
||||
matcher_labels: bool = False,
|
||||
promotion_labels: bool = False,
|
||||
omitted_promotion_labels: bool = False,
|
||||
fact_only: bool = False,
|
||||
) -> ApplyCase:
|
||||
contract = _contract()
|
||||
contract = _contract(fact_only=fact_only)
|
||||
evidence, review_resolver, reviewer = _review_evidence()
|
||||
review_policy = _review_policy(reviewer)
|
||||
effects = effects or (RelationEffect(RelationKind.CALL, "flow", "source", "target", ("matched",)),)
|
||||
@@ -283,6 +318,128 @@ def _apply(case: ApplyCase) -> tuple[SemanticRelation, ...]:
|
||||
)
|
||||
|
||||
|
||||
def _case_with_distinct_evaluation_population() -> ApplyCase:
|
||||
case = _case(fact_only=True)
|
||||
source_log = case.custody.current_log(case.population_digest)
|
||||
evaluation_digest = canonical.content_digest("distinct-evaluation-population")
|
||||
evaluation_log = provenance.empty_population_custody_log(
|
||||
evaluation_digest,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
)
|
||||
custody = MultiPopulationCustody((source_log, evaluation_log), case.custody_policy)
|
||||
return replace(case, custody=custody, population_digest=evaluation_digest)
|
||||
|
||||
|
||||
def _case_with_three_populations() -> tuple[ApplyCase, str]:
|
||||
case = _case(fact_only=True)
|
||||
custodian = case.custody_policy.reviewed_custodian_digests[0]
|
||||
source_log = case.custody.current_log(case.population_digest)
|
||||
fact_empty = provenance.empty_population_custody_log(case.population_digest, custodian)
|
||||
fact_log = provenance.append_population_use(fact_empty, source_log.receipts[0], case.custody_policy)
|
||||
matcher_population = canonical.content_digest("distinct-matcher-population")
|
||||
matcher_empty = provenance.empty_population_custody_log(matcher_population, custodian)
|
||||
matcher_request = provenance.PopulationUseRequest(
|
||||
matcher_population,
|
||||
"matcher-arm",
|
||||
provenance.LineagePurpose.MATCHING_BEHAVIOR,
|
||||
False,
|
||||
case.match.matcher_behavior_digest,
|
||||
matcher_empty.log_digest,
|
||||
)
|
||||
matcher_receipt = provenance.record_population_use(matcher_request, matcher_empty, custodian, case.custody_policy)
|
||||
matcher_log = provenance.append_population_use(matcher_empty, matcher_receipt, case.custody_policy)
|
||||
matcher_use = provenance.LineageUse(
|
||||
provenance.LineagePurpose.MATCHING_BEHAVIOR,
|
||||
matcher_population,
|
||||
False,
|
||||
case.match.matcher_behavior_digest,
|
||||
matcher_receipt.receipt_digest,
|
||||
)
|
||||
matcher_lineage = _manifest("matcher-arm", case.match.matcher_behavior_digest, (matcher_use,))
|
||||
provisional_match = replace(
|
||||
case.match,
|
||||
matcher_lineage_manifest_digest=matcher_lineage.manifest_digest,
|
||||
match_digest="0" * 64,
|
||||
)
|
||||
match = replace(provisional_match, match_digest=derive_fact_match_digest(provisional_match))
|
||||
match_reference, match_resolver = stored_artifact(fact_match_bytes(match), artifact_id="fact-match")
|
||||
evaluation_digest = canonical.content_digest("third-evaluation-population")
|
||||
evaluation_log = provenance.empty_population_custody_log(evaluation_digest, custodian)
|
||||
custody = MultiPopulationCustody((fact_log, matcher_log, evaluation_log), case.custody_policy)
|
||||
artifacts = {**case.resolver.artifacts, **match_resolver.artifacts}
|
||||
changed = replace(
|
||||
case,
|
||||
match=match,
|
||||
match_reference=match_reference,
|
||||
lineages=(case.lineages[0], matcher_lineage),
|
||||
custody=custody,
|
||||
resolver=InMemoryContentResolver(artifacts),
|
||||
population_digest=evaluation_digest,
|
||||
)
|
||||
return changed, matcher_population
|
||||
|
||||
|
||||
def _stamp(case: ApplyCase) -> provenance.ProvenanceStamp:
|
||||
return provenance.ProvenanceStamp(
|
||||
canonical.content_digest("analyzer"),
|
||||
family_contract_digest(case.contract),
|
||||
canonical.content_digest("fact-set"),
|
||||
canonical.content_digest("acquisition"),
|
||||
canonical.content_digest("configuration"),
|
||||
case.population_digest,
|
||||
canonical.content_digest("change-input"),
|
||||
)
|
||||
|
||||
|
||||
def _record(case: ApplyCase, relation: SemanticRelation) -> evidence.EvidenceRecord:
|
||||
fields = (FieldValue("name", FieldKind.TEXT, "resource"),)
|
||||
scope = evidence.AnalyzedScope(
|
||||
"base",
|
||||
"head",
|
||||
canonical.content_digest("change"),
|
||||
(evidence.SourceRegion(case.match.product_location.path, 1, 30),),
|
||||
)
|
||||
reasons = relation.reasons if relation.status is not RelationStatus.COMPLETE else ()
|
||||
packet = evidence.EvidencePacket(
|
||||
evidence.EVIDENCE_PACKET_SCHEMA,
|
||||
case.contract.family_id,
|
||||
family_contract_digest(case.contract),
|
||||
derive_security_object_id(case.contract.security_object, fields),
|
||||
case.pack.ecosystem,
|
||||
scope,
|
||||
(relation,),
|
||||
(),
|
||||
fields,
|
||||
reasons,
|
||||
_stamp(case),
|
||||
)
|
||||
outcome = NeutralOutcome.SATISFIED if relation.status is RelationStatus.COMPLETE else NeutralOutcome.INDETERMINATE
|
||||
decision = evidence.Decision(
|
||||
"safe" if relation.status is RelationStatus.COMPLETE else "unknown",
|
||||
outcome,
|
||||
evidence.EvidenceBasis.DETERMINISTIC,
|
||||
scope,
|
||||
reasons,
|
||||
evidence.derive_packet_digest(packet),
|
||||
provenance.DecisionProvenance(None, None, None, None, None, None),
|
||||
)
|
||||
return evidence.EvidenceRecord(packet, decision, "")
|
||||
|
||||
|
||||
def _validate(case: ApplyCase, record: evidence.EvidenceRecord, *, packs: tuple[FactPack, ...] | None = None) -> None:
|
||||
validate_fact_relations(
|
||||
record,
|
||||
case.contract,
|
||||
(case.pack,) if packs is None else packs,
|
||||
case.population_digest,
|
||||
case.lineages,
|
||||
case.custody,
|
||||
case.custody_policy,
|
||||
case.review_policy,
|
||||
case.resolver,
|
||||
)
|
||||
|
||||
|
||||
def test_complete_match_materializes_only_declared_effect_with_exact_provenance() -> None:
|
||||
case = _case()
|
||||
|
||||
@@ -366,3 +523,267 @@ def test_omitted_current_promotion_spend_cannot_hide_behind_clean_pack_lineage()
|
||||
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.SPENT
|
||||
with pytest.raises(FactApplicationError, match="promotion.*protected_population_labels"):
|
||||
_apply(case)
|
||||
|
||||
|
||||
def test_reviewed_dependency_relation_replays_from_every_named_input() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
def test_product_only_dependency_behavior_is_refused() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(reviewed,) = _apply(case)
|
||||
product = replace(
|
||||
reviewed,
|
||||
provenance=EvidenceProvenance(ProvenanceKind.PRODUCT_CODE, case.match.match_digest, None, None, None),
|
||||
relation_id="",
|
||||
)
|
||||
|
||||
with pytest.raises(FactRelationValidationError, match="reviewed semantic fact"):
|
||||
_validate(case, _record(case, product))
|
||||
|
||||
|
||||
def test_missing_dependency_fact_remains_named_and_indeterminate() -> None:
|
||||
case = _case(fact_only=True)
|
||||
location = case.match.product_location
|
||||
missing = make_relation(
|
||||
kind=RelationKind.CALL,
|
||||
role="flow",
|
||||
source=SemanticNode("source", "source", location),
|
||||
target=SemanticNode("target", "target", location),
|
||||
location=location,
|
||||
status=RelationStatus.INCOMPLETE,
|
||||
conditions=("matched",),
|
||||
reasons=(MISSING_REVIEWED_DEPENDENCY,),
|
||||
provenance=EvidenceProvenance(ProvenanceKind.PRODUCT_CODE, case.match.match_digest, None, None, None),
|
||||
)
|
||||
record = _record(case, missing)
|
||||
|
||||
_validate(case, record)
|
||||
|
||||
assert record.decision.outcome is NeutralOutcome.INDETERMINATE
|
||||
assert record.decision.reasons == (MISSING_REVIEWED_DEPENDENCY,)
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_undeclared_role_and_invalid_scope() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
undeclared = replace(relation, role="undeclared", relation_id="")
|
||||
with pytest.raises(FactRelationValidationError, match="undeclared semantic role"):
|
||||
_validate(case, _record(case, undeclared))
|
||||
|
||||
record = _record(case, relation)
|
||||
packet = replace(record.packet, ecosystem="javascript")
|
||||
decision = replace(record.decision, packet_digest=evidence.derive_packet_digest(packet))
|
||||
with pytest.raises(FactRelationValidationError, match="unsupported ecosystem scope"):
|
||||
_validate(case, evidence.EvidenceRecord(packet, decision, ""))
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_inconsistent_relation_and_fact_identity() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
inconsistent = replace(relation, conditions=("substituted",), relation_id="")
|
||||
with pytest.raises(FactRelationValidationError, match="does not equal replayed"):
|
||||
_validate(case, _record(case, inconsistent))
|
||||
|
||||
wrong_origin = replace(relation.provenance, semantic_fact_id="other.fact")
|
||||
wrong_fact = replace(relation, provenance=wrong_origin, relation_id="")
|
||||
with pytest.raises(FactRelationValidationError, match="fact identity"):
|
||||
_validate(case, _record(case, wrong_fact))
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_substituted_resolved_match_bytes() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
wrong_ref, wrong_resolver = stored_artifact(b'{"plausible":"but-wrong"}', artifact_id="wrong-replay-match")
|
||||
wrong_origin = replace(
|
||||
relation.provenance,
|
||||
source_content_digest=wrong_ref.content_digest,
|
||||
fact_match=wrong_ref,
|
||||
)
|
||||
wrong_relation = replace(relation, provenance=wrong_origin, relation_id="")
|
||||
changed = replace(
|
||||
case,
|
||||
resolver=InMemoryContentResolver({**case.resolver.artifacts, **wrong_resolver.artifacts}),
|
||||
)
|
||||
|
||||
with pytest.raises(FactRelationValidationError, match="match artifact"):
|
||||
_validate(changed, _record(changed, wrong_relation))
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_ambiguous_pack_and_changed_current_custody() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
with pytest.raises(FactRelationValidationError, match="pack identity is missing or ambiguous"):
|
||||
_validate(case, record, packs=(case.pack, case.pack))
|
||||
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
provenance.LineagePurpose.PROMOTION,
|
||||
case.pack.normalized_content_digest,
|
||||
labels_accessed=True,
|
||||
arm_id="other-arm",
|
||||
)
|
||||
with pytest.raises(FactRelationValidationError, match="promotion lineage is contaminated"):
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_omitted_current_semantic_fact_label_use() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
provenance.LineagePurpose.SEMANTIC_FACT,
|
||||
case.pack.normalized_content_digest,
|
||||
labels_accessed=True,
|
||||
arm_id="fact-arm",
|
||||
)
|
||||
|
||||
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.SPENT
|
||||
with pytest.raises(FactRelationValidationError, match="semantic fact.*protected_population_labels"):
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_cross_arm_protected_semantic_fact_label_use() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
provenance.LineagePurpose.SEMANTIC_FACT,
|
||||
case.pack.normalized_content_digest,
|
||||
labels_accessed=True,
|
||||
arm_id="other-arm",
|
||||
)
|
||||
|
||||
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.SPENT
|
||||
with pytest.raises(FactRelationValidationError, match="semantic fact.*protected_population_labels"):
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_cross_arm_label_use_from_lineage_source_population() -> None:
|
||||
case = _case_with_distinct_evaluation_population()
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
source_population = canonical.content_digest("population")
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
provenance.LineagePurpose.SEMANTIC_FACT,
|
||||
case.pack.normalized_content_digest,
|
||||
labels_accessed=True,
|
||||
arm_id="other-arm",
|
||||
)
|
||||
|
||||
assert source_population != case.population_digest
|
||||
assert case.custody.current_log(source_population).status is provenance.PopulationStatus.SPENT
|
||||
with pytest.raises(FactRelationValidationError, match="semantic fact.*label_access_forbidden"):
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_label_use_from_other_supplied_lineage_population() -> None:
|
||||
case, matcher_population = _case_with_three_populations()
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
provenance.LineagePurpose.SEMANTIC_FACT,
|
||||
case.pack.normalized_content_digest,
|
||||
labels_accessed=True,
|
||||
arm_id="other-arm",
|
||||
population_digest=matcher_population,
|
||||
)
|
||||
|
||||
assert matcher_population not in {
|
||||
case.population_digest,
|
||||
case.lineages[0].uses[0].source_population_digest,
|
||||
}
|
||||
assert case.custody.current_log(matcher_population).status is provenance.PopulationStatus.SPENT
|
||||
with pytest.raises(FactRelationValidationError, match="semantic fact.*label_access_forbidden"):
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("purpose", "subject", "labels_accessed"),
|
||||
(
|
||||
(provenance.LineagePurpose.SEMANTIC_FACT, "exact-pack", False),
|
||||
(provenance.LineagePurpose.MATCHING_BEHAVIOR, "exact-pack", True),
|
||||
(provenance.LineagePurpose.SEMANTIC_FACT, "wrong-digest", True),
|
||||
),
|
||||
)
|
||||
def test_fact_relation_replay_ignores_unrelated_cross_arm_source_receipts(
|
||||
purpose: provenance.LineagePurpose,
|
||||
subject: str,
|
||||
labels_accessed: bool,
|
||||
) -> None:
|
||||
case = _case_with_distinct_evaluation_population()
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
influenced = case.pack.normalized_content_digest if subject == "exact-pack" else canonical.content_digest(subject)
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
purpose,
|
||||
influenced,
|
||||
labels_accessed=labels_accessed,
|
||||
arm_id="other-arm",
|
||||
)
|
||||
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("purpose", "subject", "labels_accessed"),
|
||||
(
|
||||
(provenance.LineagePurpose.SEMANTIC_FACT, "exact-pack", False),
|
||||
(provenance.LineagePurpose.MATCHING_BEHAVIOR, "exact-pack", True),
|
||||
(provenance.LineagePurpose.SEMANTIC_FACT, "wrong-digest", True),
|
||||
),
|
||||
)
|
||||
def test_fact_relation_replay_ignores_unrelated_receipts_in_other_lineage_population(
|
||||
purpose: provenance.LineagePurpose,
|
||||
subject: str,
|
||||
labels_accessed: bool,
|
||||
) -> None:
|
||||
case, matcher_population = _case_with_three_populations()
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
influenced = case.pack.normalized_content_digest if subject == "exact-pack" else canonical.content_digest(subject)
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
purpose,
|
||||
influenced,
|
||||
labels_accessed=labels_accessed,
|
||||
arm_id="other-arm",
|
||||
population_digest=matcher_population,
|
||||
)
|
||||
|
||||
_validate(case, record)
|
||||
|
||||
|
||||
def test_fact_relation_replay_refuses_omitted_current_semantic_fact_use_without_labels() -> None:
|
||||
case = _case(fact_only=True)
|
||||
(relation,) = _apply(case)
|
||||
record = _record(case, relation)
|
||||
_append_use(
|
||||
case.custody,
|
||||
case.custody_policy.reviewed_custodian_digests[0],
|
||||
provenance.LineagePurpose.SEMANTIC_FACT,
|
||||
case.pack.normalized_content_digest,
|
||||
labels_accessed=False,
|
||||
arm_id="fact-arm",
|
||||
)
|
||||
|
||||
assert case.custody.current_log(case.population_digest).status is provenance.PopulationStatus.AVAILABLE
|
||||
with pytest.raises(FactRelationValidationError, match="semantic fact lineage omitted current use"):
|
||||
_validate(case, record)
|
||||
|
||||
Reference in New Issue
Block a user