diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5094e27..c2d53ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,36 @@ jobs: - name: Run verification suite run: npm run verify + node20: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Set up lowest supported Node.js + uses: actions/setup-node@v6 + with: + node-version: '20.19.0' + cache: 'npm' + cache-dependency-path: | + package-lock.json + server/package-lock.json + + - name: Install root dependencies + run: npm ci + + - name: Install server dependencies + run: npm ci + working-directory: server + + - name: Run unit and relay integration gates + run: | + npm run test:coverage + node scripts/test-server-routes.mjs + node scripts/test-server-ws.mjs + env: + ADMIN_METRICS_TOKEN: verify-admin-token-with-more-than-32-chars + e2e: # Kept separate from `verify`: this job needs a downloaded browser, so a # failure here should read as "the browser flow broke", not as a broken @@ -66,6 +96,10 @@ jobs: - name: Install root dependencies run: npm ci + - name: Install server dependencies + run: npm ci + working-directory: server + - name: Install Playwright browsers run: npx playwright install --with-deps chromium chromium-headless-shell firefox webkit @@ -75,3 +109,14 @@ jobs: - name: Run cross-browser detection and extension E2E tests run: npm run test:e2e + + - name: Upload browser failure diagnostics + if: failure() + uses: actions/upload-artifact@v7 + with: + name: e2e-failure-diagnostics + path: | + test-results/ + playwright-report/ + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/race-tests.yml b/.github/workflows/race-tests.yml index 7529a3b..8f14849 100644 --- a/.github/workflows/race-tests.yml +++ b/.github/workflows/race-tests.yml @@ -15,7 +15,7 @@ concurrency: jobs: extension-races: runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 steps: - name: Checkout code uses: actions/checkout@v7 @@ -30,6 +30,10 @@ jobs: - name: Install dependencies run: npm ci + - name: Install server dependencies + run: npm ci + working-directory: server + - name: Install Playwright Chromium run: npx playwright install --with-deps chromium chromium-headless-shell diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8f43a6..b627534 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,14 +5,139 @@ on: tags: - 'v*' -# A release run must never be interrupted (it commits back to main and publishes -# artifacts). Only dedupe accidental re-pushes of the same tag. concurrency: group: release-${{ github.ref_name }} cancel-in-progress: false jobs: + preflight: + runs-on: ubuntu-latest + permissions: + contents: read + checks: read + outputs: + version: ${{ steps.release-ref.outputs.version }} + tag-commit: ${{ steps.release-ref.outputs.tag_commit }} + steps: + - name: Checkout release tag + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: | + package-lock.json + server/package-lock.json + + - name: Validate annotated tag, main commit, and required checks + id: release-ref + run: node scripts/release-preflight.mjs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install root dependencies + run: npm ci + + - name: Install server dependencies + run: npm ci + working-directory: server + + - name: Run complete release verification + run: npm run verify + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium chromium-headless-shell firefox webkit + + - name: Run browser E2E suite + run: npm run test:e2e + + - name: Build relay container without publishing + run: docker build --file server/Dockerfile --tag koalasync-release-preflight . + + - name: Smoke-test relay container + run: | + CONTAINER_ID=$(docker run --detach --publish 127.0.0.1::3000 --env SERVER_SALT=release-preflight-salt-with-more-than-thirty-two-chars koalasync-release-preflight) + trap 'docker rm --force "$CONTAINER_ID" >/dev/null 2>&1 || true' EXIT + HOST_PORT=$(docker port "$CONTAINER_ID" 3000/tcp | sed 's/.*://') + for attempt in $(seq 1 30); do + if curl --fail --silent "http://127.0.0.1:$HOST_PORT/health" >/dev/null; then + exit 0 + fi + sleep 1 + done + docker logs "$CONTAINER_ID" + exit 1 + + release-extension-draft: + needs: preflight + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + steps: + - name: Checkout release tag + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies and build release artifacts + run: | + npm ci + npm run build:extension + node website/build.cjs + + - name: Validate Firefox package + run: npx addons-linter --warnings-as-errors dist/koalasync-firefox.zip + + - name: Generate extension checksums + working-directory: dist + run: sha256sum koalasync-chrome.zip koalasync-firefox.zip > SHA256SUMS + + - name: Validate release assets before publication + run: node scripts/verify-published-release.mjs "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --asset-dir dist --skip-attestation + + - name: Attest extension archives + uses: actions/attest@v4 + with: + subject-path: dist/koalasync-*.zip + + - name: Create draft GitHub release + uses: softprops/action-gh-release@v3 + with: + files: | + dist/koalasync-chrome.zip + dist/koalasync-firefox.zip + dist/SHA256SUMS + name: Release ${{ github.ref_name }} + generate_release_notes: true + draft: true + prerelease: false + + - name: Verify draft extension release + run: node scripts/verify-published-release.mjs "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload website artifacts + uses: actions/upload-artifact@v7 + with: + name: website-www + path: website/www/ + if-no-files-found: error + release-server: + needs: [preflight, release-extension-draft] runs-on: ubuntu-latest permissions: contents: read @@ -20,7 +145,7 @@ jobs: id-token: write attestations: write steps: - - name: Checkout code + - name: Checkout release tag uses: actions/checkout@v7 - name: Set up Docker Buildx @@ -52,131 +177,46 @@ jobs: platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - # Reuse layers across releases to speed up the multi-arch build. cache-from: type=gha cache-to: type=gha,mode=max - - name: Generate artifact attestation + - name: Attest relay image uses: actions/attest@v4 with: subject-name: ghcr.io/${{ github.repository }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true - release-extension: + - name: Verify manifest, provenance, and running image + env: + DIGEST: ${{ steps.build.outputs.digest }} + IMAGE: ghcr.io/${{ github.repository }} + SOURCE_DIGEST: ${{ needs.preflight.outputs.tag-commit }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + docker buildx imagetools inspect "$IMAGE@$DIGEST" --raw > /tmp/koalasync-manifest.json + node -e 'const m=require("/tmp/koalasync-manifest.json"); const p=new Set(m.manifests.map(x=>`${x.platform.os}/${x.platform.architecture}`)); for (const x of ["linux/amd64","linux/arm64"]) if(!p.has(x)) throw new Error(`missing platform ${x}`)' + gh attestation verify "oci://$IMAGE@$DIGEST" --repo "$GITHUB_REPOSITORY" --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" --source-ref "$GITHUB_REF" --source-digest "$SOURCE_DIGEST" --deny-self-hosted-runners + docker pull --platform linux/amd64 "$IMAGE@$DIGEST" + CONTAINER_ID=$(docker run --detach --publish 127.0.0.1::3000 --env SERVER_SALT=release-smoke-salt-with-more-than-thirty-two-chars "$IMAGE@$DIGEST") + trap 'docker rm --force "$CONTAINER_ID" >/dev/null 2>&1 || true' EXIT + HOST_PORT=$(docker port "$CONTAINER_ID" 3000/tcp | sed 's/.*://') + for attempt in $(seq 1 30); do + if curl --fail --silent "http://127.0.0.1:$HOST_PORT/health" >/dev/null; then + exit 0 + fi + sleep 1 + done + docker logs "$CONTAINER_ID" + exit 1 + + finalize-release: + needs: [preflight, release-extension-draft, release-server] runs-on: ubuntu-latest permissions: contents: write - id-token: write - attestations: write steps: - - name: Checkout code - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - cache: 'npm' - - - name: Extract version from tag - id: version - run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - - - name: Inject version into source files - run: | - VERSION=${{ steps.version.outputs.VERSION }} - DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - echo "Injecting version $VERSION from tag $GITHUB_REF_NAME..." - - # 1. extension/manifest.base.json - jq --arg v "$VERSION" '.version = $v' extension/manifest.base.json > tmp.json && mv tmp.json extension/manifest.base.json - echo " ✓ manifest.base.json -> $VERSION" - - # 2. shared/constants.js — APP_VERSION - sed -i "s/export const APP_VERSION = [\"'].*[\"']/export const APP_VERSION = \"$VERSION\"/" shared/constants.js - echo " ✓ shared/constants.js -> $VERSION" - - # 3. package.json - jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json - echo " ✓ package.json -> $VERSION" - - # 4. package-lock.json root package metadata - jq --arg v "$VERSION" '.version = $v | .packages[""].version = $v' package-lock.json > tmp.json && mv tmp.json package-lock.json - echo " ✓ package-lock.json -> $VERSION" - - # 5. website/version.json - jq -n --arg v "$VERSION" --arg d "$DATE" '{version: $v, date: $d}' > website/version.json - echo " ✓ website/version.json -> version $VERSION, date $DATE" - - # 6. website/template.html — SoftwareApplication schema - sed -i "s/\"softwareVersion\": \".*\"/\"softwareVersion\": \"$VERSION\"/" website/template.html - echo " ✓ website/template.html -> softwareVersion $VERSION" - - # 7. website/llms.txt — machine-readable release metadata - sed -i "s/Current website release: .*/Current website release: $VERSION/" website/llms.txt - echo " ✓ website/llms.txt -> $VERSION" - - # 8. README.md — version badge & banner - sed -i "s|Release-v[0-9]\+\.[0-9]\+\.[0-9]\+-blue|Release-v$VERSION-blue|g" README.md - sed -i "s/New v[0-9]\+\.[0-9]\+\.[0-9]\+ Release/New v$VERSION Release/g" README.md - echo " ✓ README.md -> v$VERSION" - - echo "Version injection complete." - - - name: Commit and push version updates back to main - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add extension/manifest.base.json shared/constants.js package.json package-lock.json website/version.json website/template.html website/llms.txt README.md - git commit -m "chore(release): update versions to $GITHUB_REF_NAME [skip ci]" || echo "No changes to commit" - git push origin HEAD:main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Extensions - run: | - npm ci - npm run build:extension - - - name: Generate extension checksums - run: | - cd dist - sha256sum koalasync-chrome.zip koalasync-firefox.zip > SHA256SUMS - cat SHA256SUMS - - - name: Generate artifact attestation for extensions - uses: actions/attest@v4 - with: - subject-path: dist/koalasync-*.zip - - - name: Build Website - run: node website/build.cjs - - - name: Upload Website Artifacts - uses: actions/upload-artifact@v7 - with: - name: website-www - path: website/www/ - if-no-files-found: error - - - name: Create GitHub Release - uses: softprops/action-gh-release@v3 - with: - files: | - dist/koalasync-chrome.zip - dist/koalasync-firefox.zip - dist/SHA256SUMS - name: Release ${{ github.ref_name }} - generate_release_notes: true - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Verify published extension release - run: node scripts/verify-published-release.mjs "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" + - name: Publish verified GitHub release + run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false --verify-tag env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abba016..4fce658 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,9 @@ KoalaSync uses a **single source of truth** for all protocol constants in `share ## Version Numbers > [!CAUTION] -> **Never manually bump version numbers.** The CI pipeline injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Manual bumps cause conflicts. +> **Never edit release versions independently.** Release maintainers run +> `npm run prepare:release -- MAJOR.MINOR.PATCH` on a branch and merge the +> resulting version changes through a CI-green pull request before tagging. --- diff --git a/docs/AI_INIT.md b/docs/AI_INIT.md index 7fddb36..b1a0ae2 100644 --- a/docs/AI_INIT.md +++ b/docs/AI_INIT.md @@ -118,17 +118,23 @@ Before starting any task, committing, or pushing, you **MUST** run `git pull --r > [!CAUTION] > **AI AGENTS MUST FOLLOW THIS EXACT SEQUENCE WHEN RELEASING A NEW VERSION OR TAGGING.** > -> **🚫 NO MANUAL VERSION BUMPING**: You MUST **NEVER** manually modify the version strings in `package.json`, `extension/manifest.base.json`, or `website/version.json`. The GitHub Actions CI pipeline automatically extracts the version from the git tag (e.g. `v2.0.5` -> `2.0.5`), injects it into all target files, and commits the updates back to `main` with `[skip ci]`. Manual bumps will cause merge conflicts and build failures. +> **🚫 NO INDEPENDENT VERSION EDITS**: Run `npm run prepare:release -- MAJOR.MINOR.PATCH` on a release-preparation branch. Never edit only one version source, and never expect the tag workflow to modify `main`. > - **Website Versioning**: **NEVER** manually modify generated version strings in `website/www/`. The website build injects version data from `website/version.json` into generated output. 1. **MANDATORY SYNTAX & LINT CHECKS**: Before staging, committing, or pushing any changes, you **MUST** run both checks on every modified JavaScript file: - **Syntax Validation**: Run `node -c` on every single modified JavaScript file (e.g., `node -c extension/background.js` and `node -c extension/content.js`). **NEVER** commit or push code that fails this check. - **ESLint Validation**: Run `npm run lint` (or `npx eslint .`). The output must show **zero errors and zero warnings**. ESLint is configured to catch undefined variables, unused vars, unreachable code, and other semantic issues. **NEVER** commit or push code that fails this check. -2. Commit all verified code changes and push to `main`. -3. Create and push a new tag. **MANDATORY**: Tags MUST start with a `v` (e.g., `v1.4.0`). The GitHub Actions release workflow is strictly configured to ignore any tags without the `v` prefix. +2. Commit the prepared version and release-note changes on a branch, push it, + open a pull request, and wait for required `verify`, `node20`, and `e2e` + checks. Direct pushes to `main` are not part of the release process. +3. After the PR is merged, update local `main` and create an annotated exact + SemVer tag (`git tag -a v1.4.0 -m "Release v1.4.0"`) on the same commit as + `origin/main`. - **🚫 TAG IMMUTABILITY**: Once a tag is pushed to `origin`, it is **PERMANENT**. You MUST **NEVER** reuse, move, or force-push an existing tag — not even to "fix" a mistake. If a release is missing a fix, increment the version and create a **new** tag (e.g., `v1.7.0` → `v1.7.1`). Tags are immutable identifiers; moving them breaks CI pipelines, corrupts the release history, and causes unreproducible builds. - **🚫 WHEN NOT TO TAG**: Do NOT create a release tag for changes that do NOT affect the shipped extension or server artifacts. Website text changes, documentation updates (`.md` files), and landing page content do NOT require a version tag. Tags trigger the full CI pipeline (Docker build, extension packaging, GitHub Release) — running this for a typo fix wastes CI resources and creates meaningless releases. Only tag when extension code (`extension/`), server code (`server/`), or shared protocol constants (`shared/`) have changed. -4. The CI will extract the version from the tag (e.g., `v1.4.0` → `1.4.0`), inject it into all source files, build the extension artifacts, publish the Docker image, and create a GitHub Release. -5. Verify the release builds on GitHub Actions. +4. The release workflow validates the unchanged tagged source, creates a draft + release, publishes and verifies the relay image, and makes the release public + only after every gate succeeds. +5. Verify GitHub assets, attestations, GHCR platforms/digest, and health smoke. ### 🚫 Force Push Policy > [!CAUTION] diff --git a/docs/SYNC_GUIDE.md b/docs/SYNC_GUIDE.md index df0f69b..6fec6ac 100644 --- a/docs/SYNC_GUIDE.md +++ b/docs/SYNC_GUIDE.md @@ -35,7 +35,10 @@ The build script performs the following actions: The system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake. - The version is defined in `shared/constants.js`. - If the extension and server versions mismatch, the server will reject the connection with an `Incompatible protocol version` error. -- **Never manually bump version numbers**. The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Run the build script to synchronize other constant updates. +- Never edit release versions independently. Run + `npm run prepare:release -- MAJOR.MINOR.PATCH` on a release-preparation branch; + the release tag is accepted only after that change reaches `main` with all CI + checks passing. > [!CAUTION] > **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the build script is run. Always edit the files in the root `shared/` directory and then run the build script. diff --git a/docs/devops.md b/docs/devops.md index a9d08e0..6cf702b 100644 --- a/docs/devops.md +++ b/docs/devops.md @@ -4,30 +4,32 @@ This document describes the deployment and release process for KoalaSync. ## Tag-Based Releases -KoalaSync uses a fully automated release pipeline triggered by Git tags. +KoalaSync uses a gated release pipeline triggered by immutable Git tags. > [!IMPORTANT] -> **DO NOT** manually bump the version numbers in any files (such as `package.json`, `manifest.base.json`, `shared/constants.js`, etc.) before creating a release. -> Bumping versions manually is redundant, leads to conflicts, and is completely handled by the CI/CD pipeline. +> **DO NOT** edit individual version files or tag an unmerged branch. Run +> `npm run prepare:release -- MAJOR.MINOR.PATCH` on a branch, review all generated +> source changes, and merge them through a pull request with successful CI. ### How it Works -When you push a Git tag matching `v*` (e.g., `v2.5.1`), the GitHub Actions release workflow (`.github/workflows/release.yml`) is triggered. The workflow performs the following actions: +When an annotated tag matching exact `vMAJOR.MINOR.PATCH` is pushed, the GitHub +Actions workflow performs these ordered gates: -1. **Extracts the version** from the tag (e.g., `2.5.1` from `v2.5.1`). -2. **Injects the version** automatically into the following files: - - `extension/manifest.base.json` - - `shared/constants.js` (updates `APP_VERSION`) - - `package.json` - - `package-lock.json` (root package metadata) - - `website/version.json` - - `website/template.html` (updates `softwareVersion` schema) - - `README.md` (updates badge and announcement banner) - - `website/sitemap.xml` (updates `lastmod` dates) -3. **Commits and pushes** these version updates back to the `main` branch automatically with the commit message `chore(release): update versions to vX.X.X [skip ci]`. -4. **Builds the extension** for both Chrome and Firefox and publishes the zipped archives with a `SHA256SUMS` checksum file and signed provenance attestations. -5. **Builds the website** and uploads website artifacts. -6. **Builds and publishes** the Docker image for the relay server to the GitHub Container Registry (`ghcr.io`). +1. Confirms that the tag is annotated, points exactly at current `origin/main`, + and matches every committed version source. +2. Requires successful `verify`, `node20`, and `e2e` checks for that commit. +3. Re-runs release verification, cross-browser E2E, and an unpublished relay + container smoke test. +4. Builds and locally validates Chrome/Firefox archives, checksums, AMO output, + website output, archive parity, and manifests. +5. Creates an attested **draft** GitHub Release. +6. Publishes the multi-architecture relay image, verifies both platforms, + attestation identity, digest, tag source, and a running health check. +7. Makes the GitHub Release public only after every preceding gate succeeds. + +The release workflow never writes to `main` and never derives shell code from a +tag. Version changes must pass normal branch protection first. --- @@ -35,18 +37,29 @@ When you push a Git tag matching `v*` (e.g., `v2.5.1`), the GitHub Actions relea To release a new version (e.g., `v2.5.1`), follow these steps: -1. Make sure your local repository is synced on `main`: +1. Create a release-preparation branch from current `main` and update every + version source atomically: ```bash git checkout main git pull origin main + git checkout -b release/v2.5.1 + npm run prepare:release -- 2.5.1 + npm run verify ``` -2. Create a local Git tag: +2. Commit the release notes and prepared version changes, open a pull request, + and wait for required `verify`, `node20`, and `e2e` checks. +3. After the PR is merged, fast-forward local `main` and create an **annotated** + tag on that exact commit: ```bash - git tag v2.5.1 + git checkout main + git pull --ff-only origin main + git tag -a v2.5.1 -m "Release v2.5.1" ``` -3. Push the tag to GitHub: +4. Verify the tag target, then push it once: ```bash + test "$(git rev-parse v2.5.1^{commit})" = "$(git rev-parse origin/main)" git push origin v2.5.1 ``` -The release pipeline will take care of the rest! You can monitor the progress under the **Actions** tab of the GitHub repository. +Never reuse or move a published tag. Monitor every release job and verify both +the public GitHub assets and GHCR digest before calling the release complete. diff --git a/extension/host-access.test.mjs b/extension/host-access.test.mjs index 82d5e2c..f3767b0 100644 --- a/extension/host-access.test.mjs +++ b/extension/host-access.test.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { HOST_ACCESS_REQUIRED_STATUS, addTabHostAccessRequest, @@ -16,6 +16,8 @@ import { const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); describe('host access helpers', () => { + afterEach(() => vi.useRealTimers()); + it('normalizes only positive safe tab IDs', () => { expect(HOST_ACCESS_REQUIRED_STATUS).toBe('host_permission_required'); for (const invalid of [null, undefined, '', 0, true, [42], '42.5', Number.MAX_SAFE_INTEGER + 1]) { @@ -38,6 +40,11 @@ describe('host access helpers', () => { }); expect(describeTabUrl('chrome://extensions/')).toBeNull(); expect(describeTabUrl('not a url')).toBeNull(); + expect(describeTabUrl('file:///Users/koala/movie.mp4')).toEqual({ + url: 'file:///Users/koala/movie.mp4', + host: 'local file', + originPattern: 'file:///*' + }); }); it('checks the selected tab origin and preserves an unknown callback result', async () => { @@ -112,9 +119,43 @@ describe('host access helpers', () => { }; await expect(requestOriginPermission(callbackChrome, 'https://video.example/*')).resolves.toBe(true); await expect(requestOriginPermission({ permissions: {} }, 'https://video.example/*')).resolves.toBeNull(); + await expect(requestOriginPermission(callbackChrome, '')).resolves.toBeNull(); + await expect(requestOriginPermission({ + permissions: { request: async () => { throw new Error('denied'); } } + }, 'https://video.example/*')).resolves.toBe(false); + await expect(addTabHostAccessRequest({ + permissions: { addHostAccessRequest: async () => { throw new Error('denied'); } } + }, 42)).resolves.toBe(false); + await expect(removeTabHostAccessRequest({ + permissions: { removeHostAccessRequest: async () => { throw new Error('denied'); } } + }, 42)).resolves.toBe(false); expect(isHostAccessError(new Error('Missing host permission for the tab'))).toBe(true); expect(isHostAccessError(new Error('No tab with id: 42'))).toBe(false); }); + + it('treats permission inspection errors and timeouts as advisory unknowns', async () => { + const base = { + tabs: { get: async () => ({ url: 'https://video.example/watch' }) } + }; + await expect(inspectTabHostAccess({ + ...base, + permissions: { contains: async () => { throw new Error('permission API failed'); } } + }, 42)).resolves.toMatchObject({ granted: null }); + + await expect(inspectTabHostAccess({ + ...base, + runtime: { lastError: { message: 'permission callback failed' } }, + permissions: { contains: (_request, callback) => callback(false) } + }, 42)).resolves.toMatchObject({ granted: null }); + + vi.useFakeTimers(); + const pending = inspectTabHostAccess({ + ...base, + permissions: { contains: () => undefined } + }, 42); + await vi.advanceTimersByTimeAsync(1000); + await expect(pending).resolves.toMatchObject({ granted: null }); + }); }); describe('host access recovery contracts', () => { diff --git a/extension/popup.html b/extension/popup.html index 8a8035e..05af8db 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -1413,16 +1413,16 @@ -
- - - - - +
+ + + + +
-
+
@@ -1510,7 +1510,7 @@
-
+