raw derive/fetch/pending: Länge als dritte Größenachse beim Ingest - eine CHECK-Zeile nennt jeden geschriebenen Text über LONG_TEXT_CHARS (Zeichen der .md wie geschrieben), raw pending markiert eine lange .md/.txt mit long:; ingest-large-tree liest ihn in Teilen nach der Gliederung, je Teil nur den Zeilenbereich, Extrakt als Faktenliste mit Fundstelle und Modalität (#226); 10.1.0-beta.10
CI / verify (push) Successful in 7m55s
CI / pwsh (push) Successful in 2m41s
Converters / converters (push) Successful in 9m32s
Release / converters (push) Successful in 27s
Release / release (push) Successful in 37s
Release / wiki-mirror (push) Skipped

Files changed:
- CHANGES.md
- VERSION
- docs/how-to/ingest-a-source.md
- docs/reference/commands/raw-material-and-uploads.md
- instructions/ingest-large-tree.md
- instructions/wiki-ingest/SKILL.md
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/chemenu/commands/raw/_common.py
- tools/chemenu/commands/raw/derive.py
- tools/chemenu/commands/raw/fetch.py
- tools/chemenu/commands/raw/pending.py
- tools/chemenu/tests/test_raw_derive.py
- tools/chemenu/tests/test_raw_fetch.py
- types/source.guidance.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Opus 5.5 committed 2026-10-15 21:21:24 +02:00
1 parent 9cb981a4cd
commit eae4a427d2
15 files changed
+341 -37

No files matched your search

+5 -2
View File
@@ -1495,6 +1495,7 @@ Capture a web page the user names into `incoming/`: the HTML as received plus a
- Fetch routes: the `fetch` section of the settings sends a host's pages another way. `session` fetches them with the cookies of the session the user stored with `raw login`, still with the built-in fetch and the bytes as received; the header adds `session: <host>` and never the session's content. The host's site handler names how a page shows it arrived signed in: no stored session, or a page without that sign (an expired session, a teaser), stops with exit 42 and writes nothing. Any other route names an external fetcher of this checkout: run without a shell with the URL as its last argument (120 s, 25 MiB), its stdout is the `.html` byte for byte, and the header says `fetched_by: wikitool raw fetch (fetcher: <name>)` and carries no `http_status`. A fetcher that fails is never replaced by the built-in fetch. A host no route names is fetched as before.
- Rendered pages: a site handler that declares `render` has its pages fetched through a headless browser - with the session, when the host has one - and the `.html` is the page as the browser drew it, not the bytes the server sent; the header says `rendered: true`. No page is rendered without that declaration, and an external fetcher's page never is. It needs the optional browser component; without it the fetch stops with exit 42. `--scope` fetches every page of the bundle the same way, and `_scope.json` names the `session`, `fetcher` or `rendered` it used.
- `--html` derives the `.md` beside a page saved to `incoming/` from a logged-in browser - the way past a paywall or a script-rendered page. Its header carries `derived` (when the text was derived) instead of `retrieved`, `final_url`, `http_status` and `content_type`, and the `.html` is left untouched.
- Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or `text/markdown` response stored as received - that is longer than 50,000 characters, the threshold of `raw derive`: more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`).
- Success prints the `raw accept` line for the written files and the `source_url` for the source page.
**SEE ALSO**
@@ -1613,6 +1614,7 @@ Derive reading text from a PDF, e-mail, Office file or web page in `incoming/`,
- A ZIP and a mail are containers. A ZIP at the top of `incoming/` becomes the folder `incoming/<stem>/` holding its files under their own names, each with a `<stem>.md` where a filter reads it; the ZIP is removed and the output names it with its sha256. A mail becomes a folder with `original.eml`, `derived.md` and its attachments under their own names; a ZIP attached to it is unpacked once into a folder of its own, and an attached mail is quoted in `derived.md`, not unpacked. A ZIP inside a ZIP, an absolute or `..` path, a symbolic link, an encrypted entry, more than 1000 entries, a file over 25 MiB or more than 250 MiB in total refuse the whole call. Hidden entries (`__MACOSX/`, `.DS_Store`, anything starting with a dot) are named, not written. A name taken twice, or reserved (`original.*` and `derived.md` at the top of a mail's folder, `_capture.json` anywhere), gets `-2`, `-3` before its extension, and the output lists each rename.
- A name already taken under `raw/` is a WARN, not a refusal: whether the folder is a later edition of that source or a separate one is the human's call. A later edition of a derived source is replaced file by file with `raw accept --replaces`; there is no `--replaces-bundle` for it.
- After a success the output prints the warnings the filters raised, at most three `CHECK` lines per format that was derived in this run - what to look at before the text is trusted - and the `raw accept` line for the result.
- Length: one more `CHECK` line names every `.md` this run wrote that is longer than 50,000 characters, counted in the file as written, header included - more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a model's tokenizer. `raw fetch` and `raw pending` measure against the same value.
- Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.
**SEE ALSO**
@@ -1766,9 +1768,10 @@ List what waits in `incoming/`, oldest first, and name the entry an ingest witho
- Order: oldest first by modification time. A bundle or folder counts as new as its newest file; a tie goes by name. The mtime is when a document last changed only if it was copied with its timestamps kept (`cp -p`, `rsync -a`, an unpacked archive) - for a download or a `raw fetch` it is merely when it was dropped.
- Each candidate shows its path(s), kind, file count and mtime, and whether `raw accept` would take it as it stands - the same checks, minus `--fidelity`/`--authority`. One it would refuse is listed with the reason and skipped: it needs a human.
- The default is the first candidate `raw accept` would take, and the output names it.
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`.
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`, `length`.
- A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is listed with the `raw derive` line for it, in text output and as `hint` in JSON (`null` for every other candidate). The hint changes nothing about the order or the default.
- Reads directory listings and `lstat` only, never a file's content; an empty `incoming/` is exit 0 with nothing to do.
- A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw fetch` is listed with its length in characters, in text output and as `length` in JSON (`null` for every other candidate) - that text reaches a session with no derivation in front of it to print the length `CHECK`. Like the hint, it changes nothing about the order or the default.
- Reads directory listings and `lstat`, and a file's content only to measure a lone `.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more characters than bytes; an empty `incoming/` is exit 0 with nothing to do.
**SEE ALSO**
+50
View File
@@ -508,6 +508,56 @@ def _derive_hint(path: Path) -> Optional[str]:
return f"tools/wikitool raw derive {rel_path(path)}"
# The length axis of an ingest (Gitea #226): a text longer than this many characters
# is read in parts, through `instructions/ingest-large-tree.md`, not in one pass.
# Characters rather than tokens, because they do not depend on a model's tokenizer:
# ~15 k tokens of German at ~3.3 characters a token, set early because a session's
# fixed load already fills much of its context. The value stands here and nowhere
# else - `raw derive`, `raw fetch` and `raw pending` all read it from this line.
LONG_TEXT_CHARS = 50_000
# The files `raw pending` measures: text a session reads as it is, with no derivation
# - and so no `raw derive` or `raw fetch` - in front of it to print the length CHECK.
_PLAIN_TEXT_SUFFIXES = (".md", ".txt")
def _char_count(data: bytes) -> int:
"""Characters of a UTF-8 text, the way a session reads it: an undecodable
byte counts as the one replacement character it becomes."""
return len(data.decode("utf-8", errors="replace"))
def _long_text_check(texts: list[tuple[Path, bytes]]) -> Optional[str]:
"""The one `CHECK` line for the texts longer than `LONG_TEXT_CHARS`, else None.
A UTF-8 text has no more characters than bytes, so a short one is not decoded."""
over = [
(path, count) for path, data in texts
if len(data) > LONG_TEXT_CHARS and (count := _char_count(data)) > LONG_TEXT_CHARS
]
if not over:
return None
named = ", ".join(f"{rel_path(path)} ({count:,} characters)" for path, count in over)
return (
f"Longer than one ingest reads in one pass ({LONG_TEXT_CHARS:,} characters): {named}. "
"Read it in parts - instructions/ingest-large-tree.md, the length axis."
)
def _plain_text_length(path: Path) -> Optional[int]:
"""The characters of a lone `.md`/`.txt` file when they are over
`LONG_TEXT_CHARS`, else None. Reads the file only when its size could put
it over - `lstat` decides every other case."""
if path.suffix.lower() not in _PLAIN_TEXT_SUFFIXES or path.is_symlink() or not path.is_file():
return None
if path.lstat().st_size <= LONG_TEXT_CHARS:
return None
try:
count = _char_count(path.read_bytes())
except OSError:
return None
return count if count > LONG_TEXT_CHARS else None
def _unusable_name(name: str) -> bool:
"""Not one plain directory name: empty, padded, hidden, a path, a control
character, or the manifest's reserved name."""
+27 -3
View File
@@ -28,7 +28,9 @@ from chemenu.commands.raw._common import (
_Refused,
_check_directly_in_incoming,
_folder_collision_message,
LONG_TEXT_CHARS,
_incoming_dir,
_long_text_check,
_occupied_stems,
_refusals_fail,
_resolve,
@@ -284,19 +286,36 @@ def _plan_derive_folder(folder: Path):
def _echo_notes(
derivations: list[filters.Derivation], container: Optional[filters.Container] = None
derivations: list[filters.Derivation],
texts: list[tuple[Path, bytes]],
container: Optional[filters.Container] = None,
) -> None:
"""The filters' warnings and CHECKs, then the length CHECK over `texts` -
every `.md` this run wrote, as written."""
warnings = [w for d in derivations for w in d.warnings]
checks = [c for d in derivations for c in d.checks]
if container is not None:
warnings += container.warnings
checks += container.checks
long_text = _long_text_check(texts)
if long_text is not None:
checks.append(long_text)
for warning in filters.unique(warnings):
typer.echo(f"WARN {warning}")
for line in filters.unique(checks):
typer.echo(f"CHECK {line}")
def _written_texts(plan: _FilePlan) -> list[tuple[Path, bytes]]:
"""Every `.md` the single-file form writes: `derived.md`, and the `.md`
beside each member of a container that a filter read."""
texts = [(plan.derived, plan.markdown)] if plan.derived is not None else []
if plan.container is not None:
members = dict(plan.container.files)
texts += [(plan.folder / md, members[md]) for _src, md, _name in plan.container.derived]
return texts
def _container_report(container: filters.Container) -> list[str]:
lines = [f" {src} -> {md} ({name})" for src, md, name in container.derived]
if container.plain:
@@ -413,6 +432,11 @@ def _derive_next(target: Path) -> str:
"After a success the output prints the warnings the filters raised, at most three "
"`CHECK` lines per format that was derived in this run - what to look at before the "
"text is trusted - and the `raw accept` line for the result.",
f"Length: one more `CHECK` line names every `.md` this run wrote that is longer than "
f"{LONG_TEXT_CHARS:,} characters, counted in the file as written, header included - "
"more than one ingest reads in one pass, so it is read in parts "
"(`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a "
"model's tokenizer. `raw fetch` and `raw pending` measure against the same value.",
"Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.",
),
failures=(
@@ -479,7 +503,7 @@ def raw_derive_command(
warning = _collision_warning(resolved.name)
if warning:
typer.echo("WARN " + warning)
_echo_notes([d.derivation for d in derived])
_echo_notes([d.derivation for d in derived], [(d.target, d.markdown) for d in derived])
typer.echo(_derive_next(resolved))
return
@@ -512,5 +536,5 @@ def raw_derive_command(
warning = _collision_warning(plan.stem)
if warning:
typer.echo("WARN " + warning)
_echo_notes(plan.derivations, container)
_echo_notes(plan.derivations, _written_texts(plan), container)
typer.echo(_derive_next(plan.folder))
+27 -4
View File
@@ -34,7 +34,9 @@ from chemenu.errors import ChemenuError
from chemenu.version import VersionError, read_version
from chemenu.commands.raw._common import (
_folder_collision_message,
LONG_TEXT_CHARS,
_incoming_dir,
_long_text_check,
_occupied_stems,
_resolve,
_shard_dir,
@@ -154,9 +156,19 @@ def _route_entries(route: fetch_routes.Route) -> dict:
return found
def _echo_checks(plan: sites.Plan, derivation: web_capture.Derivation, subpages: int = 0) -> None:
def _echo_checks(
plan: sites.Plan,
derivation: web_capture.Derivation,
texts: list[tuple[Path, bytes]],
subpages: int = 0,
) -> None:
"""The site handler's CHECKs for the page, then the length CHECK over
`texts` - every `.md` this run wrote, as written."""
for line in plan.checks(derivation.decoded.text, subpages):
typer.echo(f"CHECK {line}")
long_text = _long_text_check(texts)
if long_text is not None:
typer.echo(f"CHECK {long_text}")
def _fetched(plan: sites.Plan) -> str:
@@ -188,7 +200,7 @@ def _fetch_html_file(html: Path, source_url: Optional[str]) -> None:
)
_write_exclusive([(md_path, derivation.markdown)])
_warn(derivation, md_path)
_echo_checks(plan, derivation)
_echo_checks(plan, derivation, [(md_path, derivation.markdown)])
success(escape(
f"Derived {rel_path(md_path)} from {rel_path(path)} (no network access).\n"
+ _next_lines([path, md_path], source_url)
@@ -355,7 +367,7 @@ def _fetch_scope(url: str, name: Optional[str]) -> None:
for page_name, derivation in derivations:
_warn(derivation, target / f"{page_name}.md")
_echo_checks(plan, index_derivation)
_echo_checks(plan, index_derivation, [(target / f"{n}.md", d.markdown) for n, d in derivations])
success(escape(
f"Fetched {_fetched(plan)} with {len(urls)} subpage(s) to {rel_path(target)}/ "
f"({len(entries)} page pairs and {SCOPE_MANIFEST}).\n"
@@ -470,6 +482,10 @@ def _fetch_scope(url: str, name: Optional[str]) -> None:
"- the way past a paywall or a script-rendered page. Its header carries `derived` (when "
"the text was derived) instead of `retrieved`, `final_url`, `http_status` and "
"`content_type`, and the `.html` is left untouched.",
f"Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or "
f"`text/markdown` response stored as received - that is longer than "
f"{LONG_TEXT_CHARS:,} characters, the threshold of `raw derive`: more than one ingest "
"reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`).",
"Success prints the `raw accept` line for the written files and the `source_url` for the "
"source page.",
),
@@ -628,7 +644,10 @@ def raw_fetch_command(
)
_write_exclusive([(html_path, response.body), (md_path, derivation.markdown)])
_warn(derivation, md_path)
_echo_checks(plan, derivation, len(plan.subpages(derivation.decoded.text, response.final_url)))
_echo_checks(
plan, derivation, [(md_path, derivation.markdown)],
len(plan.subpages(derivation.decoded.text, response.final_url)),
)
anonymous = route.kind == fetch_routes.PLAIN and not route.render
success(escape(
f"Fetched {_fetched(plan)} to {rel_path(html_path)}, {rel_path(md_path)}\n"
@@ -640,6 +659,10 @@ def raw_fetch_command(
target = incoming / f"{stem}{web_capture.extension_for(response.content_type)}"
_refuse_existing([target])
_write_exclusive([(target, response.body)])
if kind in web_capture.PLAIN_TEXT_EXTENSIONS:
long_text = _long_text_check([(target, response.body)])
if long_text is not None:
typer.echo(f"CHECK {long_text}")
success(escape(
f"Fetched {_fetched(plan)} to {rel_path(target)} ({kind or 'no content type'}, stored as received - "
f"no text derived; retrieved {web_capture.utc_now()}).\n"
+19 -3
View File
@@ -22,6 +22,7 @@ import typer
from chemenu import cli_contract, repo_capture
from chemenu.commands._util import rel_path
from chemenu.commands.raw._common import (
LONG_TEXT_CHARS,
_Refused,
_check_files,
_derive_hint,
@@ -29,6 +30,7 @@ from chemenu.commands.raw._common import (
_plan_captured_folder,
_plan_file_moves,
_plan_folder,
_plain_text_length,
)
@@ -40,6 +42,7 @@ class _Candidate:
mtime_ns: int
reason: Optional[str] # None when `raw accept` would take it as it stands
hint: Optional[str] = None # the `raw derive` line for a lone file a filter reads
length: Optional[int] = None # characters of a lone text file over the length threshold
def label(self) -> str:
return ", ".join(rel_path(p) + ("/" if self.kind == "folder" else "") for p in self.paths)
@@ -111,6 +114,7 @@ def _pending_candidates() -> list[_Candidate]:
max(p.lstat().st_mtime_ns for p in paths),
_refusal(_plain_files, paths),
_derive_hint(paths[0]) if len(paths) == 1 else None,
_plain_text_length(paths[0]) if len(paths) == 1 else None,
))
candidates.sort(key=lambda c: (c.mtime_ns, os.fsencode(c.paths[0].name)))
return candidates
@@ -142,13 +146,19 @@ def _pending_candidates() -> list[_Candidate]:
"human.",
"The default is the first candidate `raw accept` would take, and the output names it.",
"`--json` prints the same candidates in the same order: `kind`, `paths`, `files`, "
"`mtime`, `acceptable`, `reason`, `default`, `hint`.",
"`mtime`, `acceptable`, `reason`, `default`, `hint`, `length`.",
"A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is "
"listed with the `raw derive` line for it, in text output and as `hint` in JSON "
"(`null` for every other candidate). The hint changes nothing about the order or the "
"default.",
"Reads directory listings and `lstat` only, never a file's content; an empty "
"`incoming/` is exit 0 with nothing to do.",
"A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw "
"fetch` is listed with its length in characters, in text output and as `length` in "
"JSON (`null` for every other candidate) - that text reaches a session with no "
"derivation in front of it to print the length `CHECK`. Like the hint, it changes "
"nothing about the order or the default.",
"Reads directory listings and `lstat`, and a file's content only to measure a lone "
"`.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more "
"characters than bytes; an empty `incoming/` is exit 0 with nothing to do.",
),
failures=(),
examples=(
@@ -180,6 +190,7 @@ def raw_pending_command(
"reason": c.reason,
"default": c is default,
"hint": c.hint,
"length": c.length,
}
for c in candidates
], indent=2, ensure_ascii=False))
@@ -199,6 +210,11 @@ def raw_pending_command(
typer.echo(f" {line}")
if c.hint is not None:
typer.echo(f" text: {c.hint} (derives readable text, keeps the original)")
if c.length is not None:
typer.echo(
f" long: {c.length:,} characters, over the {LONG_TEXT_CHARS:,} one ingest "
"reads in one pass - read it in parts (instructions/ingest-large-tree.md)"
)
if default is None:
typer.echo("No candidate can be accepted as it stands - each needs a human (reasons above).")
return
+66
View File
@@ -20,6 +20,7 @@ import typer
import yaml
from chemenu import filters, web_capture
from chemenu.commands.raw._common import LONG_TEXT_CHARS
from chemenu.commands.raw.accept import raw_accept_command
from chemenu.commands.raw.derive import raw_derive_command
from chemenu.commands.raw.pending import raw_pending_command
@@ -883,6 +884,71 @@ def test_header_renders_a_list_as_a_flow_sequence_and_a_string_as_before():
assert yaml.safe_load(rendered.strip("-\n").replace("---", "")) == {"a": "plain", "b": ["x y", "z"], "c": []}
# --- the length axis (Gitea #226) -------------------------------------------
def _long_checks(out: str) -> list[str]:
return [line for line in out.splitlines() if line.startswith("CHECK Longer than one ingest")]
def test_the_length_check_fires_one_character_past_the_constant_and_not_at_it(tree, capsys):
# one paragraph, the same file-name length: every header is as long as the probe's
_put(tree, "a.docx", make_docx(_p("x")))
_derive("incoming/a.docx")
overhead = len((tree / "incoming/a/derived.md").read_text(encoding="utf-8")) - 1
capsys.readouterr()
_put(tree, "b.docx", make_docx(_p("x" * (LONG_TEXT_CHARS - overhead))))
_derive("incoming/b.docx")
assert len((tree / "incoming/b/derived.md").read_text(encoding="utf-8")) == LONG_TEXT_CHARS
assert _long_checks(capsys.readouterr().out) == []
_put(tree, "c.docx", make_docx(_p("x" * (LONG_TEXT_CHARS - overhead + 1))))
_derive("incoming/c.docx")
[line] = _long_checks(capsys.readouterr().out)
assert f"incoming/c/derived.md ({LONG_TEXT_CHARS + 1:,} characters)" in line
assert "instructions/ingest-large-tree.md" in line
def test_the_length_is_counted_in_characters_not_in_bytes(tree, capsys):
_put(tree, "umlaute.docx", make_docx(_p("ä" * (LONG_TEXT_CHARS // 2 + 1000))))
_derive("incoming/umlaute.docx")
assert (tree / "incoming/umlaute/derived.md").stat().st_size > LONG_TEXT_CHARS
assert _long_checks(capsys.readouterr().out) == []
def test_the_length_check_names_each_long_md_of_a_folder_and_of_a_zip(tree, capsys):
folder = tree / "incoming/mappe"
folder.mkdir()
(folder / "lang.docx").write_bytes(make_docx(_p("x" * (LONG_TEXT_CHARS + 1))))
(folder / "kurz.docx").write_bytes(make_docx(_p("x")))
_derive("incoming/mappe")
[line] = _long_checks(capsys.readouterr().out)
assert "incoming/mappe/lang.md (" in line and "kurz.md" not in line
_put(tree, "paket.zip", _zip({"lang.docx": make_docx(_p("x" * (LONG_TEXT_CHARS + 1)))}))
_derive("incoming/paket.zip")
[line] = _long_checks(capsys.readouterr().out)
assert "incoming/paket/lang.md (" in line
def test_pending_names_the_length_of_a_long_lone_text_file_and_of_nothing_else(tree, capsys):
_put(tree, "lang.md", b"x" * (LONG_TEXT_CHARS + 1))
_put(tree, "grenze.txt", b"x" * LONG_TEXT_CHARS)
_put(tree, "umlaute.md", ("ä" * (LONG_TEXT_CHARS // 2 + 1000)).encode("utf-8"))
_put(tree, "page.html", b"x" * (LONG_TEXT_CHARS + 1))
_put(tree, "page.md", b"x" * (LONG_TEXT_CHARS + 1))
raw_pending_command(json_out=True)
by_path = {c["paths"][0]: c for c in json.loads(capsys.readouterr().out)}
assert by_path["incoming/lang.md"]["length"] == LONG_TEXT_CHARS + 1
assert by_path["incoming/grenze.txt"]["length"] is None
assert by_path["incoming/umlaute.md"]["length"] is None
assert by_path["incoming/page.html"]["length"] is None # a fetch pair: raw fetch printed it
raw_pending_command(json_out=False)
assert f"long: {LONG_TEXT_CHARS + 1:,} characters" in capsys.readouterr().out
# --- the nets in raw pending and raw accept ---------------------------------
+22
View File
@@ -20,6 +20,7 @@ import typer
import yaml
from chemenu import web_capture
from chemenu.commands.raw._common import LONG_TEXT_CHARS
from chemenu.commands.raw.accept import raw_accept_command
from chemenu.commands.raw.fetch import raw_fetch_command
@@ -330,6 +331,27 @@ def test_plain_text_is_stored_as_txt_unchanged(tree, site):
assert (tree / "incoming/notes.txt").read_bytes() == b"line one\r\nline two\n"
def _long_checks(out: str) -> list[str]:
return [line for line in out.splitlines() if line.startswith("CHECK Longer than one ingest")]
def test_a_derived_page_over_the_length_threshold_gets_the_length_check(tree, site, capsys):
_fetch(site.html("/short", ARTICLE))
assert _long_checks(capsys.readouterr().out) == []
long_page = b"<html><body><main><p>" + b"x" * (LONG_TEXT_CHARS + 1) + b"</p></main></body></html>"
_fetch(site.html("/long", long_page))
[line] = _long_checks(capsys.readouterr().out)
assert "incoming/long.md (" in line and "instructions/ingest-large-tree.md" in line
def test_plain_text_over_the_length_threshold_gets_the_length_check(tree, site, capsys):
site.routes["/notes"] = (200, {"Content-Type": "text/plain; charset=utf-8"}, b"x" * (LONG_TEXT_CHARS + 1))
_fetch(site.url("/notes"))
[line] = _long_checks(capsys.readouterr().out)
assert f"incoming/notes.txt ({LONG_TEXT_CHARS + 1:,} characters)" in line
# --- --html: a page saved from a browser ------------------------------------