Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions notes/flaky-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ and fall through to Level 2 (GitHub label search).
| Test node ID | Issue | Last blocked |
|---|---|---|
| `test/bt/test_vultrabot.py::MyTestCase::test_main` | — | 2026-05-05 |
| `test/ci/invariants/test_fv_invariants.py::test_invariant_5_expected_event_types_present[validate_report]` | #2274 | 2026-08-13 |
| `test/ci/invariants/test_fv_invariants.py::test_invariant_5_expected_event_types_present[engage_case]` | #2274 | 2026-08-13 |

> Note: the two `test_integration_script_scenarios` entries were **hard-broken
> on `main`, not flaky** — they failed deterministically. #2114 added a test that
Expand Down
9 changes: 5 additions & 4 deletions notes/structured-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,11 @@ from the #1988 implementation:
`TransitionRMtoValid`, …), reading the before-state from the latest
`ParticipantStatus` and falling back to `RM.START`.
`CreateParticipantStatusNode` is the second path — `leave.py`,
`sync/nodes/effects.py`, and `add_participant_status_trigger_tree.py` set
`rm_state=` on it directly without going through the helper — so it logs the
RM line itself. A new RM-writing node MUST route through one of these two, or
its transition will be missing from the INFO narrative.
`sync/nodes/close_case_effect.py` (`ApplyCloseCaseFromLedgerNode`), and
`add_participant_status_trigger_tree.py` set `rm_state=` on it directly
without going through the helper — so it logs the RM line itself. A new
RM-writing node MUST route through one of these two, or its transition will
be missing from the INFO narrative.
- CS: `CreateParticipantStatusNode` is the shared writer for both VFD and PXA
snapshots. `TransitionCStoFixReady` / `TransitionCStoFixDeployed` delegate to
it and log only at DEBUG — they know the target state but not the origin.
Expand Down
40 changes: 40 additions & 0 deletions plan/history/2608/learning/CONCERN-2269.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
source: CONCERN-2269
timestamp: '2026-08-13T01:00:42.254022+00:00'
title: append.py + effects.py decomposition eliminates BTND-07-004 churn
type: learning
---

Both `status/nodes/append.py` (499 lines) and `sync/nodes/effects.py` (495 lines)
were at the BTND-07-004 ceiling, causing any unrelated edit in those areas to
trigger a mandatory decomposition as a side-effect.

**Resolution**: Decomposed both modules by semantic concern in a single PR with
no backward-compatibility shims. All importers updated in-place.

## append.py → append/ subpackage

- `append/conditions.py` — 4 guard/idempotency nodes + `_has_status_in_participant` helper
- `append/actions.py` — 3 DataLayer-mutating action nodes
- `append/__init__.py` — re-exports all 7 public names (import paths unchanged)

## effects.py → per-class files + _helpers.py

- `_helpers.py` — `_extract_id_from_field` + `_LedgerEffectNode` base class
(DRY: all 4 effect nodes shared identical `setup()` + `_require_log_entry` pattern)
- `participant_status_effect.py`, `note_effect.py`, `invite_accept_effect.py`,
`close_case_effect.py` — one file per class

## Test mirroring

- `test_append.py` split into `append/conftest.py` + `test_conditions.py` + `test_actions.py`
- Added `TestCheckParticipantRMNotClosedNode` (was missing from original)
- `test_effects.py` split into 4 per-class test files
- Added tests for `ApplyNoteFromLedgerNode`, `ApplyInviteAcceptFromLedgerNode`,
`ApplyCloseCaseFromLedgerNode` (all three were previously untested)

**PR**: <https://github.com/CERTCC/Vultron/pull/2282>

**5 other near-limit modules** noted as future work: `replay.py` (498),
`suggest_actor/emit.py` (498), `deploy_fix.py` (497),
`embargo/nodes/lifecycle.py` (494), `conditions.py` (488).
Empty file.
88 changes: 88 additions & 0 deletions test/core/behaviors/status/nodes/append/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env python

# Copyright (c) 2026 Carnegie Mellon University and Contributors.
# - see Contributors.md for a full list of Contributors
# - see ContributionInstructions.md for information on how you can Contribute to this project
# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is
# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed
# with this Software or contact permission@sei.cmu.edu for full terms.
# Created, in part, with funding and support from the United States Government
# (see Acknowledgments file). This program may include and/or can make use of
# certain third party source code, object code, documentation and other files
# ("Third Party Software"). See LICENSE.md for more details.
# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the
# U.S. Patent and Trademark Office by Carnegie Mellon University

"""Shared fixtures for append subpackage tests."""

import pytest
import py_trees

from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer
from vultron.core.behaviors.bridge import BTBridge
from vultron.enums.roles import CVDRole
from vultron.wire.as2.vocab.objects.case_participant import as_CaseParticipant
from vultron.wire.as2.vocab.objects.case_status import as_ParticipantStatus
from vultron.wire.as2.vocab.objects.vulnerability_case import (
as_VulnerabilityCase,
)

ACTOR_ID = "https://example.org/actors/vendor"
CASE_MANAGER_ID = "https://example.org/actors/case-actor"
CASE_ID = "https://example.org/cases/case-01"
PARTICIPANT_ID = "https://example.org/cases/case-01/participants/vendor"
CM_PARTICIPANT_ID = "https://example.org/cases/case-01/participants/case-actor"
STATUS_ID = "https://example.org/cases/case-01/statuses/s1"


@pytest.fixture(autouse=True)
def clear_blackboard():
py_trees.blackboard.Blackboard.storage.clear()


@pytest.fixture
def dl():
return SqliteDataLayer("sqlite:///:memory:")


@pytest.fixture
def bridge(dl):
return BTBridge(datalayer=dl)


@pytest.fixture
def participant():
return as_CaseParticipant(
id_=PARTICIPANT_ID,
context=CASE_ID,
attributed_to=ACTOR_ID,
case_roles=[CVDRole.CASE_OWNER],
)


@pytest.fixture
def status_obj():
return as_ParticipantStatus(id_=STATUS_ID, context=CASE_ID)


@pytest.fixture
def populated_dl(dl, participant, status_obj):
case_manager_participant = as_CaseParticipant(
id_=CM_PARTICIPANT_ID,
context=CASE_ID,
attributed_to=CASE_MANAGER_ID,
case_roles=[CVDRole.CASE_MANAGER],
)
case = as_VulnerabilityCase(id_=CASE_ID, name="Test Case")
case.add_participant(participant)
case.add_participant(case_manager_participant)
dl.create(case)
dl.create(participant)
dl.create(case_manager_participant)
dl.create(status_obj)
return dl


@pytest.fixture
def populated_bridge(populated_dl):
return BTBridge(datalayer=populated_dl)
121 changes: 121 additions & 0 deletions test/core/behaviors/status/nodes/append/test_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python

# Copyright (c) 2026 Carnegie Mellon University and Contributors.
# - see Contributors.md for a full list of Contributors
# - see ContributionInstructions.md for information on how you can Contribute to this project
# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is
# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed
# with this Software or contact permission@sei.cmu.edu for full terms.
# Created, in part, with funding and support from the United States Government
# (see Acknowledgments file). This program may include and/or can make use of
# certain third party source code, object code, documentation and other files
# ("Third Party Software"). See LICENSE.md for more details.
# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the
# U.S. Patent and Trademark Office by Carnegie Mellon University

"""Tests for append/actions.py: load, resolve, and append action nodes."""

import py_trees
from py_trees.common import Status

from vultron.core.behaviors.status.nodes.append import (
AppendStatusAndSaveParticipantNode,
LoadParticipantNode,
ResolveAndPersistStatusObjectNode,
)

from .conftest import ACTOR_ID, PARTICIPANT_ID, STATUS_ID

# ---------------------------------------------------------------------------
# LoadParticipantNode
# ---------------------------------------------------------------------------


class TestLoadParticipantNode:
def test_loads_participant_to_blackboard(self, populated_bridge):
node = LoadParticipantNode(participant_id=PARTICIPANT_ID)
result = populated_bridge.execute_with_setup(
tree=node, actor_id=ACTOR_ID
)
assert result.status == Status.SUCCESS

def test_missing_participant_fails(self, bridge):
node = LoadParticipantNode(
participant_id="https://example.org/cases/missing/p"
)
result = bridge.execute_with_setup(tree=node, actor_id=ACTOR_ID)
assert result.status == Status.FAILURE


# ---------------------------------------------------------------------------
# ResolveAndPersistStatusObjectNode
# ---------------------------------------------------------------------------


class TestResolveAndPersistStatusObjectNode:
def test_resolves_from_dl(self, populated_bridge):
node = ResolveAndPersistStatusObjectNode(
status_id=STATUS_ID, status_obj_fallback=None
)
result = populated_bridge.execute_with_setup(
tree=node, actor_id=ACTOR_ID
)
assert result.status == Status.SUCCESS

def test_missing_without_fallback_fails(self, bridge):
node = ResolveAndPersistStatusObjectNode(
status_id="https://example.org/missing", status_obj_fallback=None
)
result = bridge.execute_with_setup(tree=node, actor_id=ACTOR_ID)
assert result.status == Status.FAILURE

def test_uses_fallback_when_missing_from_dl(self, bridge):
"""Fallback object is persisted and resolved when ID absent from DL."""
from vultron.wire.as2.vocab.objects.case_status import (
as_ParticipantStatus,
)
from .conftest import CASE_ID

fallback = as_ParticipantStatus(id_=STATUS_ID, context=CASE_ID)
node = ResolveAndPersistStatusObjectNode(
status_id=STATUS_ID, status_obj_fallback=fallback
)
result = bridge.execute_with_setup(tree=node, actor_id=ACTOR_ID)
assert result.status == Status.SUCCESS


# ---------------------------------------------------------------------------
# AppendStatusAndSaveParticipantNode
# ---------------------------------------------------------------------------


class TestAppendStatusAndSaveParticipantNode:
def test_appends_status(self, populated_bridge, populated_dl):
p_before = populated_dl.read(PARTICIPANT_ID)
initial_count = len(p_before.participant_statuses)

load = LoadParticipantNode(participant_id=PARTICIPANT_ID)
resolve = ResolveAndPersistStatusObjectNode(
status_id=STATUS_ID, status_obj_fallback=None
)
append = AppendStatusAndSaveParticipantNode(
status_id=STATUS_ID, participant_id=PARTICIPANT_ID
)
seq = py_trees.composites.Sequence(
name="TestSeq", memory=False, children=[load, resolve, append]
)
result = populated_bridge.execute_with_setup(
tree=seq, actor_id=ACTOR_ID
)
assert result.status == Status.SUCCESS

p = populated_dl.read(PARTICIPANT_ID)
assert len(p.participant_statuses) == initial_count + 1

def test_missing_blackboard_data_fails(self, bridge):
"""No prior load/resolve on blackboard → FAILURE."""
append = AppendStatusAndSaveParticipantNode(
status_id=STATUS_ID, participant_id=PARTICIPANT_ID
)
result = bridge.execute_with_setup(tree=append, actor_id=ACTOR_ID)
assert result.status == Status.FAILURE
Loading
Loading