The sibling module already reaches the scheduler via the late-bound _sched reference, so the public alias was a shim (shape gate: no re-export for internal moves). Tests kept: answer survives a huge prompt, silent run falls through to an older archive, headingless (script-mode) archive injects whole.
101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
"""context_from must inject the previous run's answer, not its prompt (#117290).
|
|
|
|
Stored agent-run archives are ``# Cron Job`` / ``## Prompt`` / ``## Response``
|
|
documents; skill-bearing prompts routinely exceed the 8000-char injection
|
|
budget, so head-truncation amputated the ``## Response`` section and
|
|
self-continuity silently became a no-op.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
|
|
|
|
@pytest.fixture
|
|
def cron_env(tmp_path, monkeypatch):
|
|
"""Isolated cron environment with temp HERMES_HOME."""
|
|
hermes_home = tmp_path / ".hermes"
|
|
hermes_home.mkdir()
|
|
(hermes_home / "cron").mkdir()
|
|
(hermes_home / "cron" / "output").mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
|
|
|
import cron.jobs as jobs_mod
|
|
monkeypatch.setattr(jobs_mod, "HERMES_DIR", hermes_home)
|
|
monkeypatch.setattr(jobs_mod, "CRON_DIR", hermes_home / "cron")
|
|
monkeypatch.setattr(jobs_mod, "JOBS_FILE", hermes_home / "cron" / "jobs.json")
|
|
monkeypatch.setattr(jobs_mod, "OUTPUT_DIR", hermes_home / "cron" / "output")
|
|
|
|
return hermes_home
|
|
|
|
|
|
def _write_archive(cron_env, job_id: str, filename: str, body: str) -> None:
|
|
from cron.jobs import OUTPUT_DIR
|
|
|
|
out_dir = OUTPUT_DIR / job_id
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
(out_dir / filename).write_text(body, encoding="utf-8")
|
|
|
|
|
|
class TestResponseSurvivesLongPrompt:
|
|
"""The answer, not the prompt, is the part continuity needs."""
|
|
|
|
def test_long_prompt_archive_keeps_response(self, cron_env):
|
|
from cron.jobs import create_job
|
|
from cron.scheduler import _build_job_prompt
|
|
|
|
job = create_job(prompt="Run the daily check", schedule="0 8 * * *", context_from="self")
|
|
_write_archive(
|
|
cron_env, job["id"], "2026-09-19_08-00-00.md",
|
|
"# Cron Job: probe\n\n## Prompt\n\n" + "SKILL LINE\n" * 1800 +
|
|
"\n\n## Response\n\nCONCLUSION-MARKER-42\n",
|
|
)
|
|
|
|
prompt = _build_job_prompt(job)
|
|
|
|
assert "CONCLUSION-MARKER-42" in prompt
|
|
assert "SKILL LINE" not in prompt # the prompt half is dropped, not the answer
|
|
|
|
|
|
class TestUnusableAnswersFallThrough:
|
|
"""A [SILENT] or blank response is not usable continuity."""
|
|
|
|
def test_silent_response_falls_through_to_older_archive(self, cron_env):
|
|
from cron.jobs import create_job
|
|
from cron.scheduler_prompt import _inject_context_from
|
|
|
|
job = create_job(prompt="Report", schedule="0 8 * * *", context_from="self")
|
|
_write_archive(
|
|
cron_env, job["id"], "2026-09-18_08-00-00.md",
|
|
"# Cron Job: probe\n\n## Prompt\n\ndo the thing\n\n## Response\n\nOLDER-REAL-ANSWER\n",
|
|
)
|
|
_write_archive(
|
|
cron_env, job["id"], "2026-09-19_08-00-00.md",
|
|
"# Cron Job: probe\n\n## Prompt\n\ndo the thing\n\n## Response\n\n[SILENT]\n",
|
|
)
|
|
|
|
prompt, injected = _inject_context_from(job, "Report")
|
|
|
|
assert injected is True
|
|
assert "OLDER-REAL-ANSWER" in prompt
|
|
assert "[SILENT]" not in prompt
|
|
|
|
class TestScriptModeArchives:
|
|
"""Archives without a ## Response heading (script-mode) stay whole-document."""
|
|
|
|
def test_headingless_archive_injects_whole_document(self, cron_env):
|
|
from cron.jobs import create_job
|
|
from cron.scheduler_prompt import _inject_context_from
|
|
|
|
job = create_job(prompt="Report", schedule="0 8 * * *", context_from="self")
|
|
_write_archive(cron_env, job["id"], "2026-09-19_08-00-00.md", "plain script payload\nline two")
|
|
|
|
prompt, injected = _inject_context_from(job, "Report")
|
|
|
|
assert injected is True
|
|
assert "plain script payload" in prompt
|
|
assert "line two" in prompt
|