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
9 changes: 9 additions & 0 deletions .agentmesh/policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"public_private": {
"content_scan_exempt_globs": [
"tests/fixtures/secrets/**",
"tests/test_secret_detection.py",
"tests/test_public_private.py"
Comment on lines +4 to +6
]
}
}
44 changes: 44 additions & 0 deletions .github/workflows/nightly-agentmesh-simulations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Nightly QA Simulations

on:
schedule:
# 03:15 UTC daily, offset from other nightly workflows
- cron: "15 3 * * *"
workflow_dispatch:

jobs:
full-suite:
name: Full test suite
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run full test suite
run: pytest tests/ -v

# Contention tests with extended timeout for heavier concurrency pressure
contention-heavy:
name: Contention tests (extended)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run contention tests with extended timeout
run: pytest tests/test_contention.py -v
38 changes: 38 additions & 0 deletions .github/workflows/pr-agentmesh-qa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: PR QA – Contention & Secret Detection

on:
pull_request:
branches: [main]

jobs:
contention:
name: Contention tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run contention tests
run: pytest tests/test_contention.py -v

secret-detection:
name: Secret detection tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run secret detection tests
run: pytest tests/test_secret_detection.py -v
57 changes: 57 additions & 0 deletions .github/workflows/release-agentmesh-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Release Check

on:
release:
types: [created]
workflow_dispatch:

jobs:
build-and-verify:
name: Build, install, and verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Build wheel
run: |
pip install build
python -m build

- name: Install wheel in clean venv
run: |
python -m venv .release-venv
. .release-venv/bin/activate
pip install dist/*.whl

- name: Verify CLI entry points
run: |
. .release-venv/bin/activate
agentmesh --version
agentmesh --help
agentmesh doctor --help

- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: release-wheel
path: dist/

test-suite:
name: Full test suite against source
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run full test suite
run: pytest tests/ -v
3 changes: 2 additions & 1 deletion src/agentmesh/public_private.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def classify_path(
private_globs = _policy_list(cfg.get("private_path_globs")) or _DEFAULT_PRIVATE_GLOBS
review_globs = _policy_list(cfg.get("review_path_globs")) or _DEFAULT_REVIEW_GLOBS
private_patterns = _policy_list(cfg.get("private_content_patterns")) or _DEFAULT_PRIVATE_PATTERNS
content_scan_exempt = _policy_list(cfg.get("content_scan_exempt_globs"))

rel = _rel_path(path, repo_root)
reasons: list[str] = []
Expand All @@ -125,7 +126,7 @@ def classify_path(
reasons.append("path matches private pattern")

content_marker = None
if path.exists() and path.is_file():
if path.exists() and path.is_file() and not _has_match(rel, content_scan_exempt):
try:
text = path.read_text(errors="ignore")
except OSError:
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/secrets/aws_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This file contains a leaked AWS access key for testing secret detection.
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
5 changes: 5 additions & 0 deletions tests/fixtures/secrets/clean_public.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""A module with no secrets or sensitive content."""


def greet(name: str) -> str:
return f"Hello, {name}!"
6 changes: 6 additions & 0 deletions tests/fixtures/secrets/edge_ghp_in_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Utility module with an accidentally pasted token in a comment."""


def do_work() -> None:
# TODO: remove this token ghp_A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6
pass
2 changes: 2 additions & 0 deletions tests/fixtures/secrets/ghp_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This file contains a leaked GitHub PAT for testing secret detection.
API_TOKEN = "ghp_R8x2mN4vL6pQ9wK1jT3yF5bA7cE0hU2sG4nM"
3 changes: 3 additions & 0 deletions tests/fixtures/secrets/pricing_doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Internal Strategy

Our pricing model targets enterprise customers at $25K per seat.
4 changes: 4 additions & 0 deletions tests/fixtures/secrets/private_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBogIBAAJBALRiMLAHudeSA/x3hB2f+2NRkJLA/FAKEFAKEFAKEFAKEFAKE
FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE1234
-----END RSA PRIVATE KEY-----
Comment on lines +1 to +4
162 changes: 162 additions & 0 deletions tests/test_contention.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""Contention tests -- concurrent claim races, steal guards, and heavy weave append."""

from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

from agentmesh import db
from agentmesh.claims import make_claim, normalize_path
from agentmesh.models import Agent, ResourceType
from agentmesh.waiters import steal_resource
from agentmesh.weaver import append_weave, verify_weave


def _register(agent_id: str, data_dir: Path) -> None:
db.register_agent(Agent(agent_id=agent_id, cwd="/tmp"), data_dir)


# -- S1: same-file claim race --

def test_same_file_claim_race(tmp_data_dir: Path) -> None:
"""Two agents race to claim the same file via ThreadPoolExecutor.

Exactly one must win, one must get conflicts. No orphan active claims.
"""
_register("racer_a", tmp_data_dir)
_register("racer_b", tmp_data_dir)

target = normalize_path("/tmp/contended.py")

def _claim(agent_id: str) -> tuple[bool, list]:
ok, _clm, conflicts = make_claim(agent_id, "/tmp/contended.py", data_dir=tmp_data_dir)
return ok, conflicts

with ThreadPoolExecutor(max_workers=2) as pool:
fut_a = pool.submit(_claim, "racer_a")
fut_b = pool.submit(_claim, "racer_b")
result_a = fut_a.result()
result_b = fut_b.result()

wins = [r for r in [result_a, result_b] if r[0]]
losses = [r for r in [result_a, result_b] if not r[0]]

# Exactly one winner, one loser
assert len(wins) == 1, f"Expected 1 winner, got {len(wins)}"
assert len(losses) == 1, f"Expected 1 loser, got {len(losses)}"

# Loser must have received conflict info
loser_conflicts = losses[0][1]
assert len(loser_conflicts) >= 1, "Loser should see at least 1 conflict"

# No orphan active claims: exactly 1 active edit claim on that path
all_active = db.list_claims(tmp_data_dir, active_only=True)
active_on_target = [c for c in all_active if c.path == target]
assert len(active_on_target) == 1, (
f"Expected exactly 1 active claim on target, got {len(active_on_target)}"
)


# -- S2: port/resource double-claim race --

def test_port_double_claim_race(tmp_data_dir: Path) -> None:
"""Two agents race for PORT:3000; exactly one winner.

db.list_claims(active_only=True) for that port has exactly 1 entry.
"""
_register("port_a", tmp_data_dir)
_register("port_b", tmp_data_dir)

def _claim(agent_id: str) -> bool:
ok, _clm, _conflicts = make_claim(agent_id, "PORT:3000", data_dir=tmp_data_dir)
return ok

with ThreadPoolExecutor(max_workers=2) as pool:
fut_a = pool.submit(_claim, "port_a")
fut_b = pool.submit(_claim, "port_b")
ok_a = fut_a.result()
ok_b = fut_b.result()

# Exactly one winner
assert ok_a != ok_b, f"Expected exactly one winner: a={ok_a}, b={ok_b}"

# Exactly 1 active claim for PORT:3000
all_active = db.list_claims(tmp_data_dir, active_only=True)
port_claims = [
c for c in all_active
if c.resource_type == ResourceType.PORT and c.path == "3000"
]
assert len(port_claims) == 1, (
f"Expected exactly 1 active PORT:3000 claim, got {len(port_claims)}"
)


# -- S4: steal against fresh holder fails --

def test_steal_against_fresh_holder_fails(tmp_data_dir: Path) -> None:
"""Agent tries to steal while holder is active with fresh heartbeat.

Must fail. Holder retains claim.
"""
_register("holder", tmp_data_dir)
_register("thief", tmp_data_dir)

# Holder claims the file with a long TTL
ok, _clm, _conflicts = make_claim(
"holder", "/tmp/guarded.py", ttl_s=3600, data_dir=tmp_data_dir,
)
assert ok, "Holder should acquire claim"

# Refresh holder heartbeat (make it current)
db.update_heartbeat("holder", data_dir=tmp_data_dir)

target = normalize_path("/tmp/guarded.py")

# Thief attempts steal with a short stale threshold
stolen, msg = steal_resource(
"thief", target, reason="hostile takeover", stale_threshold_s=300,
data_dir=tmp_data_dir,
)
assert not stolen, f"Steal should fail, but succeeded with msg: {msg}"
assert "still active" in msg

# Holder retains their active claim
holder_claims = db.list_claims(tmp_data_dir, agent_id="holder", active_only=True)
holder_on_target = [c for c in holder_claims if c.path == target]
assert len(holder_on_target) == 1, "Holder must still own the claim"

# Thief has no active claim on that path
thief_claims = db.list_claims(tmp_data_dir, agent_id="thief", active_only=True)
thief_on_target = [c for c in thief_claims if c.path == target]
assert len(thief_on_target) == 0, "Thief must not hold any claim"


# -- S11: heavy concurrent weave append --

def test_heavy_concurrent_weave_append(tmp_data_dir: Path) -> None:
"""50 concurrent writers via ThreadPoolExecutor(max_workers=16).

All must get unique monotonic sequence IDs, no gaps, verify_weave() passes.
"""
n = 50

def _append(i: int) -> int:
evt = append_weave(capsule_id=f"heavy_{i}", data_dir=tmp_data_dir)
return evt.sequence_id

with ThreadPoolExecutor(max_workers=16) as pool:
seqs = list(pool.map(_append, range(n)))

# All sequence IDs must be unique
assert len(set(seqs)) == n, (
f"Expected {n} unique sequence IDs, got {len(set(seqs))}"
)

# Sorted sequence IDs must form a contiguous range 1..n (no gaps)
assert sorted(seqs) == list(range(1, n + 1)), (
f"Sequence IDs not contiguous 1..{n}: {sorted(seqs)}"
)

# Hash chain must verify cleanly
valid, err = verify_weave(tmp_data_dir)
assert valid, f"Weave verification failed: {err}"
25 changes: 25 additions & 0 deletions tests/test_public_private.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,31 @@ def test_classify_honors_policy_overrides(tmp_path: Path, monkeypatch) -> None:
assert payload["results"][0]["classification"] == PUBLIC


def test_content_scan_exempt_skips_secret_detection(tmp_path: Path) -> None:
"""A file with secret content under an exempt glob is NOT flagged private."""
repo = tmp_path / "repo"
(repo / ".agentmesh").mkdir(parents=True)
(repo / "tests" / "fixtures" / "secrets").mkdir(parents=True)
(repo / "tests" / "fixtures" / "secrets" / "token.py").write_text(
'API_TOKEN = "ghp_R8x2mN4vL6pQ9wK1jT3yF5bA7cE0hU2sG4nM"\n'
)
Comment on lines +122 to +124
(repo / ".agentmesh" / "policy.json").write_text(
json.dumps(
{
"public_private": {
"content_scan_exempt_globs": ["tests/fixtures/secrets/**"],
}
}
)
)

result = classify_path(
repo / "tests" / "fixtures" / "secrets" / "token.py", repo_root=repo,
)
assert result.classification == PUBLIC
assert all("content matches" not in r for r in result.reasons)


def test_classify_docs_public_json_as_public(tmp_path: Path, monkeypatch) -> None:
repo = tmp_path / "repo"
(repo / "docs").mkdir(parents=True)
Expand Down
Loading
Loading