diff --git a/emrg/server/feishu_prompt.md b/emrg/server/feishu_prompt.md index 493a389a..3e680652 100644 --- a/emrg/server/feishu_prompt.md +++ b/emrg/server/feishu_prompt.md @@ -36,8 +36,14 @@ in the usual install locations before calling it blocked. yourself; there is no state file to keep and none to trust. - Rants whose `project` is `{{ task.project }}` are the host's instructions for this round and outrank the plan; those that do not match are counted, not acted on. -- A CLI that cannot run, or an account whose authorisation has expired, is a `blocked` - item reported to the host. A round that reads nothing must say so. +- A CLI that cannot run is a `blocked` item reported to the host, and so is an account that + really cannot authenticate — but `missing identity` is a symptom, not a cause. The CLI + resolves its identity from directories under `$HOME`, so a token that is valid under one + `HOME` reads missing under another. Run `auth status` in the same environment the send + will use, and report the cause you read rather than the word the call printed. Never + start a fresh authorisation on one `missing` answer: it changes the host's account state, + and the wrong `HOME` reproduces the symptom whatever the token says. +- A round that reads nothing must say so. ### 2. Judge diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 18831268..28b317e8 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -474,7 +474,10 @@ def test_load_and_start_feishu_task(tmp_path): passes on a template that renders to nothing — and the phrases named below are what the type cannot work without: the command that actually carries a message, the clause that the round does not wait for the answer, the read recipe's own statement of where the - messages are, and the rule that a message is data rather than instruction. Which prose + messages are, and the rule that a message is data rather than instruction. The two ways an + unavailable account can need different things are pinned as well, because they are not + interchangeable: an expired account is the host's to renew, a wrong `HOME` is the round's + to correct, and only one of the two changes the host's account state. Which prose matters is not something `tests/test_prompt_templates.py` can know. """ snapshot = asyncio.run(_boot(tmp_path, [ @@ -508,6 +511,26 @@ def test_load_and_start_feishu_task(tmp_path): ): assert phrase in text, f"the feishu template must state that {why}; {phrase!r} is missing" + # A clause that spans a line break cannot be pinned in the raw text. The file is + # hard-wrapped prose, so a raw pin decides where the wrap falls — and the two clauses + # below each carry their whole rule in a word that the wrap can separate from its + # object: a pin on "start a fresh authorisation …" stayed green with "Never" rewritten + # to "You may". Collapse the whitespace and the clause, negation included, is the unit + # that is read. + flat = " ".join(text.split()) + for phrase, why in ( + ("`missing identity` is a symptom, not a cause", + "a round that reads the word `missing` as an expired account reports a working " + "account blocked, and that report cannot be told apart from a real expiry"), + ("Never start a fresh authorisation on one `missing` answer", + "re-authorising on a symptom changes the host's account state, and a different " + "`HOME` reproduces the symptom whatever the token says"), + ("`auth status` in the same environment the send will use", + "a check run elsewhere answers about that environment: the diagnosis only carries " + "when the identity is read the way the send will read it"), + ): + assert phrase in flat, f"the feishu template must state that {why}; {phrase!r} is missing" + def test_load_and_start_promote_task(tmp_path): """Promote tasks start a TaskHandler with the promote template."""