Files
DECNET/tests/db/test_attribution_state.py
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

226 lines
7.4 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""AttributionStateRow + identity-stub repo tests — Phase 1 substrate.
Mirrors ``tests/db/test_observations.py``: SQLite ``tmp_path`` factory,
``@pytest.mark.anyio`` markers, an ``Attacker`` seeded so the stub-
materialisation path has a valid FK.
"""
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
import pytest
from decnet.web.db.factory import get_repository
@pytest.fixture
async def repo(tmp_path: Path):
r = get_repository(db_path=str(tmp_path / "attribution.db"))
await r.initialize()
return r
@pytest.fixture
async def attacker_uuid(repo) -> str:
now = datetime.now(timezone.utc)
return await repo.upsert_attacker({
"ip": "10.0.0.7",
"first_seen": now,
"last_seen": now,
})
@pytest.mark.anyio
async def test_ensure_stub_creates_identity_for_new_attacker(
repo, attacker_uuid: str,
) -> None:
"""First call: Attacker has no identity_id → stub created and
stamped onto the Attacker row."""
identity_uuid = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
assert identity_uuid is not None
assert isinstance(identity_uuid, str)
attacker = await repo.get_attacker_by_uuid(attacker_uuid)
assert attacker is not None
assert attacker["identity_id"] == identity_uuid
identity = await repo.get_identity_by_uuid(identity_uuid)
assert identity is not None
assert identity["uuid"] == identity_uuid
assert identity["merged_into_uuid"] is None
assert identity["schema_version"] == 1
@pytest.mark.anyio
async def test_ensure_stub_idempotent(repo, attacker_uuid: str) -> None:
"""Second call returns the same identity_uuid; no second insert."""
first = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
second = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
third = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
assert first == second == third
@pytest.mark.anyio
async def test_ensure_stub_returns_none_for_missing_attacker(repo) -> None:
"""Worker treats missing-Attacker as 'defer' — repo returns None
without raising or inserting an orphan identity."""
out = await repo.ensure_stub_identity_for_attacker(
"00000000000000000000000000000000",
)
assert out is None
@pytest.mark.anyio
async def test_upsert_and_read_back_state(repo, attacker_uuid: str) -> None:
"""Round-trip: every column on the state row survives one
insert + read."""
identity_uuid = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
assert identity_uuid is not None
await repo.upsert_attribution_state({
"identity_uuid": identity_uuid,
"primitive": "motor.input_modality",
"current_value": "pasted",
"state": "stable",
"confidence": 0.91,
"observation_count": 5,
"last_change_ts": 1714521660.456,
"last_observation_ts": 1714521660.456,
})
out = await repo.get_attribution_state(
identity_uuid, "motor.input_modality",
)
assert out is not None
assert out["state"] == "stable"
assert out["confidence"] == 0.91
assert out["current_value"] == "pasted"
assert out["observation_count"] == 5
assert out["last_change_ts"] == 1714521660.456
@pytest.mark.anyio
async def test_upsert_idempotent_on_natural_key(
repo, attacker_uuid: str,
) -> None:
"""Same (identity_uuid, primitive) twice → one row, second wins
on mutable fields."""
identity_uuid = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
assert identity_uuid is not None
base = {
"identity_uuid": identity_uuid,
"primitive": "motor.input_modality",
"current_value": "typed",
"state": "stable",
"confidence": 0.7,
"observation_count": 3,
"last_change_ts": 1714000000.0,
"last_observation_ts": 1714000000.0,
}
await repo.upsert_attribution_state(base)
await repo.upsert_attribution_state({
**base,
"current_value": "pasted",
"state": "drifting",
"confidence": 0.85,
"observation_count": 8,
"last_change_ts": 1714000300.0,
"last_observation_ts": 1714000400.0,
})
rows = await repo.get_attribution_state_for_identity(identity_uuid)
assert len(rows) == 1
assert rows[0]["state"] == "drifting"
assert rows[0]["confidence"] == 0.85
assert rows[0]["current_value"] == "pasted"
@pytest.mark.anyio
async def test_get_state_for_identity_orders_by_primitive(
repo, attacker_uuid: str,
) -> None:
"""Multiple primitives → one row each, primitive-ordered for
deterministic API output."""
identity_uuid = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
assert identity_uuid is not None
primitives = [
"motor.input_modality",
"cognitive.feedback_loop_engagement",
"temporal.weekend_cadence",
]
for i, p in enumerate(primitives):
await repo.upsert_attribution_state({
"identity_uuid": identity_uuid,
"primitive": p,
"current_value": "x",
"state": "stable",
"confidence": 0.8,
"observation_count": 5,
"last_change_ts": 1714000000.0 + i,
"last_observation_ts": 1714000000.0 + i,
})
rows = await repo.get_attribution_state_for_identity(identity_uuid)
assert [r["primitive"] for r in rows] == sorted(primitives)
@pytest.mark.anyio
async def test_list_multi_actor_requires_two_primitives(
repo, attacker_uuid: str,
) -> None:
"""Single-primitive multi_actor flag is too noisy. Correlator
only fires on ≥ 2 primitives independently flagging the same
identity."""
identity_uuid = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
assert identity_uuid is not None
# One multi_actor row → no co-flag yet.
await repo.upsert_attribution_state({
"identity_uuid": identity_uuid,
"primitive": "motor.input_modality",
"current_value": "conflicted",
"state": "multi_actor",
"confidence": 0.55,
"observation_count": 10,
"last_change_ts": 1714000000.0,
"last_observation_ts": 1714000000.0,
})
assert await repo.list_multi_actor_identities() == []
# Add a second multi_actor row → identity surfaces with both
# primitives.
await repo.upsert_attribution_state({
"identity_uuid": identity_uuid,
"primitive": "cognitive.feedback_loop_engagement",
"current_value": "conflicted",
"state": "multi_actor",
"confidence": 0.6,
"observation_count": 8,
"last_change_ts": 1714000100.0,
"last_observation_ts": 1714000100.0,
})
out = await repo.list_multi_actor_identities()
assert len(out) == 1
assert out[0]["identity_uuid"] == identity_uuid
assert sorted(out[0]["primitives"]) == [
"cognitive.feedback_loop_engagement",
"motor.input_modality",
]
@pytest.mark.anyio
async def test_get_state_returns_none_for_unknown_pair(
repo, attacker_uuid: str,
) -> None:
"""Worker uses None as 'no prior state, initialise from this
observation' — surface the contract directly."""
identity_uuid = await repo.ensure_stub_identity_for_attacker(attacker_uuid)
assert identity_uuid is not None
out = await repo.get_attribution_state(
identity_uuid, "motor.input_modality",
)
assert out is None