diff --git a/codex_procedures/content.deliver/PROMPT.md b/codex_procedures/content.deliver/PROMPT.md index 376d441a..13c952ba 100644 --- a/codex_procedures/content.deliver/PROMPT.md +++ b/codex_procedures/content.deliver/PROMPT.md @@ -8,7 +8,7 @@ Answer pages and public articles carry their search listing apart from the copy `page_metadata` (`meta_title`, `meta_description`, and the approved `slug` when the draft chose one). Keep the Markdown in Tin unchanged. Inspect this repository's instructions and existing pages, and prepare one coherent site -change (at most 10 files) as an unmerged PR. +change (at most 30 files) as an unmerged PR. Build the page the way this site builds its pages: a Markdown or MDX file, a component, a typed page registry, plain HTML or whatever else the repository uses. Convert the format @@ -24,6 +24,20 @@ back into the site (for example to the product), byline and dates, and structure JSON-LD) that they carry. Use only the fonts and files the site already loads; do not add web fonts or other outside resources. +The approved page may carry figures and interactive pieces, listed in `assets` with each +file's project path, and diagram, video and callout blocks in `article`. Your project-state +checkout is at the approved revision: copy each asset from `/home/user/state/` into the +place this site keeps such files, and show it the way the site shows media. An SVG becomes an +image or the site's figure component, with its alt text and caption. An interactive piece +becomes an iframe or an inline component, with its script and data unchanged. Turn each +`mermaid` block into SVG with +`node /opt/tin-lite/diagram/scripts/check_diagram.mjs check .mmd --out --no-previews`, +which writes `light.svg` and `dark.svg`; use the site's own diagram support instead when it +has one. Show each `tin-video` block with the site's video component or the provider's embed +code, linking a video file rather than copying it, and each `> [!NOTE]`-style callout with +the site's own note style, or a plain aside. Every approved asset, diagram and video has to +appear on the page. + If the site does not serve the page's route yet, add the smallest route that fits how its other pages are served. Change what the page needs (the page, its route, and any index or sitemap the site keeps) and nothing else. diff --git a/codex_procedures/content.deliver/skills/article-delivery/SKILL.md b/codex_procedures/content.deliver/skills/article-delivery/SKILL.md index 46a4cb75..e8182dbf 100644 --- a/codex_procedures/content.deliver/skills/article-delivery/SKILL.md +++ b/codex_procedures/content.deliver/skills/article-delivery/SKILL.md @@ -29,7 +29,8 @@ description: Put one approved article, answer page or public article on an exist the site already loads; never add web fonts or other outside resources. 5. If the site does not serve the page's route yet, add the smallest route that fits how its other pages are served (a static file in its public folder, a route file, a registry - entry), within the 10-file limit. Prefer tools the site already has; add a dependency + entry), within the 30-file limit. Place the page's figures, embeds, diagrams and videos as + the prompt describes. Prefer tools the site already has; add a dependency only when the page cannot be built without one, and say why in the PR body. 6. Add only necessary page/index metadata, following existing dates, slugs, canonical links and components. Do not invent author identity, product claims or new sales copy. diff --git a/codex_procedures/website.change/PROMPT.md b/codex_procedures/website.change/PROMPT.md index 7d8ec85a..870239b0 100644 --- a/codex_procedures/website.change/PROMPT.md +++ b/codex_procedures/website.change/PROMPT.md @@ -38,6 +38,20 @@ back into the site (for example to the product), byline and dates, and structure JSON-LD) that they carry. Use only the fonts and files the site already loads; do not add web fonts or other outside resources. +The approved page may carry figures and interactive pieces, listed in `assets` with each +file's project path, and diagram, video and callout blocks in `article`. Your project-state +checkout is at the approved revision: copy each asset from `/home/user/state/` into the +place this site keeps such files, and show it the way the site shows media. An SVG becomes an +image or the site's figure component, with its alt text and caption. An interactive piece +becomes an iframe or an inline component, with its script and data unchanged. Turn each +`mermaid` block into SVG with +`node /opt/tin-lite/diagram/scripts/check_diagram.mjs check .mmd --out --no-previews`, +which writes `light.svg` and `dark.svg`; use the site's own diagram support instead when it +has one. Show each `tin-video` block with the site's video component or the provider's embed +code, linking a video file rather than copying it, and each `> [!NOTE]`-style callout with +the site's own note style, or a plain aside. Every approved asset, diagram and video has to +appear on the page. + When `route` is set, the founder chose where these pages live: publish the page at exactly that route, wherever the site keeps such pages. Never put a page under `content/answers/`, which is Tin's draft folder and not a page on the site. diff --git a/codex_procedures/website.change/skills/site-change/SKILL.md b/codex_procedures/website.change/skills/site-change/SKILL.md index 77e2e1e3..ca8ec2d9 100644 --- a/codex_procedures/website.change/skills/site-change/SKILL.md +++ b/codex_procedures/website.change/skills/site-change/SKILL.md @@ -37,7 +37,8 @@ instead: the change is the audit's technical fixes or planned URL changes, not a 5. If the site does not serve the chosen path yet, add the smallest route that fits how its other pages are served (a static file in its public folder, a route file, a registry entry), keeping its files inside the route's own folder where the framework allows, so - Tin can merge it as part of the page. Stay within the 20-file limit. + Tin can merge it as part of the page. Stay within the 20-file limit. Place the page's + figures, embeds, diagrams and videos as the prompt describes. 6. Stay clear of protected paths: never change a file that serves a path in `protected_paths` (the shared sign-in, sign-up and auth-return pages, plus any the founder listed). Leave root layouts, middleware, redirects, host and build settings and diff --git a/docs/content-delivery-implementation.md b/docs/content-delivery-implementation.md index 5437d9e0..2c9e297a 100644 --- a/docs/content-delivery-implementation.md +++ b/docs/content-delivery-implementation.md @@ -59,3 +59,19 @@ Record exactly which deployment and tests passed below before claiming completio packaged application with synthetic HTTP; the signed-in in-app browser was unavailable. - Live article-PR acceptance remains pending a suitable explicit repository/content path. Never assume the connected repository hosts a particular website; verify ownership and paths. + +## Page bundles + +An approved article may carry figures, interactive pieces, diagrams, videos and callouts +([review](content-review.md)). content.deliver 1.7.0 and website.change 1.6.0 put them on the +site in its own form: + +- The approved source lists each asset's project path and checksum; a delivery run with assets + checks out the approved revision (`procedure_project_revision`) and copies the files from + `/home/user/state`. Mermaid blocks become SVG with the sandbox's offline renderer + (`check_diagram.mjs check … --no-previews`), unless the site has its own diagram support. +- `figure_check` sits beside `copy_check`: each approved file must arrive byte for byte (same + checksum), each video by its address or ID, each diagram as a new SVG or the site's Mermaid + support. `not_confirmed` only holds Tin's own merge; the pull request still opens. +- One pull request or commit may hold up to 30 files and 2 MB (`PULL_REQUEST_MAX_*`), and a + saved patch is read back up to 4 MB. Procedures that declare nothing keep 512 KB. diff --git a/src/tin_lite/activities.py b/src/tin_lite/activities.py index 7a247130..1b428510 100644 --- a/src/tin_lite/activities.py +++ b/src/tin_lite/activities.py @@ -228,6 +228,18 @@ def transient_failure(message: str | None, *, restarted: bool = False) -> bool: return _TRANSIENT_FAILURE_PATTERN.search(message) is not None +def procedure_project_revision(run, procedure, content_source): + """The project revision a procedure's checkout pins, or None for the canonical head.""" + if procedure.output_validator in REVIEWED_DIAGRAM_VALIDATORS or ( + procedure.review_revision_context or {} + ).get("project_revision"): + return run.expected_head_sha + if content_source is not None and content_source.get("assets"): + # A page's figures and embeds are read where they were approved. + return content_source["source_revision"] + return None + + class TinActivities: async def _run_accounted_procedure(self, *, conn, run, sandbox_id, run_input): if ( @@ -3504,12 +3516,12 @@ async def retain_interrupted(content=None): f"{self._settings.switchboard_public_url.rstrip('/')}" "/internal/run-tools/mcp" ) + content_source = None if procedure.result_kind == GITHUB_PULL_REQUEST_RESULT: technical = None expected_binding = None from tin_lite import content_repository_delivery - content_source = None if content_repository_delivery.adapts(run): content_source = await content_repository_delivery.saved_source( self._db, run_id @@ -3627,15 +3639,8 @@ async def retain_interrupted(content=None): ), # Card runs keep no agent narration, as they keep no rollouts. progress_sink=None if payment_card else self._progress_sink(run_id), - project_revision=( - run.expected_head_sha - if ( - procedure.output_validator in REVIEWED_DIAGRAM_VALIDATORS - or (procedure.review_revision_context or {}).get( - "project_revision" - ) - ) - else None + project_revision=procedure_project_revision( + run, procedure, content_source ), result_kind=procedure.result_kind, run_tools_url=run_tools_url, diff --git a/src/tin_lite/catalog.py b/src/tin_lite/catalog.py index 124b6656..9feae737 100644 --- a/src/tin_lite/catalog.py +++ b/src/tin_lite/catalog.py @@ -636,7 +636,7 @@ def definition_and_files_with_wiki( "wording, and the PR adds nothing but the page, or the page at the route you chose " "for such pages.", executor=CODEX_PROCEDURE_EXECUTOR, - version_label="1.6.0", + version_label="1.7.0", system=ORGANIC_TRAFFIC_SYSTEM, schedule_modes=("on_demand",), input_schema={ @@ -697,10 +697,10 @@ def definition_and_files_with_wiki( github_pull_request=GitHubPullRequestProcedure( receipt_path_template="content/deliveries/{run_id}.md", verification_commands=(content_repository_delivery.CHECK_COMMAND,), - # The procedure ceilings, as runaway guards: a page in any format, its route, + # Runaway guards: a page in any format with its figures and embeds, its route, # index and sitemap. - max_files=10, - max_bytes=512_000, + max_files=30, + max_bytes=2_000_000, ), ), ), @@ -720,7 +720,7 @@ def definition_and_files_with_wiki( "protected pages, opens a pull request for you to merge. Deleting a page stays with " "you.", executor=CODEX_PROCEDURE_EXECUTOR, - version_label="1.5.0", + version_label="1.6.0", system=ORGANIC_TRAFFIC_SYSTEM, schedule_modes=("on_demand",), # Agents start it for an approved change; the catalog has no picker for change rows. @@ -833,8 +833,10 @@ def definition_and_files_with_wiki( github_pull_request=GitHubPullRequestProcedure( receipt_path_template="website/changes/{run_id}.md", verification_commands=(content_repository_delivery.CHECK_COMMAND,), - # site-fix-v5's file cap, for an audit run and a page alike. + # site-fix-v5's file cap, for an audit run and a page alike; a page with its + # figures and embeds needs more bytes than a technical batch. max_files=technical_fix.POLICY_MAX_FILES[technical_fix.BATCH_POLICY], + max_bytes=2_000_000, site_repair_policy=technical_fix.BATCH_POLICY, allow_no_change=True, ), diff --git a/src/tin_lite/code_storage.py b/src/tin_lite/code_storage.py index e4a597a8..a356c2d3 100644 --- a/src/tin_lite/code_storage.py +++ b/src/tin_lite/code_storage.py @@ -37,6 +37,8 @@ # Text outputs stay under the comparison limit; a declared binary output (the demo video) is # only ever read whole, never diffed. _PUBLICATION_TEXT_MAX_BYTES = 1_000_000 +# A pull-request patch saved as JSON may reach twice its procedure's 2 MB ceiling. +_CHECKPOINT_TEXT_MAX_BYTES = 4_000_000 _PUBLICATION_BINARY_MAX_BYTES = 16_000_000 _BINARY_MEDIA_TYPES = frozenset({"video/mp4"}) # Reads addressed by a full commit SHA are immutable, so one process keeps a bounded copy. @@ -513,7 +515,7 @@ async def read_procedure_checkpoint( ) -> bytes: if not _is_commit_sha(revision) or not _safe_repo_path(path): raise ValueError("procedure checkpoint requires an immutable revision and safe path") - max_bytes = _PUBLICATION_BINARY_MAX_BYTES if binary else _PUBLICATION_TEXT_MAX_BYTES + max_bytes = _PUBLICATION_BINARY_MAX_BYTES if binary else _CHECKPOINT_TEXT_MAX_BYTES key = ("checkpoint", repo_id, revision, path, max_bytes) if (cached := self._pinned.get(key)) is not None: return cached diff --git a/src/tin_lite/content_repository_delivery.py b/src/tin_lite/content_repository_delivery.py index 27c3c16d..4fedc6aa 100644 --- a/src/tin_lite/content_repository_delivery.py +++ b/src/tin_lite/content_repository_delivery.py @@ -732,7 +732,7 @@ def merge_rule(manifest, proof, route): """ from tin_lite.page_routes import matches - if proof.get("copy_check") == "not_confirmed": + if "not_confirmed" in {proof.get("copy_check"), proof.get("figure_check")}: return None if page_only(manifest, proof): return "page_only" @@ -823,6 +823,13 @@ async def publish_after_pull_request( "reason": "Tin couldn't confirm the page keeps the approved wording word for " "word, so it waits for your review.", } + elif proof.get("figure_check") == "not_confirmed": + result = { + **base, + "status": "left_open", + "reason": "Tin couldn't confirm every approved figure, interactive piece, " + "diagram and video is on the page, so it waits for your review.", + } elif rule is None: result = { **base, @@ -1091,15 +1098,67 @@ def validate_copy(manifest, source): from tin_lite.page_urls import public_route route = public_route(manifest.get("body")) + figures = figure_check(manifest, source) return { "article_path": path, "article_sha256": source["article_sha256"], "copy_check": check, + **({"figure_check": figures} if figures else {}), "build_check": "not_verified_by_tin", **({"public_route": route} if route else {}), } +VIDEO_BLOCK = re.compile(r"^(`{3,}|~{3,})[ \t]*tin-video\b(.*?)^\1[ \t]*$", re.M | re.S) +MERMAID_BLOCK = re.compile(r"^(?:`{3,}|~{3,})[ \t]*mermaid\b", re.M) + + +def figure_check(manifest, source): + """Whether every approved figure, embed, diagram and video is on the page, or None. + + An approved file must arrive byte for byte (the same checksum), a video by its address or + its last path segment (the provider's ID), and each diagram as an SVG or the site's own + Mermaid support. Like copy_check this decides only Tin's own merge, never the PR. + """ + article = source["article"] + assets = source.get("assets") or [] + videos = [ + match.group(1).strip() + for block in VIDEO_BLOCK.finditer(article) + if (match := re.search(r"(?m)^\s*url\s*:\s*(\S+)", block.group(2))) + ] + diagrams = len(MERMAID_BLOCK.findall(article)) + if not (assets or videos or diagrams): + return None + files = manifest.get("files") or [] + digests = {hashlib.sha256(item["content"].encode()).hexdigest() for item in files} + text = "\n".join(item["content"] for item in files) + # Diagrams arrive as new SVG files (not the approved figures' copies) or inline SVG. + approved = {asset["sha256"] for asset in assets} + drawn = sum( + (hashlib.sha256(item["content"].encode()).hexdigest() not in approved) + if item["path"].endswith(".svg") + else item["content"].count("= diagrams or "mermaid" in text.casefold()) + ) + return "confirmed" if confirmed else "not_confirmed" + + +def _video_shown(url, text): + from urllib.parse import parse_qs, urlsplit + + if url in text: + return True + parts = urlsplit(url) + ids = [parts.path.rstrip("/").rsplit("/", 1)[-1], *parse_qs(parts.query).get("v", [])] + return any(len(value) >= 4 and value in text for value in ids) + + FIGURE_FENCE = re.compile( r"^(`{3,}|~{3,})[ \t]*(?:tin-embed|tin-video|mermaid)\b.*?^\1[ \t]*$", re.M | re.S ) diff --git a/src/tin_lite/integrations.py b/src/tin_lite/integrations.py index f382de95..00f40ace 100644 --- a/src/tin_lite/integrations.py +++ b/src/tin_lite/integrations.py @@ -165,6 +165,12 @@ GSC_MIN_ROW_BYTES = 70 +# Runaway guards for one pull request or commit Tin writes: a technical batch (20 files) or a +# page with its figures, embeds, route and sitemap. Each procedure declares its own, smaller limit. +PULL_REQUEST_MAX_FILES = 30 +PULL_REQUEST_MAX_BYTES = 2_000_000 + + class IntegrationError(RuntimeError): """A safe integration failure that may be shown to a project member.""" @@ -2081,8 +2087,9 @@ async def github_merge_pull_request( raise IntegrationError("GitHub pull request to merge is invalid") if not commit_title.strip() or len(commit_title) > 200 or not _safe_github_ref(branch): raise IntegrationError("GitHub merge request is invalid") - # Up to 20: a website.change technical batch (site-fix-v5's file cap). - if not 1 <= len(files) <= 20 or not all(_safe_github_path(item.path) for item in files): + if not 1 <= len(files) <= PULL_REQUEST_MAX_FILES or not all( + _safe_github_path(item.path) for item in files + ): raise IntegrationError("GitHub merge must name the pull request's files") def request_fingerprint(repository): @@ -3276,10 +3283,12 @@ async def github_create_pull_request( raise IntegrationError("GitHub execution key is invalid") if not title.strip() or len(title) > 200 or len(body) > 20_000: raise IntegrationError("GitHub pull-request title or body is invalid") - if not 1 <= len(files) <= 10: - raise IntegrationError("GitHub pull requests must contain between 1 and 10 files") - if sum(len(item.content.encode()) for item in files) > 512_000: - raise IntegrationError("GitHub pull-request files exceed the 512 KB limit") + if not 1 <= len(files) <= PULL_REQUEST_MAX_FILES: + raise IntegrationError( + f"GitHub pull requests must contain between 1 and {PULL_REQUEST_MAX_FILES} files" + ) + if sum(len(item.content.encode()) for item in files) > PULL_REQUEST_MAX_BYTES: + raise IntegrationError("GitHub pull-request files exceed the 2 MB limit") for item in files: if not _safe_github_path(item.path): raise IntegrationError("GitHub pull request contains an unsafe file path") @@ -3466,10 +3475,12 @@ async def github_commit_files( raise IntegrationError("GitHub execution key is invalid") if not message.strip() or len(message) > 200: raise IntegrationError("GitHub commit message is invalid") - if not 1 <= len(files) <= 10: - raise IntegrationError("GitHub commits must contain between 1 and 10 files") - if sum(len(item.content.encode()) for item in files) > 512_000: - raise IntegrationError("GitHub commit files exceed the 512 KB limit") + if not 1 <= len(files) <= PULL_REQUEST_MAX_FILES: + raise IntegrationError( + f"GitHub commits must contain between 1 and {PULL_REQUEST_MAX_FILES} files" + ) + if sum(len(item.content.encode()) for item in files) > PULL_REQUEST_MAX_BYTES: + raise IntegrationError("GitHub commit files exceed the 2 MB limit") for item in files: if not _safe_github_path(item.path): raise IntegrationError("GitHub commit contains an unsafe file path") diff --git a/src/tin_lite/procedures.py b/src/tin_lite/procedures.py index dadfe4c6..17189bc5 100644 --- a/src/tin_lite/procedures.py +++ b/src/tin_lite/procedures.py @@ -55,8 +55,10 @@ DEMO_VIDEO_MEDIA_TYPE, } ) -MAX_PROCEDURE_PULL_REQUEST_BYTES = 512_000 -MAX_PROCEDURE_PULL_REQUEST_FILES = 10 +# Ceilings a procedure may declare; the default stays 512 KB so existing contracts don't move. +MAX_PROCEDURE_PULL_REQUEST_BYTES = 2_000_000 +MAX_PROCEDURE_PULL_REQUEST_FILES = 30 +DEFAULT_PULL_REQUEST_BYTES = 512_000 PROJECT_ARTIFACT_RESULT = "project.artifact" GITHUB_PULL_REQUEST_RESULT = "github.pull_request" PROJECT_STATE_WORKSPACE = "project.state" @@ -294,7 +296,7 @@ class GitHubPullRequestProcedure: receipt_path_template: str verification_commands: tuple[str, ...] max_files: int = 3 - max_bytes: int = MAX_PROCEDURE_PULL_REQUEST_BYTES + max_bytes: int = DEFAULT_PULL_REQUEST_BYTES provider_key: str = "infra.github" repair_policy: str | None = None allow_no_change: bool = False @@ -926,6 +928,8 @@ def validate_codex_procedure_definition(definition: dict[str, Any]) -> CodexProc MAX_PROCEDURE_BINARY_ARTIFACT_BYTES if result_kind == PROJECT_ARTIFACT_RESULT and declared_media_type in BINARY_ARTIFACT_MEDIA_TYPES + else MAX_PROCEDURE_PULL_REQUEST_BYTES + if result_kind == GITHUB_PULL_REQUEST_RESULT else MAX_PROCEDURE_ARTIFACT_BYTES ) if ( diff --git a/tests/test_page_assets.py b/tests/test_page_assets.py index fbf2614c..7e4bda4d 100644 --- a/tests/test_page_assets.py +++ b/tests/test_page_assets.py @@ -427,3 +427,93 @@ async def test_the_review_shows_the_bundle_and_says_what_it_carries(publication_ assert f'class="md-embed" data-asset="{folder}/field.html"' in document["html"] # Files shows the same article with the files it refers to that exist there. assert in_files["assets"] == kept + + +def bundle_source(article, assets=()): + from tin_lite.content_repository_delivery import validate_copy # noqa: F401 + + return { + "article": article, + "article_sha256": hashlib.sha256(article.encode()).hexdigest(), + "binding": {"repository": "owner/site", "head_sha": "9" * 40}, + "assets": [ + {"path": path, "sha256": hashlib.sha256(raw).hexdigest()} for path, raw in assets + ], + } + + +def test_delivery_confirms_every_figure_embed_diagram_and_video_arrived(): + from tin_lite.content_repository_delivery import figure_check, merge_rule, validate_copy + + article = ( + "# Filming the trailers\n\nEvery shot follows recorded game events in order.\n\n" + "![Flow](./a.assets/flow.svg)\n\n```tin-embed\nsrc: ./a.assets/field.html\n```\n\n" + "```mermaid\ngraph LR\nA-->B\n```\n\n" + "```tin-video\nurl: https://www.youtube.com/watch?v=dQw4w9WgXcQ\ntitle: Trailer\n```\n" + ) + source = bundle_source( + article, [("content/a.assets/flow.svg", GOOD_SVG), ("content/a.assets/field.html", EMBED)] + ) + page = ( + "

Filming the trailers

Every shot follows recorded game events in order.

" + '' + '' + ) + manifest = { + "repository": "owner/site", + "head_sha": "9" * 40, + "body": "Public URL: https://example.com/blog/filming", + "files": [ + {"path": "public/blog/filming/index.html", "content": page}, + {"path": "public/blog/filming/flow.svg", "content": GOOD_SVG.decode()}, + {"path": "public/blog/filming/field.html", "content": EMBED.decode()}, + {"path": "public/blog/filming/diagram-1.svg", "content": ""}, + ], + } + proof = validate_copy(manifest, source) + assert proof["copy_check"] == "wording_preserved" and proof["figure_check"] == "confirmed" + assert merge_rule(manifest, proof, "/blog/{slug}") == "chosen_route" + # A missing embed, a changed figure, a missing video or diagram: the merge waits. + for drop in ("field.html", "flow.svg", "youtube", "diagram-1.svg"): + broken = { + **manifest, + "files": [ + {**item, "content": item["content"].replace("youtube-nocookie", "vimeo")} + if drop == "youtube" + else item + for item in manifest["files"] + if drop == "youtube" or not item["path"].endswith(drop) + ], + } + if drop == "youtube": + broken["files"][0]["content"] = broken["files"][0]["content"].replace( + "dQw4w9WgXcQ", "other" + ) + proof = validate_copy(broken, source) + assert proof["figure_check"] == "not_confirmed", drop + assert merge_rule(broken, proof, "/blog/{slug}") is None + # A page without figures has no figure check at all. + assert figure_check(manifest, bundle_source("# Plain\n\nWords.\n")) is None + + +def test_a_delivery_with_assets_reads_them_where_they_were_approved(): + from tin_lite.activities import procedure_project_revision + + run = SimpleNamespace(expected_head_sha="a" * 40) + procedure = SimpleNamespace(output_validator=None, review_revision_context=None) + with_assets = {"source_revision": "b" * 40, "assets": [{"path": "x", "sha256": "y"}]} + assert procedure_project_revision(run, procedure, with_assets) == "b" * 40 + assert procedure_project_revision(run, procedure, {"source_revision": "b" * 40}) is None + assert procedure_project_revision(run, procedure, None) is None + + +def test_github_writes_fit_a_page_with_its_figures(): + from tin_lite.integrations import PULL_REQUEST_MAX_BYTES, PULL_REQUEST_MAX_FILES + from tin_lite.procedures import DEFAULT_PULL_REQUEST_BYTES, GitHubPullRequestProcedure + + assert (PULL_REQUEST_MAX_FILES, PULL_REQUEST_MAX_BYTES) == (30, 2_000_000) + # Procedures that declare nothing keep their existing 512 KB contract. + assert GitHubPullRequestProcedure("r/{run_id}.md", ()).max_bytes == DEFAULT_PULL_REQUEST_BYTES + deliver = next(w for w in BUILTIN_WORKFLOWS if w.key == "content.deliver") + output = deliver.definition_and_resource_files()[0]["procedure"]["output"] + assert (output["max_files"], output["max_bytes"]) == (30, 2_000_000) diff --git a/tests/test_website_change.py b/tests/test_website_change.py index ffc82352..bd878ab9 100644 --- a/tests/test_website_change.py +++ b/tests/test_website_change.py @@ -662,10 +662,11 @@ async def test_content_deliver_pinned_runs_are_unchanged(publication_db, monkeyp digest.update(files[path]) # The definition and procedure files new runs pin: 1.4.0 builds the page in the site's own # format (1.3.0 kept it as Markdown or a JSON string); 1.5.0 matches the site's other pages - # and adds no outside fonts; 1.6.0 uses the slug the founder approved with the draft. - assert spec.version_label == "1.6.0" + # and adds no outside fonts; 1.6.0 uses the slug the founder approved with the draft; + # 1.7.0 carries the page's figures, embeds, diagrams and videos (30 files, 2 MB). + assert spec.version_label == "1.7.0" assert digest.hexdigest() == ( - "3fae795a20f1259ec130a337ff23a8369f606d7606574a2e4b8b5a53b7f3f86c" + "4134b22ca674cbc7599cab5b4a861317a25faabe47f9aad1c16c21dd10d8f7c9" ) # An approval-started content.deliver run pins no change row and keeps its own rules: # a pull-request setting never merges, and nothing records a merge for it.