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
7 changes: 7 additions & 0 deletions emrg/server/feishu_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ so the report names the Feishu message id and the `oc_…` conversation it was a
request without them is one the host has not been told about, and the round counts it as unasked
rather than as progress.

A conclusion about how something ran carries the evidence it rests on. When the report says
which step failed, who errored or what the cause was, it names the file that answer was read
from and an address the reader can open for that file — one per file involved, because a
conclusion drawn from three of them is three readings and a reader checking one alone cannot
tell which of them said it. A file the round could not retrieve is named as well, with the step
it could not pass: leaving it out reads as though it had been read and found to say nothing.

### Forbidden

- 🛑 Never message anyone but the host's own account without the host's approval for that exact message
Expand Down
92 changes: 88 additions & 4 deletions tests/test_feishu_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@
#: The first step of the round — the thing the rule has to precede to bind it.
FIRST_STEP = "### 1. Read"

#: §4 defines the report, so it is where the obligation to name evidence belongs.
REPORT_HEADING = "### 4. Report"

#: One half of that obligation: a conclusion points at the file it was read from.
LOG_ADDRESS_MARK = "an address the reader can open"

#: The other half: a file that could not be read is said to be unread, not left out.
UNRETRIEVED_MARK = "could not retrieve"

#: And the reason the clause is not satisfied by one address: three files are three readings.
PER_FILE_MARK = "one per file"

#: The agent name the render fixture asks for — a name in no template, so "the render
#: carries it" proves a placeholder resolved rather than a string being copied through.
PROMPT_PROJECT = "demoproj"
Expand All @@ -93,13 +105,13 @@
EMPTY_ROUND_MARK = "a correct round"


def _cadence_block(text: str) -> str | None:
"""The `### 0. Cadence` block as a reader meets it, or `None` when it is absent.
def _section(text: str, heading: str) -> str | None:
"""The block under `heading` as a reader meets it, or `None` when it is absent.

Deliberately answers rather than asserting, so the both-ways control below can show
Deliberately answers rather than asserting, so the both-ways controls below can show
that a missing block is reported as missing instead of as nothing to check.
"""
marker = f"\n{HEADING}"
marker = f"\n{heading}"
start = text.find(marker)
if start < 0:
return None
Expand All @@ -108,6 +120,11 @@ def _cadence_block(text: str) -> str | None:
return rest[: end.start()] if end else rest


def _cadence_block(text: str) -> str | None:
"""The `### 0. Cadence` block — the section this file's first rule binds."""
return _section(text, HEADING)


def _require_block(text: str, subject: str) -> str:
block = _cadence_block(text)
assert block is not None, (
Expand Down Expand Up @@ -334,3 +351,70 @@ def test_the_cadence_scanner_answers_both_ways():
assert _cadence_block("\n### 1. Read\n") is None, (
"a template without the block was reported as carrying one"
)


def test_the_template_says_a_conclusion_names_the_file_it_was_read_from():
"""§4 owes the report an evidence clause, and its halves are asserted separately.

One string carrying all three would pass on a sentence that kept the words and lost
the requirement, so each half is its own reading (issue #2670).
"""
block = _section(PROMPT.read_text(encoding="utf-8"), REPORT_HEADING)
assert block is not None, (
f"`emrg/server/feishu_prompt.md` carries no `{REPORT_HEADING}` block — the report is "
"defined nowhere, so a conclusion about how something ran is free to arrive with no "
"evidence behind it (issue #2670)"
)
assert LOG_ADDRESS_MARK in block, (
"§4 no longer says a conclusion names an address the reader can open for the file it "
"was read from — an unverifiable conclusion cannot be told from a guess, and the host "
"acts on it either way"
)
assert PER_FILE_MARK in block, (
"§4 lost the one-address-per-file clause: a conclusion drawn from three files is three "
"readings, and a reader who can open only one cannot tell which of them said it"
)
assert UNRETRIEVED_MARK in block, (
"§4 no longer says a file the round could not retrieve is named as such — leaving it "
"out reads exactly like having read it and found nothing in it, so the one case where "
"the round is blind is the case that looks cleanest"
)


def test_the_report_rule_reaches_the_round_and_not_merely_the_file(rendered):
"""The same clause asserted on the prompt a round is sent: a Jinja construct hides it in the file."""
block = _section(rendered, REPORT_HEADING)
assert block is not None, (
f"the rendered Feishu prompt has no `{REPORT_HEADING}` block at all — whether or not "
"the file still carries one"
)
missing = [
mark for mark in (LOG_ADDRESS_MARK, PER_FILE_MARK, UNRETRIEVED_MARK) if mark not in block
]
assert not missing, (
f"the rendered report section does not carry {missing}: the clause is in the file and "
"in no render, which is the one failure a file-level guard cannot see"
)


def test_the_report_scanner_answers_both_ways():
"""The both-ways control for the section this file's second rule binds.

A scanner that answered the whole text for a missing marker would make the assertions
above pass on a template that had lost §4, so its absence is measured too — and so is a
block that runs to the end of the file, where there is no following heading to stop at.
"""
present = "\n### 4. Report\n\nrule text\n\n### Forbidden\n\nbody\n"
block = _section(present, REPORT_HEADING)
assert block is not None
assert "rule text" in block
assert "### Forbidden" not in block, (
"the scanner ran past the next heading, so a deleted block would be read as its "
"neighbours"
)
assert _section(present, "### 5. Absent") is None, (
"a heading the template does not carry was reported as present"
)
assert _section("\n### 4. Report\n\nrule text", REPORT_HEADING) is not None, (
"a block that is last in the file, with no heading after it, was reported as missing"
)
Loading