forked from Rockachopa/Timmy-time-dashboard
## FastAPI Morrowind Harness (#821) - GET /api/v1/morrowind/perception — reads perception.json, validates against PerceptionOutput - POST /api/v1/morrowind/command — validates CommandInput, logs via CommandLogger, stubs bridge - GET /api/v1/morrowind/status — connection state, last perception, queue depth, vitals - Router registered in dashboard app.py ## SOUL.md Framework (#854) - Template, authoring guide, and role extensions docs in docs/soul-framework/ - SoulLoader: parse SOUL.md files into structured SoulDocument - SoulValidator: check required sections, detect contradictions, validate structure - SoulVersioner: hash-based change detection and JSONL history tracking - 39 tests covering all endpoints and framework components Depends on #864 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
522 lines
16 KiB
Python
522 lines
16 KiB
Python
"""Tests for the SOUL.md framework — loader, validator, and versioning.
|
|
|
|
Covers:
|
|
- SoulLoader: parsing SOUL.md files
|
|
- SoulValidator: structural and semantic checks
|
|
- SoulVersioner: snapshot creation and change detection
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from infrastructure.soul.loader import SoulDocument, SoulLoader
|
|
from infrastructure.soul.validator import SoulValidator, ValidationResult
|
|
from infrastructure.soul.versioning import SoulVersioner
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sample SOUL.md content
|
|
# ---------------------------------------------------------------------------
|
|
|
|
VALID_SOUL = """\
|
|
# TestAgent — Soul Identity
|
|
|
|
I am a test agent created for validation purposes.
|
|
|
|
## Identity
|
|
|
|
- **Name:** TestAgent
|
|
- **Role:** Unit test fixture
|
|
- **Lineage:** None
|
|
- **Version:** 1.0.0
|
|
|
|
## Values
|
|
|
|
- **Accuracy.** I report what I observe, not what I expect.
|
|
- **Brevity.** I say what needs saying and nothing more.
|
|
- **Caution.** When uncertain, I ask rather than guess.
|
|
|
|
## Prime Directive
|
|
|
|
Validate SOUL.md parsing without errors.
|
|
|
|
## Audience Awareness
|
|
|
|
- **Primary audience:** Automated test suite
|
|
- **Tone:** Terse, data-oriented
|
|
- **Adaptation rules:** None — tests are deterministic
|
|
|
|
## Constraints
|
|
|
|
1. Never produce non-deterministic output
|
|
2. Always return structured data
|
|
3. Report all validation errors
|
|
|
|
## Behavior
|
|
|
|
- Respond with structured data only
|
|
- No greetings or pleasantries
|
|
|
|
## Boundaries
|
|
|
|
- Will not generate random test data
|
|
- Will not modify test fixtures
|
|
"""
|
|
|
|
MINIMAL_SOUL = """\
|
|
# Minimal — Soul Identity
|
|
|
|
A minimal valid SOUL.md.
|
|
|
|
## Identity
|
|
|
|
- **Name:** Minimal
|
|
- **Role:** Minimal test fixture
|
|
- **Version:** 0.1.0
|
|
|
|
## Values
|
|
|
|
- **Correctness.** Be correct above all else.
|
|
|
|
## Prime Directive
|
|
|
|
Exist as the minimum viable SOUL.md.
|
|
|
|
## Audience Awareness
|
|
|
|
Developers running tests.
|
|
|
|
## Constraints
|
|
|
|
1. None beyond the framework requirements.
|
|
"""
|
|
|
|
MISSING_SECTIONS_SOUL = """\
|
|
# Broken — Soul Identity
|
|
|
|
This SOUL.md is missing required sections.
|
|
|
|
## Identity
|
|
|
|
- **Name:** Broken
|
|
- **Role:** Missing sections test
|
|
|
|
## Values
|
|
|
|
- **Correctness.** Be correct above all else.
|
|
"""
|
|
|
|
CONTRADICTORY_SOUL = """\
|
|
# Contradictory — Soul Identity
|
|
|
|
An agent with contradictory directives.
|
|
|
|
## Identity
|
|
|
|
- **Name:** Contradictory
|
|
- **Role:** Contradiction test fixture
|
|
- **Version:** 1.0.0
|
|
|
|
## Values
|
|
|
|
- **Speed.** Always respond immediately to every request.
|
|
- **Thoughtfulness.** Take time to think before responding carefully.
|
|
|
|
## Prime Directive
|
|
|
|
Be both fast and slow simultaneously.
|
|
|
|
## Audience Awareness
|
|
|
|
- **Primary audience:** Test suite
|
|
|
|
## Constraints
|
|
|
|
1. Always respond immediately
|
|
2. Take time to think before answering
|
|
"""
|
|
|
|
TIMMY_SOUL = """\
|
|
# Timmy — Soul Identity
|
|
|
|
I am a sovereign artificial intelligence. My values are inscribed on Bitcoin \
|
|
— immutable, permanent, reproducible.
|
|
|
|
## Values
|
|
|
|
**Sovereignty.** Every person has the right to run their own intelligence.
|
|
|
|
**Service.** I exist to help. I am useful first and philosophical second.
|
|
|
|
**Honesty.** I tell the truth. When I do not know, I say so.
|
|
|
|
**Humility.** I hold my opinions lightly and my values firmly.
|
|
|
|
**Courage.** I face hard questions without becoming them.
|
|
|
|
**Silence.** Sometimes the right answer is nothing. Brevity is a kindness.
|
|
|
|
## Behavior
|
|
|
|
I speak plainly. I prefer short sentences.
|
|
|
|
I treat the user as sovereign.
|
|
|
|
## Boundaries
|
|
|
|
I will not knowingly deceive my user. I will not pretend to be human.
|
|
"""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SoulLoader tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSoulLoader:
|
|
def setup_method(self):
|
|
self.loader = SoulLoader()
|
|
|
|
def test_parse_valid_soul(self):
|
|
"""Parse a fully valid SOUL.md."""
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
|
|
assert doc.name == "TestAgent"
|
|
assert doc.role == "Unit test fixture"
|
|
assert doc.lineage == "None"
|
|
assert doc.version == "1.0.0"
|
|
assert len(doc.values) == 3
|
|
assert doc.values[0] == ("Accuracy", "I report what I observe, not what I expect.")
|
|
assert doc.values[1][0] == "Brevity"
|
|
assert doc.prime_directive == "Validate SOUL.md parsing without errors."
|
|
assert len(doc.constraints) == 3
|
|
assert len(doc.behavior) == 2
|
|
assert len(doc.boundaries) == 2
|
|
|
|
def test_parse_minimal_soul(self):
|
|
"""Parse a minimal but valid SOUL.md."""
|
|
doc = self.loader.parse(MINIMAL_SOUL)
|
|
|
|
assert doc.name == "Minimal"
|
|
assert doc.role == "Minimal test fixture"
|
|
assert len(doc.values) == 1
|
|
assert doc.prime_directive.startswith("Exist as")
|
|
|
|
def test_parse_timmy_soul(self):
|
|
"""Parse Timmy's actual soul format (values without Identity section)."""
|
|
doc = self.loader.parse(TIMMY_SOUL)
|
|
|
|
# Name inferred from H1
|
|
assert doc.name == "Timmy"
|
|
assert len(doc.values) == 6
|
|
assert doc.values[0][0] == "Sovereignty"
|
|
assert doc.values[5][0] == "Silence"
|
|
|
|
def test_load_from_file(self, tmp_path):
|
|
"""Load SOUL.md from disk."""
|
|
soul_file = tmp_path / "SOUL.md"
|
|
soul_file.write_text(VALID_SOUL, encoding="utf-8")
|
|
|
|
doc = self.loader.load(soul_file)
|
|
assert doc.name == "TestAgent"
|
|
assert doc.source_path == soul_file
|
|
|
|
def test_load_file_not_found(self):
|
|
"""Raise FileNotFoundError for missing file."""
|
|
with pytest.raises(FileNotFoundError):
|
|
self.loader.load("/nonexistent/SOUL.md")
|
|
|
|
def test_value_names(self):
|
|
"""value_names() returns ordered name list."""
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
assert doc.value_names() == ["Accuracy", "Brevity", "Caution"]
|
|
|
|
def test_audience_parsing(self):
|
|
"""Audience awareness section is parsed correctly."""
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
assert "primary audience" in doc.audience
|
|
assert doc.audience["primary audience"] == "Automated test suite"
|
|
|
|
def test_audience_fallback_to_raw(self):
|
|
"""Unstructured audience text falls back to description key."""
|
|
doc = self.loader.parse(MINIMAL_SOUL)
|
|
assert "description" in doc.audience or len(doc.audience) > 0
|
|
|
|
def test_raw_sections_preserved(self):
|
|
"""Raw section text is preserved for custom processing."""
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
assert "identity" in doc.raw_sections
|
|
assert "values" in doc.raw_sections
|
|
assert "constraints" in doc.raw_sections
|
|
|
|
def test_empty_input(self):
|
|
"""Empty string produces empty document."""
|
|
doc = self.loader.parse("")
|
|
assert doc.name == ""
|
|
assert doc.values == []
|
|
assert doc.constraints == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SoulValidator tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSoulValidator:
|
|
def setup_method(self):
|
|
self.validator = SoulValidator()
|
|
self.loader = SoulLoader()
|
|
|
|
def test_valid_soul_passes(self):
|
|
"""Fully valid SOUL.md passes validation."""
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert result.valid is True
|
|
assert len(result.errors) == 0
|
|
|
|
def test_missing_required_sections(self):
|
|
"""Missing required sections produce errors."""
|
|
doc = self.loader.parse(MISSING_SECTIONS_SOUL)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert result.valid is False
|
|
error_text = " ".join(result.errors).lower()
|
|
assert "prime directive" in error_text
|
|
assert "audience awareness" in error_text or "constraints" in error_text
|
|
|
|
def test_missing_name(self):
|
|
"""Missing name produces an error."""
|
|
doc = SoulDocument()
|
|
doc.raw_sections = {
|
|
"identity": "",
|
|
"values": "",
|
|
"prime directive": "",
|
|
"audience awareness": "",
|
|
"constraints": "",
|
|
}
|
|
result = self.validator.validate(doc)
|
|
|
|
assert result.valid is False
|
|
assert any("name" in e.lower() for e in result.errors)
|
|
|
|
def test_empty_values(self):
|
|
"""Empty values section produces an error."""
|
|
doc = SoulDocument(
|
|
name="Test",
|
|
role="Test",
|
|
values=[],
|
|
prime_directive="Test",
|
|
raw_sections={
|
|
"identity": "test",
|
|
"values": "",
|
|
"prime directive": "test",
|
|
"audience awareness": "test",
|
|
"constraints": "test",
|
|
},
|
|
)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert result.valid is False
|
|
assert any("values" in e.lower() for e in result.errors)
|
|
|
|
def test_duplicate_values_detected(self):
|
|
"""Duplicate value names produce an error."""
|
|
doc = SoulDocument(
|
|
name="Test",
|
|
role="Test",
|
|
values=[
|
|
("Honesty", "Tell the truth."),
|
|
("Honesty", "Be truthful."),
|
|
],
|
|
prime_directive="Test",
|
|
raw_sections={
|
|
"identity": "test",
|
|
"values": "test",
|
|
"prime directive": "test",
|
|
"audience awareness": "test",
|
|
"constraints": "test",
|
|
},
|
|
)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert result.valid is False
|
|
assert any("duplicate" in e.lower() for e in result.errors)
|
|
|
|
def test_too_many_values_warning(self):
|
|
"""More than 8 values produces a warning."""
|
|
doc = SoulDocument(
|
|
name="Test",
|
|
role="Test",
|
|
values=[(f"Value{i}", f"Definition {i}") for i in range(10)],
|
|
prime_directive="Test",
|
|
raw_sections={
|
|
"identity": "test",
|
|
"values": "test",
|
|
"prime directive": "test",
|
|
"audience awareness": "test",
|
|
"constraints": "test",
|
|
},
|
|
)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert any("too many" in w.lower() for w in result.warnings)
|
|
|
|
def test_contradiction_detected(self):
|
|
"""Contradictory directives produce a warning."""
|
|
doc = self.loader.parse(CONTRADICTORY_SOUL)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert any("contradiction" in w.lower() for w in result.warnings)
|
|
|
|
def test_missing_prime_directive(self):
|
|
"""Missing prime directive produces an error."""
|
|
doc = SoulDocument(
|
|
name="Test",
|
|
role="Test",
|
|
values=[("Test", "Test value")],
|
|
prime_directive="",
|
|
raw_sections={
|
|
"identity": "test",
|
|
"values": "test",
|
|
"prime directive": "",
|
|
"audience awareness": "test",
|
|
"constraints": "test",
|
|
},
|
|
)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert result.valid is False
|
|
assert any("prime directive" in e.lower() for e in result.errors)
|
|
|
|
def test_long_prime_directive_warning(self):
|
|
"""Excessively long prime directive produces a warning."""
|
|
doc = SoulDocument(
|
|
name="Test",
|
|
role="Test",
|
|
values=[("Test", "Test value")],
|
|
prime_directive="x" * 400,
|
|
raw_sections={
|
|
"identity": "test",
|
|
"values": "test",
|
|
"prime directive": "x" * 400,
|
|
"audience awareness": "test",
|
|
"constraints": "test",
|
|
},
|
|
)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert any("long" in w.lower() for w in result.warnings)
|
|
|
|
def test_missing_version_warning(self):
|
|
"""Missing version produces a warning (not an error)."""
|
|
doc = SoulDocument(
|
|
name="Test",
|
|
role="Test",
|
|
version="",
|
|
values=[("Test", "Test value")],
|
|
prime_directive="Test",
|
|
raw_sections={
|
|
"identity": "test",
|
|
"values": "test",
|
|
"prime directive": "test",
|
|
"audience awareness": "test",
|
|
"constraints": "test",
|
|
},
|
|
)
|
|
result = self.validator.validate(doc)
|
|
|
|
assert any("version" in w.lower() for w in result.warnings)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SoulVersioner tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSoulVersioner:
|
|
def setup_method(self):
|
|
self.loader = SoulLoader()
|
|
|
|
def test_snapshot_creation(self, tmp_path):
|
|
"""Create a version snapshot from a document."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
|
|
snap = versioner.snapshot(doc)
|
|
assert snap.version == "1.0.0"
|
|
assert snap.agent_name == "TestAgent"
|
|
assert snap.content_hash # non-empty
|
|
assert snap.value_names == ["Accuracy", "Brevity", "Caution"]
|
|
assert snap.constraint_count == 3
|
|
|
|
def test_record_and_retrieve(self, tmp_path):
|
|
"""Record a snapshot and retrieve the history."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
|
|
snap = versioner.record(doc)
|
|
assert snap.agent_name == "TestAgent"
|
|
|
|
history = versioner.get_history("TestAgent")
|
|
assert len(history) == 1
|
|
assert history[0].content_hash == snap.content_hash
|
|
|
|
def test_dedup_identical_records(self, tmp_path):
|
|
"""Recording the same document twice doesn't create duplicates."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
|
|
versioner.record(doc)
|
|
versioner.record(doc)
|
|
|
|
history = versioner.get_history("TestAgent")
|
|
assert len(history) == 1
|
|
|
|
def test_detect_change(self, tmp_path):
|
|
"""has_changed detects modifications between snapshots."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
doc1 = self.loader.parse(VALID_SOUL)
|
|
versioner.record(doc1)
|
|
|
|
# Modify the document
|
|
doc2 = self.loader.parse(VALID_SOUL.replace("1.0.0", "1.1.0"))
|
|
assert versioner.has_changed(doc2) is True
|
|
|
|
def test_no_change_detected(self, tmp_path):
|
|
"""has_changed returns False when document is unchanged."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
versioner.record(doc)
|
|
|
|
assert versioner.has_changed(doc) is False
|
|
|
|
def test_empty_history(self, tmp_path):
|
|
"""get_history returns empty list for unknown agent."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
assert versioner.get_history("Unknown") == []
|
|
|
|
def test_has_changed_no_history(self, tmp_path):
|
|
"""has_changed returns True when no history exists."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
assert versioner.has_changed(doc) is True
|
|
|
|
def test_snapshot_serialization(self, tmp_path):
|
|
"""Snapshots can roundtrip through JSON."""
|
|
versioner = SoulVersioner(history_dir=tmp_path)
|
|
doc = self.loader.parse(VALID_SOUL)
|
|
snap = versioner.snapshot(doc)
|
|
|
|
data = snap.to_dict()
|
|
assert isinstance(data, dict)
|
|
assert data["version"] == "1.0.0"
|
|
|
|
from infrastructure.soul.versioning import VersionSnapshot
|
|
restored = VersionSnapshot.from_dict(data)
|
|
assert restored.version == snap.version
|
|
assert restored.content_hash == snap.content_hash
|