From 111166a563db3e124d3cf14f01aa5e0487f3a6a0 Mon Sep 17 00:00:00 2001 From: Patrick Schiller
Date: Sat, 1 Aug 2026 12:47:36 +0200 Subject: [PATCH] Move Chrome extension into product directory Signed-off-by: Patrick Schiller
--- AGENTS.md | 8 +- CONTRIBUTING.md | 7 +- README.de.md | 10 +- README.md | 12 +- RELEASING.md | 5 +- .../.github/workflows/convert-pdfs.yml | 62 +++++ .../sourcebraid/background.js | 0 .../sourcebraid/capture-utils.js | 0 .../sourcebraid/content.js | 0 .../sourcebraid/icons}/icon-128.png | Bin .../sourcebraid/icons}/icon-16.png | Bin .../sourcebraid/icons}/icon-32.png | Bin .../sourcebraid/icons}/icon-48.png | Bin .../sourcebraid/manifest.json | 0 .../sourcebraid/popup.css | 0 .../sourcebraid/popup.html | 0 .../sourcebraid/popup.js | 0 .../sourcebraid/requirements-docling.txt | 2 + .../sourcebraid/scripts/convert_pdfs.py | 231 ++++++++++++++++++ .../sourcebraid/scripts/push_with_retry.py | 72 ++++++ scripts/build_chrome_package.py | 24 +- tests/capture-utils.test.js | 7 +- tests/test_release_packages.py | 24 +- 23 files changed, 433 insertions(+), 31 deletions(-) create mode 100644 chrome-extension/sourcebraid/.github/workflows/convert-pdfs.yml rename background.js => chrome-extension/sourcebraid/background.js (100%) rename capture-utils.js => chrome-extension/sourcebraid/capture-utils.js (100%) rename content.js => chrome-extension/sourcebraid/content.js (100%) rename {icons => chrome-extension/sourcebraid/icons}/icon-128.png (100%) rename {icons => chrome-extension/sourcebraid/icons}/icon-16.png (100%) rename {icons => chrome-extension/sourcebraid/icons}/icon-32.png (100%) rename {icons => chrome-extension/sourcebraid/icons}/icon-48.png (100%) rename manifest.json => chrome-extension/sourcebraid/manifest.json (100%) rename popup.css => chrome-extension/sourcebraid/popup.css (100%) rename popup.html => chrome-extension/sourcebraid/popup.html (100%) rename popup.js => chrome-extension/sourcebraid/popup.js (100%) create mode 100644 chrome-extension/sourcebraid/requirements-docling.txt create mode 100644 chrome-extension/sourcebraid/scripts/convert_pdfs.py create mode 100644 chrome-extension/sourcebraid/scripts/push_with_retry.py diff --git a/AGENTS.md b/AGENTS.md index ed4ad5f..830edf9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,10 +29,12 @@ These instructions apply to human contributors and automated coding agents. ## Project map -- Chrome extension: root-level `manifest.json`, HTML, CSS, and JavaScript files. -- Shared capture behavior and tests: `capture-utils.js` and `tests/`. +- Chrome extension: `chrome-extension/sourcebraid/`. +- Shared capture behavior and tests: + `chrome-extension/sourcebraid/capture-utils.js` and `tests/`. - PDF conversion: `scripts/`, `requirements-docling.txt`, and - `.github/workflows/convert-pdfs.yml`. + `.github/workflows/convert-pdfs.yml`; the Chrome extension contains verified + bundled copies of these support files for unpacked development and releases. - ChatGPT/Codex plugin: `codex-plugin/sourcebraid/`. - iOS app and Share Extension: `ios/`. - Public documentation and community policy: root Markdown files and `.github/`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80acd72..6ec34f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,9 +66,10 @@ node --test tests/capture-utils.test.js python3 -m unittest discover -s tests -p "test_*.py" ``` -The Chrome extension uses Manifest V3 and has no build step. Load the repository -as an unpacked extension for manual browser testing. The iOS app and Share -Extension require Xcode; see [ios/README.md](ios/README.md) for build commands. +The Chrome extension uses Manifest V3 and has no build step. Load +`chrome-extension/sourcebraid` as an unpacked extension for manual browser +testing. The iOS app and Share Extension require Xcode; see +[ios/README.md](ios/README.md) for build commands. The website is maintained separately from the public release. Maintainers with access to its private source can validate it with: diff --git a/README.de.md b/README.de.md index d9c605b..7b6a50d 100644 --- a/README.de.md +++ b/README.de.md @@ -159,7 +159,7 @@ Bei einem Gist wird eine einzelne Markdown-Datei direkt als Dokumentinhalt gespe 1. `chrome://extensions` öffnen. 2. **Entwicklermodus** aktivieren. 3. **Entpackte Erweiterung laden** auswählen. -4. Diesen Ordner auswählen. +4. [`chrome-extension/sourcebraid`](chrome-extension/sourcebraid) auswählen. 5. Eine unterstützte Quelle öffnen und auf das **SourceBraid**-Symbol klicken. 6. GitHub-Repository konfigurieren, optional Tags oder Notizen ergänzen und **Save to GitHub** wählen. @@ -248,4 +248,10 @@ Die native iOS-App und Share Extension liegen unter [`ios/`](ios/README.md). Nac ## Technische Hinweise -Die Chrome-Erweiterung benötigt keinen Build-Schritt und bündelt keine Drittanbieter-Runtime. Docling läuft ausschließlich in der GitHub Action des Ziel-Repositorys. Die HTML-Konvertierung erfolgt lokal in der Erweiterung; API- und Bildzugriffe nutzen je nach Quelle entweder normale HTTP-Anfragen oder die vorhandene angemeldete Browser-Sitzung. +Der Quellcode der Chrome-Erweiterung liegt unter +[`chrome-extension/sourcebraid`](chrome-extension/sourcebraid). Die Erweiterung +benötigt keinen Build-Schritt und bündelt keine Drittanbieter-Runtime. Docling +läuft ausschließlich in der GitHub Action des Ziel-Repositorys. Die +HTML-Konvertierung erfolgt lokal in der Erweiterung; API- und Bildzugriffe +nutzen je nach Quelle entweder normale HTTP-Anfragen oder die vorhandene +angemeldete Browser-Sitzung. diff --git a/README.md b/README.md index 1b65aa6..4dd52c2 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ be loaded through the still-open Gist tab. 1. Open `chrome://extensions`. 2. Enable **Developer mode**. 3. Select **Load unpacked**. -4. Choose this repository folder. +4. Choose [`chrome-extension/sourcebraid`](chrome-extension/sourcebraid). 5. Open a supported source and select the **SourceBraid** icon. 6. Configure the private GitHub repository, optionally add tags or notes, and choose **Save to GitHub**. @@ -331,7 +331,9 @@ the local, user-controlled data flow and licensing boundaries. ## Technical notes -The Chrome extension has no build step and bundles no third-party runtime. -Docling runs only inside the target repository's GitHub Action. HTML conversion -happens locally in the extension; API and image requests use either ordinary -HTTP or the browser's existing authenticated session, depending on the source. +The Chrome extension source lives under +[`chrome-extension/sourcebraid`](chrome-extension/sourcebraid). It has no build +step and bundles no third-party runtime. Docling runs only inside the target +repository's GitHub Action. HTML conversion happens locally in the extension; +API and image requests use either ordinary HTTP or the browser's existing +authenticated session, depending on the source. diff --git a/RELEASING.md b/RELEASING.md index d3d6d9b..96d516f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -26,7 +26,7 @@ customer-facing version unless the maintainers document a different decision. Before creating the first release candidate, align these customer-facing versions: -- Chrome: `version` in `manifest.json`. +- Chrome: `version` in `chrome-extension/sourcebraid/manifest.json`. - ChatGPT/Codex skills package: `version` in `codex-plugin/sourcebraid/.codex-plugin/plugin.json`. - iOS app and Share Extension: `MARKETING_VERSION` in @@ -71,7 +71,8 @@ changes. ## Chrome package -Build the Manifest V3 archive from its explicit allowlist: +The unpacked extension source lives in `chrome-extension/sourcebraid`. Build the +Manifest V3 archive from its explicit allowlist: ```bash python3 scripts/build_chrome_package.py diff --git a/chrome-extension/sourcebraid/.github/workflows/convert-pdfs.yml b/chrome-extension/sourcebraid/.github/workflows/convert-pdfs.yml new file mode 100644 index 0000000..dfc839f --- /dev/null +++ b/chrome-extension/sourcebraid/.github/workflows/convert-pdfs.yml @@ -0,0 +1,62 @@ +# Managed by SourceBraid PDF support. +name: Convert PDFs to Markdown + +on: + push: + paths: + - "**/assets/*/source.pdf" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: pdf-conversion-${{ github.ref }} + cancel-in-progress: false + +jobs: + convert: + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: requirements-docling.txt + + - name: Install Docling + run: python -m pip install --requirement requirements-docling.txt + + - name: Convert queued PDFs + if: github.event_name == 'push' + run: >- + python scripts/convert_pdfs.py + --before "${{ github.event.before }}" + --after "${{ github.sha }}" + + - name: Convert all queued PDFs + if: github.event_name == 'workflow_dispatch' + run: python scripts/convert_pdfs.py --all + + - name: Commit converted Markdown + run: | + git add --all + if git diff --cached --quiet; then + echo "No converted files changed." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "Convert queued PDFs to Markdown" + python scripts/push_with_retry.py \ + --branch "${GITHUB_REF_NAME}" \ + --attempts 5 \ + --delay-seconds 2 diff --git a/background.js b/chrome-extension/sourcebraid/background.js similarity index 100% rename from background.js rename to chrome-extension/sourcebraid/background.js diff --git a/capture-utils.js b/chrome-extension/sourcebraid/capture-utils.js similarity index 100% rename from capture-utils.js rename to chrome-extension/sourcebraid/capture-utils.js diff --git a/content.js b/chrome-extension/sourcebraid/content.js similarity index 100% rename from content.js rename to chrome-extension/sourcebraid/content.js diff --git a/icons/icon-128.png b/chrome-extension/sourcebraid/icons/icon-128.png similarity index 100% rename from icons/icon-128.png rename to chrome-extension/sourcebraid/icons/icon-128.png diff --git a/icons/icon-16.png b/chrome-extension/sourcebraid/icons/icon-16.png similarity index 100% rename from icons/icon-16.png rename to chrome-extension/sourcebraid/icons/icon-16.png diff --git a/icons/icon-32.png b/chrome-extension/sourcebraid/icons/icon-32.png similarity index 100% rename from icons/icon-32.png rename to chrome-extension/sourcebraid/icons/icon-32.png diff --git a/icons/icon-48.png b/chrome-extension/sourcebraid/icons/icon-48.png similarity index 100% rename from icons/icon-48.png rename to chrome-extension/sourcebraid/icons/icon-48.png diff --git a/manifest.json b/chrome-extension/sourcebraid/manifest.json similarity index 100% rename from manifest.json rename to chrome-extension/sourcebraid/manifest.json diff --git a/popup.css b/chrome-extension/sourcebraid/popup.css similarity index 100% rename from popup.css rename to chrome-extension/sourcebraid/popup.css diff --git a/popup.html b/chrome-extension/sourcebraid/popup.html similarity index 100% rename from popup.html rename to chrome-extension/sourcebraid/popup.html diff --git a/popup.js b/chrome-extension/sourcebraid/popup.js similarity index 100% rename from popup.js rename to chrome-extension/sourcebraid/popup.js diff --git a/chrome-extension/sourcebraid/requirements-docling.txt b/chrome-extension/sourcebraid/requirements-docling.txt new file mode 100644 index 0000000..0beff32 --- /dev/null +++ b/chrome-extension/sourcebraid/requirements-docling.txt @@ -0,0 +1,2 @@ +# Managed by SourceBraid PDF support. +docling>=2.0,<3.0 diff --git a/chrome-extension/sourcebraid/scripts/convert_pdfs.py b/chrome-extension/sourcebraid/scripts/convert_pdfs.py new file mode 100644 index 0000000..6515dfe --- /dev/null +++ b/chrome-extension/sourcebraid/scripts/convert_pdfs.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# Managed by SourceBraid PDF support. +"""Convert queued web-clip PDFs with Docling and update their Markdown/index.""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import re +import shutil +import subprocess +from datetime import datetime, timezone +from pathlib import Path + + +ZERO_SHA = "0" * 40 +NOTES_PATTERN = re.compile( + r".*?\s*", + re.DOTALL, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--before", default="") + parser.add_argument("--after", default="HEAD") + parser.add_argument("--all", action="store_true") + args = parser.parse_args() + + sources = discover_sources(args.before, args.after, args.all) + if not sources: + print("No queued source.pdf files found.") + return 0 + + for source in sources: + convert_source(source) + return 0 + + +def discover_sources(before: str, after: str, process_all: bool) -> list[Path]: + if process_all or not before or before == ZERO_SHA: + candidates = Path(".").glob("**/assets/*/source.pdf") + else: + result = subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=AM", before, after, "--", "**/assets/*/source.pdf"], + check=True, + capture_output=True, + text=True, + ) + candidates = (Path(line) for line in result.stdout.splitlines()) + + return sorted( + path for path in candidates + if path.is_file() and path.name == "source.pdf" and path.parent.parent.name == "assets" + ) + + +def convert_source(source: Path) -> None: + from docling.datamodel.base_models import InputFormat + from docling.datamodel.pipeline_options import PdfPipelineOptions + from docling.document_converter import DocumentConverter, PdfFormatOption + from docling_core.types.doc import ImageRefMode + + source = source.resolve() + clip_slug = source.parent.name + target = source.parent.parent.parent / f"{clip_slug}.md" + pending = target.read_text(encoding="utf-8") if target.exists() else "" + frontmatter = extract_frontmatter(pending) + notes = extract_notes(pending) + + pipeline_options = PdfPipelineOptions() + pipeline_options.images_scale = 1.5 + pipeline_options.generate_picture_images = True + pipeline_options.do_ocr = True + pipeline_options.do_table_structure = True + converter = DocumentConverter( + format_options={ + InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options) + } + ) + result = converter.convert(source) + + export_path = source.parent / "docling-output.md" + artifact_path = source.parent / "docling-output_artifacts" + if artifact_path.exists(): + shutil.rmtree(artifact_path) + result.document.save_as_markdown(export_path, image_mode=ImageRefMode.REFERENCED) + body = export_path.read_text(encoding="utf-8").strip() + export_path.unlink(missing_ok=True) + + artifact_prefix = source.parent.relative_to(target.parent).as_posix() + body = rewrite_artifact_links(body, artifact_prefix) + + converted_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + pages = len(result.document.pages) + version = importlib.metadata.version("docling") + frontmatter = update_frontmatter( + frontmatter, + { + "capture_method": json.dumps("pdf-docling"), + "conversion_status": json.dumps("complete"), + "converter": json.dumps("docling"), + "converter_version": json.dumps(version), + "pages": str(pages), + "ocr_enabled": "true", + "converted_at": json.dumps(converted_at), + }, + ) + + title = frontmatter_value(frontmatter, "title") or clip_slug + if not re.match(r"^#\s+", body): + body = f"# {title}\n\n{body}" + sections = [frontmatter.strip(), notes.strip(), body] + target.write_text("\n\n".join(section for section in sections if section).rstrip() + "\n", encoding="utf-8") + + images = sorted( + path.relative_to(Path.cwd()).as_posix() + for path in source.parent.glob("docling-output_artifacts/**/*") + if path.is_file() + ) + update_index(target, source, images, pages, version, converted_at) + print(f"Converted {source.relative_to(Path.cwd())} -> {target.relative_to(Path.cwd())}") + + +def extract_frontmatter(markdown: str) -> str: + match = re.match(r"\A---\s*\n.*?\n---\s*", markdown, re.DOTALL) + return match.group(0).strip() if match else "---\n---" + + +def extract_notes(markdown: str) -> str: + match = NOTES_PATTERN.search(markdown) + return match.group(0).strip() if match else "" + + +def rewrite_artifact_links(markdown: str, artifact_prefix: str) -> str: + """Make Docling image links relative, regardless of its exported path style.""" + replacement = f"{artifact_prefix.rstrip('/')}/docling-output_artifacts/" + return re.sub( + r"(?<=\]\()[^)\n]*docling-output_artifacts/", + lambda _match: replacement, + markdown, + ) + + +def update_frontmatter(frontmatter: str, updates: dict[str, str]) -> str: + lines = frontmatter.splitlines() + if not lines or lines[0].strip() != "---": + lines = ["---", "---"] + if lines[-1].strip() != "---": + lines.append("---") + + seen: set[str] = set() + output = [lines[0]] + for line in lines[1:-1]: + match = re.match(r"^([A-Za-z0-9_-]+):", line) + key = match.group(1) if match else "" + if key in updates: + output.append(f"{key}: {updates[key]}") + seen.add(key) + else: + output.append(line) + for key, value in updates.items(): + if key not in seen: + output.append(f"{key}: {value}") + output.append("---") + return "\n".join(output) + + +def frontmatter_value(frontmatter: str, key: str) -> str: + match = re.search(rf"^{re.escape(key)}:\s*(.+)$", frontmatter, re.MULTILINE) + if not match: + return "" + value = match.group(1).strip() + try: + decoded = json.loads(value) + return str(decoded) + except json.JSONDecodeError: + return value.strip('"\'') + + +def update_index( + target: Path, + source: Path, + images: list[str], + pages: int, + version: str, + converted_at: str, +) -> None: + root = target.parents[2] + index_paths = [root / "index.jsonl"] + shard_root = root / "index" + if shard_root.exists(): + index_paths.extend(sorted(shard_root.glob("*.jsonl"))) + + repository_root = Path.cwd().resolve() + target_repo_path = target.resolve().relative_to(repository_root).as_posix() + source_repo_path = source.resolve().relative_to(repository_root).as_posix() + for index_path in index_paths: + if not index_path.exists(): + continue + output: list[str] = [] + changed = False + for line in index_path.read_text(encoding="utf-8").splitlines(): + try: + entry = json.loads(line) + except json.JSONDecodeError: + output.append(line) + continue + if entry.get("path") == target_repo_path or entry.get("pdf_path") == source_repo_path: + entry.update( + { + "capture_method": "pdf-docling", + "conversion_status": "complete", + "converter": "docling", + "converter_version": version, + "pages": pages, + "ocr_enabled": True, + "converted_at": converted_at, + "images": [{"path": image} for image in images], + } + ) + changed = True + output.append(json.dumps(entry, ensure_ascii=False, separators=(",", ":"))) + + if changed: + index_path.write_text("\n".join(output).rstrip() + "\n", encoding="utf-8") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/chrome-extension/sourcebraid/scripts/push_with_retry.py b/chrome-extension/sourcebraid/scripts/push_with_retry.py new file mode 100644 index 0000000..82989ba --- /dev/null +++ b/chrome-extension/sourcebraid/scripts/push_with_retry.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Push a generated commit while tolerating concurrent branch updates.""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +import time + + +def synchronize_and_push( + branch: str, + *, + remote: str = "origin", + attempts: int = 5, + delay_seconds: float = 2.0, +) -> None: + if not branch: + raise ValueError("branch must not be empty") + if attempts < 1: + raise ValueError("attempts must be at least 1") + if delay_seconds < 0: + raise ValueError("delay_seconds must not be negative") + + for attempt in range(1, attempts + 1): + subprocess.run(["git", "fetch", remote, branch], check=True) + subprocess.run(["git", "rebase", "FETCH_HEAD"], check=True) + result = subprocess.run( + ["git", "push", remote, f"HEAD:{branch}"], + check=False, + ) + if result.returncode == 0: + print(f"Pushed converted Markdown on attempt {attempt}.") + return + + if attempt == attempts: + raise RuntimeError( + f"Could not push converted Markdown after {attempts} attempts." + ) + + delay = delay_seconds * attempt + print( + f"Push attempt {attempt} was rejected; refreshing the branch " + f"and retrying in {delay:g} seconds." + ) + time.sleep(delay) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--branch", required=True) + parser.add_argument("--remote", default="origin") + parser.add_argument("--attempts", type=int, default=5) + parser.add_argument("--delay-seconds", type=float, default=2.0) + args = parser.parse_args() + + try: + synchronize_and_push( + args.branch, + remote=args.remote, + attempts=args.attempts, + delay_seconds=args.delay_seconds, + ) + except (ValueError, RuntimeError, subprocess.CalledProcessError) as error: + print(error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build_chrome_package.py b/scripts/build_chrome_package.py index 86fb833..a91a68d 100644 --- a/scripts/build_chrome_package.py +++ b/scripts/build_chrome_package.py @@ -12,8 +12,17 @@ from pathlib import Path, PurePosixPath -PACKAGE_FILES = ( +EXTENSION_DIRECTORY = PurePosixPath("chrome-extension/sourcebraid") + +PDF_SUPPORT_FILES = ( ".github/workflows/convert-pdfs.yml", + "requirements-docling.txt", + "scripts/convert_pdfs.py", + "scripts/push_with_retry.py", +) + +PACKAGE_FILES = ( + *PDF_SUPPORT_FILES, "background.js", "capture-utils.js", "content.js", @@ -25,9 +34,6 @@ "popup.css", "popup.html", "popup.js", - "requirements-docling.txt", - "scripts/convert_pdfs.py", - "scripts/push_with_retry.py", ) VERSION_PATTERN = re.compile(r"^(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*)){0,3}$") @@ -38,7 +44,7 @@ class PackageError(RuntimeError): def validated_manifest(repository_root: Path) -> dict[str, object]: - manifest_path = repository_root / "manifest.json" + manifest_path = repository_root.joinpath(*EXTENSION_DIRECTORY.parts, "manifest.json") try: manifest = json.loads(manifest_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as error: @@ -57,11 +63,12 @@ def validated_manifest(repository_root: Path) -> dict[str, object]: def validated_package_files(repository_root: Path) -> list[tuple[Path, str]]: files: list[tuple[Path, str]] = [] + extension_root = repository_root.joinpath(*EXTENSION_DIRECTORY.parts) for archive_name in PACKAGE_FILES: relative = PurePosixPath(archive_name) if relative.is_absolute() or ".." in relative.parts: raise PackageError(f"unsafe package path: {archive_name}") - source = repository_root.joinpath(*relative.parts) + source = extension_root.joinpath(*relative.parts) if source.is_symlink(): raise PackageError(f"refusing to package symlink: {archive_name}") if not source.is_file(): @@ -106,7 +113,10 @@ def parser() -> argparse.ArgumentParser: "--repository-root", type=Path, default=Path(__file__).resolve().parents[1], - help="SourceBraid repository root (defaults to the parent of scripts/).", + help=( + "SourceBraid repository root containing chrome-extension/sourcebraid " + "(defaults to the parent of scripts/)." + ), ) result.add_argument( "--output", diff --git a/tests/capture-utils.test.js b/tests/capture-utils.test.js index 8ca5b69..cb7bff8 100644 --- a/tests/capture-utils.test.js +++ b/tests/capture-utils.test.js @@ -4,7 +4,8 @@ const path = require("node:path"); const test = require("node:test"); const vm = require("node:vm"); -const Core = require("../capture-utils.js"); +const extensionRoot = path.join(__dirname, "..", "chrome-extension", "sourcebraid"); +const Core = require(path.join(extensionRoot, "capture-utils.js")); test("buildGitHubPath keeps the configured root and local capture date", () => { assert.equal( @@ -229,7 +230,7 @@ test("GitHub saves reject an unavailable target repository before reading archiv }; context.globalThis = context; context.SourceBraidCore = Core; - vm.runInNewContext(fs.readFileSync(path.join(__dirname, "..", "background.js"), "utf8"), context); + vm.runInNewContext(fs.readFileSync(path.join(extensionRoot, "background.js"), "utf8"), context); await assert.rejects( context.ensureGitHubRepository({ owner: "missing-owner", repo: "missing-repo", token: "test-token" }), @@ -286,7 +287,7 @@ test("DeepMind blog capture excludes cover and related-post cards", () => { }; context.globalThis = context; context.SourceBraidCore = Core; - vm.runInNewContext(fs.readFileSync(path.join(__dirname, "..", "content.js"), "utf8"), context); + vm.runInNewContext(fs.readFileSync(path.join(extensionRoot, "content.js"), "utf8"), context); const result = context.tryGoogleDeepMindBlog({ pageUrl: "https://deepmind.google/blog/example/", diff --git a/tests/test_release_packages.py b/tests/test_release_packages.py index 0f1a2b7..445c390 100644 --- a/tests/test_release_packages.py +++ b/tests/test_release_packages.py @@ -31,12 +31,13 @@ class ChromePackageTests(unittest.TestCase): def test_build_uses_only_the_explicit_allowlist(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) + extension_root = root.joinpath(*chrome_package.EXTENSION_DIRECTORY.parts) manifest = {"manifest_version": 3, "name": "SourceBraid", "version": "1.2.3"} for relative in chrome_package.PACKAGE_FILES: - target = root / relative + target = extension_root / relative target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(b"fixture") - (root / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + (extension_root / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") (root / "web-clips" / "private.md").parent.mkdir(parents=True) (root / "web-clips" / "private.md").write_text("secret", encoding="utf-8") output = root / "dist" / "sourcebraid.zip" @@ -52,20 +53,31 @@ def test_build_uses_only_the_explicit_allowlist(self): def test_symlink_is_rejected(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) + extension_root = root.joinpath(*chrome_package.EXTENSION_DIRECTORY.parts) for relative in chrome_package.PACKAGE_FILES: - target = root / relative + target = extension_root / relative target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(b"fixture") - (root / "manifest.json").write_text( + (extension_root / "manifest.json").write_text( json.dumps({"manifest_version": 3, "name": "SourceBraid", "version": "1.0.0"}), encoding="utf-8", ) - (root / "content.js").unlink() - (root / "content.js").symlink_to(root / "background.js") + (extension_root / "content.js").unlink() + (extension_root / "content.js").symlink_to(extension_root / "background.js") with self.assertRaises(chrome_package.PackageError): chrome_package.validated_package_files(root) + def test_bundled_pdf_support_matches_the_canonical_files(self): + extension_root = REPOSITORY_ROOT.joinpath(*chrome_package.EXTENSION_DIRECTORY.parts) + + for relative in chrome_package.PDF_SUPPORT_FILES: + with self.subTest(path=relative): + self.assertEqual( + (extension_root / relative).read_bytes(), + (REPOSITORY_ROOT / relative).read_bytes(), + ) + class PluginPackageTests(unittest.TestCase): def test_public_package_removes_local_mcp_configuration(self):