From 7f6082f558123cbd780602a753f40e91eb64828d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:40:32 +0000 Subject: [PATCH 1/4] test: gate the webapp overlay against the vendored core it is copied from The published app ships the same 61 webapp files twice - the vendored core's app/z2ui5/webapp and the overlay assemble-cap.js copies to app/z2ui5/webapp so cds watch, the approuter and ui5 build find a real CAP app folder. The copy is deliberate (a symlink survives neither a Windows checkout nor an mbt archive); what was missing is the gate that keeps 'twice' from becoming 'two versions' - an overlay edited in place, or left stale by a partial run, drifts silently between what the browser gets and what the framework serves. src/test/webapp-overlay.test.js proves the two trees byte-identical. It ships with the app, so it runs in the assemble's publish gate AND in the published repository's own CI; in this builder checkout, where no overlay exists yet, the byte half skips and only the source side is asserted. Green: 10 suites, 53 tests over the assembled app. mirror-core.js additionally names its twin: the honor-the-pin-only- when-newer arbitration exists a second time in builder-cap2UI5-web's mirror.mjs, and each header now points at the other. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01BuzK8EC2CdQ5PJ6YCdfKJc --- scripts/mirror-core.js | 5 +++ src/test/webapp-overlay.test.js | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/test/webapp-overlay.test.js diff --git a/scripts/mirror-core.js b/scripts/mirror-core.js index 4ec4f11..b2d4715 100644 --- a/scripts/mirror-core.js +++ b/scripts/mirror-core.js @@ -20,6 +20,11 @@ * must not pin the nightly to an old core, so the newer of the two commits * wins (committer date; equal-or-older slot ⇒ HEAD). If the slot sha cannot * be fetched at all, the mirror falls back to HEAD with a warning. + * + * The same arbitration exists a second time in builder-cap2UI5-web's + * mirror.mjs (its trigger slot points at cap2UI5) — two implementations of + * one algorithm, kept in step by hand; whoever changes the rule here + * carries it there too. */ "use strict"; diff --git a/src/test/webapp-overlay.test.js b/src/test/webapp-overlay.test.js new file mode 100644 index 0000000..4961981 --- /dev/null +++ b/src/test/webapp-overlay.test.js @@ -0,0 +1,77 @@ +// The same 61 webapp files exist twice in the published app, on purpose — +// and this is the gate that keeps "twice" from becoming "two versions". +// +// WHY THE COPY EXISTS +// ------------------- +// The UI5 frontend ships inside the vendored core package +// (core/app/z2ui5/webapp — mirrored 1:1 from upstream abap2UI5), and +// assemble-cap.js overlays it into app/z2ui5/webapp so the tree is a real +// CAP app folder: `cds watch` serves it, the approuter routes to it, and +// `ui5 build` in an MTA pipeline finds it where every UI5 toolchain looks. +// A symlink would not survive a Windows checkout or an mbt archive, and +// serving core/ directly would put deployment paths (xs-app.json, ui5.yaml) +// inside a vendored package nobody may edit. So: a copy, made by the +// assemble step, never by hand. +// +// WHY THE GATE EXISTS +// ------------------- +// Nothing checked the copy. The overlay is only correct as long as every +// publish reruns the assemble; an overlay edited in place (the obvious place +// to "fix the frontend") or a stale overlay after a partial run is byte-drift +// between what the browser gets (app/) and what the framework serves its +// embedded assets from (core/) — the split-brain that is invisible until a +// user reports it. This test makes the drift a failing suite instead: it +// runs in the assembled app (the publish gate) and again in the published +// repository's own CI, so both sides prove the two trees are byte-identical. +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.join(__dirname, ".."); + +// The vendored core is wherever package.json says it is - "file:./core" in +// the assembled/published app, the builder's input mirror here in src (the +// path is deliberately not spelled out: assemble validates that no output +// file references the builder-side location). +const spec = require(path.join(ROOT, "package.json")).dependencies.abap2UI5; +const CORE = path.resolve(ROOT, spec.replace(/^file:/, "")); + +const OVERLAY = path.join(ROOT, "app", "z2ui5", "webapp"); +const SOURCE = path.join(CORE, "app", "z2ui5", "webapp"); + +const walk = (dir, base = dir) => { + const out = []; + for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const p = path.join(dir, e.name); + if (e.isDirectory()) out.push(...walk(p, base)); + else out.push(path.relative(base, p)); + } + return out; +}; + +// In the builder checkout the overlay does not exist yet — assemble creates +// it. There the test proves only that the SOURCE side is present; the +// byte comparison runs where the overlay does exist: in the assembled app +// (npm test is the publish gate) and in the published repository. +const assembled = fs.existsSync(OVERLAY); + +describe("the webapp overlay", () => { + test("the vendored core carries the webapp the overlay is copied from", () => { + expect(fs.existsSync(SOURCE)).toBe(true); + expect(walk(SOURCE).length).toBeGreaterThan(0); + }); + + (assembled ? describe : describe.skip)("in the assembled app", () => { + test("carries exactly the files the core webapp has", () => { + expect(walk(OVERLAY)).toEqual(walk(SOURCE)); + }); + + test("is byte-identical to the core webapp", () => { + for (const rel of walk(SOURCE)) { + const a = fs.readFileSync(path.join(OVERLAY, rel)); + const b = fs.readFileSync(path.join(SOURCE, rel)); + // name the file, not just "buffers differ" + expect(a.equals(b) ? rel : `${rel} differs`).toBe(rel); + } + }); + }); +}); From 2bb401efc1589d7b975effaa7f065801af4e009d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:36:59 +0000 Subject: [PATCH 2/4] test: hold the two ecosystem tables to one repository set Same test as in builder-abap2UI5-js, reading only this repository's own AGENTS.md and README.md: the linked repo sets of the two ecosystem tables must agree, the wording stays free. The self row is a wording convention (README links it, AGENTS bolds it) and is dropped. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01BuzK8EC2CdQ5PJ6YCdfKJc --- test/ecosystem-table.test.js | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/ecosystem-table.test.js diff --git a/test/ecosystem-table.test.js b/test/ecosystem-table.test.js new file mode 100644 index 0000000..8229762 --- /dev/null +++ b/test/ecosystem-table.test.js @@ -0,0 +1,46 @@ +// AGENTS.md and README.md each open with the ecosystem table - the same six +// repositories, worded for different readers (roles and generated-ness for +// an agent, what-it-is and how-to-run for a human). Two tables of one set is +// fine; two tables of two DIFFERENT sets is how a renamed or added +// repository ends up documented in one file and missing from the other, and +// nothing noticed. This holds the two sets equal; the wording stays free. +// +// The same test file exists in builder-cap2UI5 - it reads only the repo's +// own two files, so the copies carry nothing repository-specific. +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.join(__dirname, ".."); + +/** The OTHER cap2UI5-org repositories a file's ecosystem table links. The + * row for the repository itself is dropped (README links it, AGENTS bolds + * it without a link - a wording convention, not a disagreement), so each + * file yields the same thing: everyone else. */ +const SELF = require(path.join(ROOT, "package.json")).name.toLowerCase(); + +function tableRepoSet(file) { + const lines = fs.readFileSync(path.join(ROOT, file), "utf8").split("\n"); + const start = lines.findIndex((l) => /^\| Repo/i.test(l)); + if (start === -1) throw new Error(`${file}: no ecosystem table (header row "| Repo...")`); + const set = new Set(); + for (let i = start; i < lines.length && lines[i].startsWith("|"); i++) { + for (const m of lines[i].matchAll(/github\.com\/cap2UI5\/([\w-]+)/g)) { + if (m[1].toLowerCase() !== SELF) set.add(m[1]); + } + } + return set; +} + +describe("the ecosystem table", () => { + const agents = tableRepoSet("AGENTS.md"); + const readme = tableRepoSet("README.md"); + + test("names a real set in both files", () => { + expect(agents.size).toBeGreaterThanOrEqual(4); + expect(readme.size).toBeGreaterThanOrEqual(4); + }); + + test("AGENTS.md and README.md agree on which repositories exist", () => { + expect([...agents].sort()).toEqual([...readme].sort()); + }); +}); From 66908198834c9f70d3d262c7dcdfda796066c6e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:59:57 +0000 Subject: [PATCH 3/4] chore: normalize line endings - the mirrored core is a generated tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONVENTIONS §6 asks every repository carrying ABAP or generated trees for a .gitattributes; this one commits the mirrored core package and had none, so a CRLF checkout re-committing the mirror would have turned every line into a diff. The src/ copy ships into the published cap2UI5 app, whose content outside .github/ can only come from here - verified: assemble carries it into the output and all 10 suites stay green. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01BuzK8EC2CdQ5PJ6YCdfKJc --- .gitattributes | 16 ++++++++++++++++ src/.gitattributes | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 .gitattributes create mode 100644 src/.gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1e797e9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Normalize line endings to LF for all text files. +# +# This repository commits the mirrored core package (run/input/core/) - a +# generated tree in the sense of CONVENTIONS §6. A CRLF checkout that +# re-commits the mirror would turn every line into a diff. The assembled +# app gets its own copy from src/.gitattributes. +* text=auto eol=lf +*.abap text eol=lf +*.xml text eol=lf +*.json text eol=lf +*.jsonc text eol=lf +*.md text eol=lf +*.mjs text eol=lf +*.js text eol=lf +*.yaml text eol=lf +*.yml text eol=lf diff --git a/src/.gitattributes b/src/.gitattributes new file mode 100644 index 0000000..88a80d4 --- /dev/null +++ b/src/.gitattributes @@ -0,0 +1,16 @@ +# Normalize line endings to LF for all text files. +# +# Ships into the published cap2UI5 app, which vendors the generated core/ +# tree - CONVENTIONS §6 asks every repository carrying a generated tree for +# this file, and the app repo's content outside .github/ can only come from +# here. +* text=auto eol=lf +*.abap text eol=lf +*.xml text eol=lf +*.json text eol=lf +*.jsonc text eol=lf +*.md text eol=lf +*.mjs text eol=lf +*.js text eol=lf +*.yaml text eol=lf +*.yml text eol=lf From 7ae6f29c450d618095e056846917ef5a1e76221c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 22:05:27 +0000 Subject: [PATCH 4/4] chore: timeout ceilings for both jobs, dependabot coverage for the html5 module abap2UI5 sets timeout-minutes on all of its jobs and most repositories follow; here neither job had a ceiling, and update_cap pushes to the app repo - a hung run held that pipeline for GitHub's six-hour default. dependabot also never saw src/app/z2ui5/package.json (@ui5/cli ^4.0.0) - directory is not recursive, and this is the same nobody-covered-this -manifest story the config's own root-manifest comment records. The html5 module's manifest is covered now; as with every app dependency, here is the only place a bump survives the nightly publish. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01BuzK8EC2CdQ5PJ6YCdfKJc --- .github/dependabot.yml | 12 ++++++++++++ .github/workflows/test.yml | 1 + .github/workflows/update_cap.yml | 1 + 3 files changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bc8946c..5b606fc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -50,3 +50,15 @@ updates: # compatibility question attached, not a routine dependency update -- # see the pin policy in builder-abap2UI5-js:AGENTS.md. - dependency-name: openui5-dist + + # The html5 module the mta.yaml `abap2UI5` module builds (`ui5 build`). + # Same story as the root manifest above, one level down: `directory` is + # not recursive, no ecosystem covered this manifest, and its @ui5/cli + # never saw a proposed update. It publishes through to + # cap2UI5/app/z2ui5/package.json, where (as with every app dependency) + # a bump merged over there is reverted by the next publish - here is + # the only place it survives. + - package-ecosystem: npm + directory: /src/app/z2ui5 + schedule: + interval: weekly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8ef15f..2622283 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,7 @@ permissions: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/update_cap.yml b/.github/workflows/update_cap.yml index 7a7b006..6360d36 100644 --- a/.github/workflows/update_cap.yml +++ b/.github/workflows/update_cap.yml @@ -35,6 +35,7 @@ concurrency: jobs: run: runs-on: ubuntu-latest + timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: