This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/test_soul_framework.py
Perplexity Computer f634886f9b feat: FastAPI Morrowind harness + SOUL.md framework (#821, #854)
## 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>
2026-03-21 22:49:56 +00:00

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