Compare commits
1 Commits
fix/513
...
fix/570-an
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e278758e0 |
22
ansible/playbooks/deploy_mempalace.yml
Normal file
22
ansible/playbooks/deploy_mempalace.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# ansible/playbooks/deploy_mempalace.yml — Deploy MemPalace v3.0.0 to fleet wizards.
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventory/hosts.ini playbooks/deploy_mempalace.yml --limit ezra
|
||||
# ansible-playbook -i inventory/hosts.ini playbooks/deploy_mempalace.yml
|
||||
#
|
||||
# Refs: Issue #570
|
||||
|
||||
- name: Deploy MemPalace v3.0.0 to wizard hosts
|
||||
hosts: fleet
|
||||
become: false
|
||||
gather_facts: false
|
||||
vars:
|
||||
mempalace_hermes_home: "{{ ansible_env.HOME }}/.hermes"
|
||||
mempalace_sessions_dir: "{{ mempalace_hermes_home }}/sessions"
|
||||
mempalace_palace_path: "{{ ansible_env.HOME }}/.mempalace/palace"
|
||||
mempalace_wing: "{{ inventory_hostname }}_home"
|
||||
roles:
|
||||
- role: ../roles/mempalace
|
||||
vars:
|
||||
mempalace_venv_path: "{{ ansible_env.HOME }}/.mempalace-venv"
|
||||
16
ansible/roles/mempalace/defaults/main.yml
Normal file
16
ansible/roles/mempalace/defaults/main.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
# MemPalace role defaults
|
||||
mempalace_package_spec: "mempalace==3.0.0"
|
||||
mempalace_hermes_home: "{{ ansible_env.HOME }}/.hermes"
|
||||
mempalace_sessions_dir: "{{ mempalace_hermes_home }}/sessions"
|
||||
mempalace_palace_path: "{{ ansible_env.HOME }}/.mempalace/palace"
|
||||
mempalace_wing: "{{ inventory_hostname }}_home"
|
||||
mempalace_wakeup_dir: "{{ mempalace_hermes_home }}/wakeups"
|
||||
mempalace_wakeup_file: "{{ mempalace_wakeup_dir }}/{{ mempalace_wing }}.txt"
|
||||
mempalace_venv_path: "{{ ansible_env.HOME }}/.mempalace-venv"
|
||||
mempalace_config_path: "{{ mempalace_hermes_home }}/mempalace.yaml"
|
||||
mempalace_mcp_config_path: "{{ mempalace_hermes_home }}/hermes-mcp-mempalace.yaml"
|
||||
mempalace_session_hook_path: "{{ mempalace_hermes_home }}/session-start-mempalace.sh"
|
||||
mempalace_run_mining: true
|
||||
mempalace_run_search_test: true
|
||||
mempalace_run_wake_up: true
|
||||
2
ansible/roles/mempalace/meta/main.yml
Normal file
2
ansible/roles/mempalace/meta/main.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
dependencies: []
|
||||
119
ansible/roles/mempalace/tasks/main.yml
Normal file
119
ansible/roles/mempalace/tasks/main.yml
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
# MemPalace v3.0.0 deployment role for fleet wizards.
|
||||
# Refs: Issue #570
|
||||
|
||||
- name: Ensure mempalace venv directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_venv_path }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Create mempalace virtual environment
|
||||
ansible.builtin.command:
|
||||
cmd: "python3 -m venv {{ mempalace_venv_path }}"
|
||||
creates: "{{ mempalace_venv_path }}/bin/python"
|
||||
|
||||
- name: Install mempalace package
|
||||
ansible.builtin.pip:
|
||||
name: "{{ mempalace_package_spec }}"
|
||||
virtualenv: "{{ mempalace_venv_path }}"
|
||||
virtualenv_command: "{{ mempalace_venv_path }}/bin/python -m venv"
|
||||
|
||||
- name: Ensure Hermes home directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_hermes_home }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure sessions directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_sessions_dir }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure wakeup directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_wakeup_dir }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Ensure palace directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ mempalace_palace_path }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
- name: Deploy mempalace.yaml configuration
|
||||
ansible.builtin.template:
|
||||
src: mempalace.yaml.j2
|
||||
dest: "{{ mempalace_config_path }}"
|
||||
mode: '0640'
|
||||
|
||||
- name: Deploy Hermes MCP mempalace config
|
||||
ansible.builtin.template:
|
||||
src: hermes-mcp-mempalace.yaml.j2
|
||||
dest: "{{ mempalace_mcp_config_path }}"
|
||||
mode: '0640'
|
||||
|
||||
- name: Deploy session-start wake-up hook
|
||||
ansible.builtin.template:
|
||||
src: session-start-mempalace.sh.j2
|
||||
dest: "{{ mempalace_session_hook_path }}"
|
||||
mode: '0750'
|
||||
|
||||
- name: Mine Hermes home directory
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
echo "" | {{ mempalace_venv_path }}/bin/mempalace mine {{ mempalace_hermes_home }} --config {{ mempalace_config_path }}
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_mining | bool
|
||||
register: mine_home_result
|
||||
changed_when: mine_home_result.rc == 0
|
||||
|
||||
- name: Mine session history
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
echo "" | {{ mempalace_venv_path }}/bin/mempalace mine {{ mempalace_sessions_dir }} --mode convos --config {{ mempalace_config_path }}
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_mining | bool
|
||||
register: mine_sessions_result
|
||||
changed_when: mine_sessions_result.rc == 0
|
||||
|
||||
- name: Run search test
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
{{ mempalace_venv_path }}/bin/mempalace search "common queries" --config {{ mempalace_config_path }} | head -20
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_search_test | bool
|
||||
register: search_test_result
|
||||
changed_when: false
|
||||
|
||||
- name: Generate wake-up context
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
{{ mempalace_venv_path }}/bin/mempalace wake-up --config {{ mempalace_config_path }} > {{ mempalace_wakeup_file }}
|
||||
export HERMES_MEMPALACE_WAKEUP_FILE="{{ mempalace_wakeup_file }}"
|
||||
printf '[MemPalace] wake-up context refreshed: %s\n' "$HERMES_MEMPALACE_WAKEUP_FILE"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: mempalace_run_wake_up | bool
|
||||
register: wake_up_result
|
||||
changed_when: wake_up_result.rc == 0
|
||||
|
||||
- name: Report MemPalace deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "MemPalace deployed for {{ inventory_hostname }}"
|
||||
- "Package: {{ mempalace_package_spec }}"
|
||||
- "Config: {{ mempalace_config_path }}"
|
||||
- "Palace: {{ mempalace_palace_path }}"
|
||||
- "Wake-up: {{ mempalace_wakeup_file }}"
|
||||
- "MCP config: {{ mempalace_mcp_config_path }}"
|
||||
- "Session hook: {{ mempalace_session_hook_path }}"
|
||||
- "Home mine: {{ 'OK' if mine_home_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
- "Sessions mine: {{ 'OK' if mine_sessions_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
- "Search test: {{ 'OK' if search_test_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
- "Wake-up: {{ 'OK' if wake_up_result.rc | default(1) == 0 else 'SKIPPED' }}"
|
||||
@@ -0,0 +1,6 @@
|
||||
mcp_servers:
|
||||
mempalace:
|
||||
command: "{{ mempalace_venv_path }}/bin/python"
|
||||
args:
|
||||
- -m
|
||||
- mempalace.mcp_server
|
||||
21
ansible/roles/mempalace/templates/mempalace.yaml.j2
Normal file
21
ansible/roles/mempalace/templates/mempalace.yaml.j2
Normal file
@@ -0,0 +1,21 @@
|
||||
wing: {{ mempalace_wing }}
|
||||
palace: {{ mempalace_palace_path }}
|
||||
rooms:
|
||||
- name: sessions
|
||||
description: Conversation history and durable agent transcripts
|
||||
globs:
|
||||
- "*.json"
|
||||
- "*.jsonl"
|
||||
- name: config
|
||||
description: Hermes configuration and runtime settings
|
||||
globs:
|
||||
- "*.yaml"
|
||||
- "*.yml"
|
||||
- "*.toml"
|
||||
- name: docs
|
||||
description: Notes, markdown docs, and operating reports
|
||||
globs:
|
||||
- "*.md"
|
||||
- "*.txt"
|
||||
people: []
|
||||
projects: []
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if command -v {{ mempalace_venv_path }}/bin/mempalace >/dev/null 2>&1; then
|
||||
mkdir -p "{{ mempalace_wakeup_dir }}"
|
||||
{{ mempalace_venv_path }}/bin/mempalace wake-up --config {{ mempalace_config_path }} > "{{ mempalace_wakeup_file }}"
|
||||
export HERMES_MEMPALACE_WAKEUP_FILE="{{ mempalace_wakeup_file }}"
|
||||
printf '[MemPalace] wake-up context refreshed: %s\n' "$HERMES_MEMPALACE_WAKEUP_FILE"
|
||||
fi
|
||||
@@ -146,6 +146,23 @@ That bundle writes:
|
||||
- `session-start-mempalace.sh`
|
||||
- `issue-568-comment-template.md`
|
||||
|
||||
## Fleet Ansible deployment
|
||||
|
||||
Deploy MemPalace to Ezra (or the whole fleet) with the Ansible playbook:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/deploy_mempalace.yml --limit ezra
|
||||
```
|
||||
|
||||
This playbook:
|
||||
1. Creates a dedicated venv and installs `mempalace==3.0.0`
|
||||
2. Deploys `mempalace.yaml`, MCP config, and session-start hook
|
||||
3. Mines the Hermes home and sessions directories
|
||||
4. Runs a search smoke test
|
||||
5. Generates the wake-up context file
|
||||
|
||||
Set `mempalace_run_mining=false` to skip mining on hosts where the corpus is already populated.
|
||||
|
||||
## Why this shape
|
||||
|
||||
- `wing: ezra_home` matches the issue's Ezra-specific integration target.
|
||||
|
||||
@@ -1059,46 +1059,6 @@ class GameEngine:
|
||||
self.log("It will always pulse. That much you know.")
|
||||
self.log("")
|
||||
self.world.save()
|
||||
|
||||
def _bridge_is_hazardous(self):
|
||||
bridge = self.world.rooms["Bridge"]
|
||||
return bool(
|
||||
self.world.state.get("bridge_flooding")
|
||||
or bridge.get("weather") == "rain"
|
||||
or bridge.get("rain_ticks", 0) > 0
|
||||
)
|
||||
|
||||
def _bridge_crossing_extra_cost(self, current_room, dest):
|
||||
if "Bridge" not in (current_room, dest):
|
||||
return 0
|
||||
return 2 if self._bridge_is_hazardous() else 0
|
||||
|
||||
def _event_dialogue(self, char_name, room_name):
|
||||
if char_name == "Bezalel" and room_name == "Forge":
|
||||
if self.world.rooms["Forge"]["fire"] == "cold":
|
||||
return random.choice([
|
||||
"The forge is cold. We cannot work until the fire lives again.",
|
||||
"No forging now. The hearth is dead cold.",
|
||||
])
|
||||
if self.world.state.get("forge_fire_dying"):
|
||||
return random.choice([
|
||||
"The fire is dying. Tend it before the forge goes dark.",
|
||||
"The forge is losing heat. Help me keep it alive.",
|
||||
])
|
||||
|
||||
if char_name == "Ezra" and room_name == "Tower" and self.world.state.get("tower_power_low"):
|
||||
return random.choice([
|
||||
"The Tower power is too low. The servers won't hold a clean study right now.",
|
||||
"The LED is flickering. We need steady power before the Tower can be read properly.",
|
||||
])
|
||||
|
||||
if char_name in {"Marcus", "Allegro"} and room_name == "Bridge" and self._bridge_is_hazardous():
|
||||
return random.choice([
|
||||
"The Bridge is slick with rain. Cross carefully or wait it out.",
|
||||
"This rain changes the Bridge. Don't treat it like dry stone.",
|
||||
])
|
||||
|
||||
return None
|
||||
|
||||
def log(self, message):
|
||||
"""Add to Timmy's log."""
|
||||
@@ -1134,7 +1094,6 @@ class GameEngine:
|
||||
}
|
||||
|
||||
# Process Timmy's action
|
||||
room_name = self.world.characters["Timmy"]["room"]
|
||||
timmy_energy = self.world.characters["Timmy"]["energy"]
|
||||
|
||||
# Energy constraint checks
|
||||
@@ -1197,17 +1156,8 @@ class GameEngine:
|
||||
|
||||
if direction in connections:
|
||||
dest = connections[direction]
|
||||
bridge_extra_cost = self._bridge_crossing_extra_cost(current_room, dest)
|
||||
move_cost = 1 + bridge_extra_cost
|
||||
if self.world.characters["Timmy"]["energy"] < move_cost:
|
||||
scene["log"].append("The rain makes the Bridge too costly to cross right now. Rest first.")
|
||||
scene["room_desc"] = self.world.get_room_desc(current_room, "Timmy")
|
||||
here = [n for n in self.world.characters if self.world.characters[n]["room"] == current_room and n != "Timmy"]
|
||||
scene["here"] = here
|
||||
return scene
|
||||
|
||||
self.world.characters["Timmy"]["room"] = dest
|
||||
self.world.characters["Timmy"]["energy"] -= move_cost
|
||||
self.world.characters["Timmy"]["energy"] -= 1
|
||||
|
||||
scene["log"].append(f"You move {direction} to The {dest}.")
|
||||
scene["timmy_room"] = dest
|
||||
@@ -1215,8 +1165,6 @@ class GameEngine:
|
||||
# Check for rain on bridge
|
||||
if dest == "Bridge" and self.world.rooms["Bridge"]["weather"] == "rain":
|
||||
scene["world_events"].append("Rain mists on the dark water below. The railing is slick.")
|
||||
if bridge_extra_cost:
|
||||
scene["log"].append("Rain turns the Bridge crossing into work. You brace against the slick stone. (-2 extra energy)")
|
||||
|
||||
# Check trust changes for arrival
|
||||
here = [n for n in self.world.characters if self.world.characters[n]["room"] == dest and n != "Timmy"]
|
||||
@@ -1362,69 +1310,25 @@ class GameEngine:
|
||||
|
||||
elif timmy_action == "write_rule":
|
||||
if self.world.characters["Timmy"]["room"] == "Tower":
|
||||
if self.world.state.get("tower_power_low"):
|
||||
scene["world_events"].append("The Tower power is too low. The LED flickers over the whiteboard.")
|
||||
scene["log"].append("The power is too low to write a new rule.")
|
||||
else:
|
||||
rules = [
|
||||
f"Rule #{self.world.tick}: The room remembers those who enter it.",
|
||||
f"Rule #{self.world.tick}: A man in the dark needs to know someone is in the room.",
|
||||
f"Rule #{self.world.tick}: The forge does not care about your schedule.",
|
||||
f"Rule #{self.world.tick}: Every footprint on the stone means someone made it here.",
|
||||
f"Rule #{self.world.tick}: The bridge does not judge. It only carries.",
|
||||
f"Rule #{self.world.tick}: A seed planted in patience grows in time.",
|
||||
f"Rule #{self.world.tick}: What is carved in wood outlasts what is said in anger.",
|
||||
f"Rule #{self.world.tick}: The garden grows whether anyone watches or not.",
|
||||
f"Rule #{self.world.tick}: Trust is built one tick at a time.",
|
||||
f"Rule #{self.world.tick}: The fire remembers who tended it.",
|
||||
]
|
||||
new_rule = random.choice(rules)
|
||||
self.world.rooms["Tower"]["messages"].append(new_rule)
|
||||
self.world.characters["Timmy"]["energy"] -= 1
|
||||
scene["log"].append(f"You write on the Tower whiteboard: \"{new_rule}\"")
|
||||
rules = [
|
||||
f"Rule #{self.world.tick}: The room remembers those who enter it.",
|
||||
f"Rule #{self.world.tick}: A man in the dark needs to know someone is in the room.",
|
||||
f"Rule #{self.world.tick}: The forge does not care about your schedule.",
|
||||
f"Rule #{self.world.tick}: Every footprint on the stone means someone made it here.",
|
||||
f"Rule #{self.world.tick}: The bridge does not judge. It only carries.",
|
||||
f"Rule #{self.world.tick}: A seed planted in patience grows in time.",
|
||||
f"Rule #{self.world.tick}: What is carved in wood outlasts what is said in anger.",
|
||||
f"Rule #{self.world.tick}: The garden grows whether anyone watches or not.",
|
||||
f"Rule #{self.world.tick}: Trust is built one tick at a time.",
|
||||
f"Rule #{self.world.tick}: The fire remembers who tended it.",
|
||||
]
|
||||
new_rule = random.choice(rules)
|
||||
self.world.rooms["Tower"]["messages"].append(new_rule)
|
||||
self.world.characters["Timmy"]["energy"] -= 1
|
||||
scene["log"].append(f"You write on the Tower whiteboard: \"{new_rule}\"")
|
||||
else:
|
||||
scene["log"].append("You are not in the Tower.")
|
||||
|
||||
elif timmy_action == "study":
|
||||
if self.world.characters["Timmy"]["room"] == "Tower":
|
||||
if self.world.state.get("tower_power_low"):
|
||||
scene["world_events"].append("The Tower power is too low. The servers stutter in weak light.")
|
||||
scene["log"].append("The power is too low to study the servers.")
|
||||
else:
|
||||
insights = [
|
||||
"You study the server rhythm until the pulse resolves into something readable.",
|
||||
"You trace the signal paths and feel the Tower settle into focus.",
|
||||
"You study the green LED and the server racks until the pattern becomes clear.",
|
||||
]
|
||||
insight = random.choice(insights)
|
||||
self.world.characters["Timmy"]["energy"] -= 1
|
||||
self.world.characters["Timmy"]["memories"].append(insight)
|
||||
scene["log"].append(insight)
|
||||
scene["world_events"].append("The Tower answers with a steady hum.")
|
||||
else:
|
||||
scene["log"].append("You are not in the Tower.")
|
||||
|
||||
elif timmy_action == "forge":
|
||||
if self.world.characters["Timmy"]["room"] == "Forge":
|
||||
forge_fire = self.world.rooms["Forge"]["fire"]
|
||||
if forge_fire == "cold":
|
||||
scene["world_events"].append("The forge is cold. No metal will take shape here yet.")
|
||||
scene["log"].append("The forge is cold. Tend the fire before you try to forge.")
|
||||
else:
|
||||
forged_items = [
|
||||
f"bridge nail #{self.world.tick}",
|
||||
f"tower key blank #{self.world.tick}",
|
||||
f"garden trowel #{self.world.tick}",
|
||||
]
|
||||
forged_item = random.choice(forged_items)
|
||||
self.world.rooms["Forge"]["forged_items"].append(forged_item)
|
||||
self.world.characters["Timmy"]["energy"] -= 2
|
||||
self.world.state["items_crafted"] += 1
|
||||
scene["log"].append(f"You forge {forged_item} at the anvil.")
|
||||
scene["world_events"].append("The anvil rings and the hearth answers.")
|
||||
else:
|
||||
scene["log"].append("You are not in the Forge.")
|
||||
|
||||
elif timmy_action == "carve":
|
||||
if self.world.characters["Timmy"]["room"] == "Bridge":
|
||||
carvings = [
|
||||
@@ -1510,11 +1414,7 @@ class GameEngine:
|
||||
speech_chance = 0.20
|
||||
|
||||
if random.random() < speech_chance:
|
||||
event_line = self._event_dialogue(char_name, room_name)
|
||||
if event_line:
|
||||
self.world.characters[char_name]["spoken"].append(event_line)
|
||||
scene["log"].append(f"{char_name} says: \"{event_line}\"")
|
||||
elif char_name == "Marcus":
|
||||
if char_name == "Marcus":
|
||||
marcus_pool = self.DIALOGUES["Marcus"].get(phase, self.DIALOGUES["Marcus"]["quietus"])
|
||||
line = random.choice(marcus_pool)
|
||||
self.world.characters[char_name]["spoken"].append(line)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -67,82 +66,6 @@ class TestEvenniaLocalWorldGame(unittest.TestCase):
|
||||
self.assertIn("Ezra is already here.", result["log"])
|
||||
self.assertIn("The servers hum steady. The green LED pulses.", result["world_events"])
|
||||
|
||||
def test_bridge_rain_crossing_costs_extra_energy_and_warns(self):
|
||||
module = load_game_module()
|
||||
|
||||
dry_engine = module.GameEngine()
|
||||
dry_engine.start_new_game()
|
||||
dry_engine.world.update_world_state = lambda: None
|
||||
dry_engine.world.characters["Timmy"]["energy"] = 10
|
||||
dry_result = dry_engine.run_tick("move:south")
|
||||
dry_energy = dry_engine.world.characters["Timmy"]["energy"]
|
||||
|
||||
rainy_engine = module.GameEngine()
|
||||
rainy_engine.start_new_game()
|
||||
rainy_engine.world.update_world_state = lambda: None
|
||||
rainy_engine.world.characters["Timmy"]["energy"] = 10
|
||||
rainy_engine.world.rooms["Bridge"]["weather"] = "rain"
|
||||
rainy_engine.world.rooms["Bridge"]["rain_ticks"] = 3
|
||||
rainy_engine.world.state["bridge_flooding"] = True
|
||||
rainy_result = rainy_engine.run_tick("move:south")
|
||||
|
||||
self.assertEqual(rainy_engine.world.characters["Timmy"]["room"], "Bridge")
|
||||
self.assertLess(rainy_engine.world.characters["Timmy"]["energy"], dry_energy)
|
||||
self.assertTrue(
|
||||
any("bridge" in line.lower() and ("rain" in line.lower() or "slick" in line.lower()) for line in rainy_result["log"] + rainy_result["world_events"]),
|
||||
rainy_result,
|
||||
)
|
||||
|
||||
def test_tower_power_low_blocks_study_and_write_rule(self):
|
||||
module = load_game_module()
|
||||
engine = module.GameEngine()
|
||||
engine.start_new_game()
|
||||
engine.world.update_world_state = lambda: None
|
||||
engine.world.characters["Timmy"]["room"] = "Tower"
|
||||
engine.world.characters["Timmy"]["energy"] = 10
|
||||
engine.world.state["tower_power_low"] = True
|
||||
|
||||
rules_before = list(engine.world.rooms["Tower"]["messages"])
|
||||
study_result = engine.run_tick("study")
|
||||
self.assertEqual(engine.world.characters["Timmy"]["energy"], 10)
|
||||
self.assertTrue(
|
||||
any("power" in line.lower() and ("study" in line.lower() or "servers" in line.lower()) for line in study_result["log"] + study_result["world_events"]),
|
||||
study_result,
|
||||
)
|
||||
|
||||
write_result = engine.run_tick("write_rule")
|
||||
self.assertEqual(engine.world.rooms["Tower"]["messages"], rules_before)
|
||||
self.assertTrue(
|
||||
any("power" in line.lower() and ("write" in line.lower() or "whiteboard" in line.lower()) for line in write_result["log"] + write_result["world_events"]),
|
||||
write_result,
|
||||
)
|
||||
|
||||
def test_cold_forge_blocks_forge_action_and_bezalel_reacts(self):
|
||||
module = load_game_module()
|
||||
engine = module.GameEngine()
|
||||
engine.start_new_game()
|
||||
engine.world.update_world_state = lambda: None
|
||||
engine.npc_ai.make_choice = lambda _name: None
|
||||
engine.world.characters["Timmy"]["room"] = "Forge"
|
||||
engine.world.characters["Timmy"]["energy"] = 10
|
||||
engine.world.characters["Bezalel"]["room"] = "Forge"
|
||||
engine.world.rooms["Forge"]["fire"] = "cold"
|
||||
engine.world.state["forge_fire_dying"] = True
|
||||
forged_before = list(engine.world.rooms["Forge"]["forged_items"])
|
||||
|
||||
with patch.object(module.random, "random", return_value=0.0), patch.object(module.random, "choice", side_effect=lambda seq: seq[0]):
|
||||
result = engine.run_tick("forge")
|
||||
|
||||
self.assertEqual(engine.world.rooms["Forge"]["forged_items"], forged_before)
|
||||
self.assertTrue(
|
||||
any("forge" in line.lower() and ("cold" in line.lower() or "fire" in line.lower()) for line in result["log"] + result["world_events"]),
|
||||
result,
|
||||
)
|
||||
self.assertTrue(
|
||||
any(line.startswith("Bezalel says:") and ("fire" in line.lower() or "forge" in line.lower()) for line in result["log"]),
|
||||
result,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
92
tests/test_mempalace_ansible_role.py
Normal file
92
tests/test_mempalace_ansible_role.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ROLE_PATH = ROOT / "ansible" / "roles" / "mempalace"
|
||||
PLAYBOOK_PATH = ROOT / "ansible" / "playbooks" / "deploy_mempalace.yml"
|
||||
|
||||
|
||||
class TestMempalaceAnsibleRole(unittest.TestCase):
|
||||
def test_role_directory_structure_exists(self):
|
||||
self.assertTrue(ROLE_PATH.exists(), "mempalace role directory missing")
|
||||
for subdir in ["tasks", "templates", "defaults", "meta"]:
|
||||
self.assertTrue(
|
||||
(ROLE_PATH / subdir).exists(),
|
||||
f"mempalace role subdir missing: {subdir}",
|
||||
)
|
||||
|
||||
def test_role_defaults_contains_required_variables(self):
|
||||
defaults_path = ROLE_PATH / "defaults" / "main.yml"
|
||||
self.assertTrue(defaults_path.exists())
|
||||
text = defaults_path.read_text(encoding="utf-8")
|
||||
required_vars = [
|
||||
"mempalace_package_spec",
|
||||
"mempalace_hermes_home",
|
||||
"mempalace_sessions_dir",
|
||||
"mempalace_palace_path",
|
||||
"mempalace_wing",
|
||||
"mempalace_wakeup_dir",
|
||||
"mempalace_wakeup_file",
|
||||
"mempalace_venv_path",
|
||||
"mempalace_config_path",
|
||||
"mempalace_mcp_config_path",
|
||||
"mempalace_session_hook_path",
|
||||
"mempalace_run_mining",
|
||||
"mempalace_run_search_test",
|
||||
"mempalace_run_wake_up",
|
||||
]
|
||||
for var in required_vars:
|
||||
self.assertIn(var, text, f"missing default var: {var}")
|
||||
|
||||
def test_role_tasks_contain_required_steps(self):
|
||||
tasks_path = ROLE_PATH / "tasks" / "main.yml"
|
||||
self.assertTrue(tasks_path.exists())
|
||||
text = tasks_path.read_text(encoding="utf-8")
|
||||
required_steps = [
|
||||
"Create mempalace virtual environment",
|
||||
"Install mempalace package",
|
||||
"Deploy mempalace.yaml configuration",
|
||||
"Deploy Hermes MCP mempalace config",
|
||||
"Deploy session-start wake-up hook",
|
||||
"Mine Hermes home directory",
|
||||
"Mine session history",
|
||||
"Run search test",
|
||||
"Generate wake-up context",
|
||||
]
|
||||
for step in required_steps:
|
||||
self.assertIn(step, text, f"missing task: {step}")
|
||||
|
||||
def test_role_templates_are_valid(self):
|
||||
yaml_template = ROLE_PATH / "templates" / "mempalace.yaml.j2"
|
||||
mcp_template = ROLE_PATH / "templates" / "hermes-mcp-mempalace.yaml.j2"
|
||||
hook_template = ROLE_PATH / "templates" / "session-start-mempalace.sh.j2"
|
||||
|
||||
self.assertTrue(yaml_template.exists())
|
||||
self.assertTrue(mcp_template.exists())
|
||||
self.assertTrue(hook_template.exists())
|
||||
|
||||
yaml_text = yaml_template.read_text(encoding="utf-8")
|
||||
self.assertIn("wing: {{ mempalace_wing }}", yaml_text)
|
||||
self.assertIn("palace: {{ mempalace_palace_path }}", yaml_text)
|
||||
self.assertIn("rooms:", yaml_text)
|
||||
|
||||
mcp_text = mcp_template.read_text(encoding="utf-8")
|
||||
self.assertIn("mcp_servers:", mcp_text)
|
||||
self.assertIn("mempalace:", mcp_text)
|
||||
self.assertIn("mempalace.mcp_server", mcp_text)
|
||||
|
||||
hook_text = hook_template.read_text(encoding="utf-8")
|
||||
self.assertIn("mempalace wake-up", hook_text)
|
||||
self.assertIn("HERMES_MEMPALACE_WAKEUP_FILE", hook_text)
|
||||
|
||||
def test_playbook_exists_and_targets_fleet(self):
|
||||
self.assertTrue(PLAYBOOK_PATH.exists(), "deploy_mempalace.yml playbook missing")
|
||||
text = PLAYBOOK_PATH.read_text(encoding="utf-8")
|
||||
self.assertIn("hosts: fleet", text)
|
||||
self.assertIn("../roles/mempalace", text)
|
||||
self.assertIn("mempalace_venv_path", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -85,6 +85,8 @@ class TestMempalaceEzraIntegration(unittest.TestCase):
|
||||
"mcp_servers:",
|
||||
"HERMES_MEMPALACE_WAKEUP_FILE",
|
||||
"Metrics reply for #568",
|
||||
"Fleet Ansible deployment",
|
||||
"ansible-playbook",
|
||||
]
|
||||
for snippet in required:
|
||||
self.assertIn(snippet, text)
|
||||
|
||||
Reference in New Issue
Block a user