import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import path from 'node:path' const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..') const read = (name) => JSON.parse(readFileSync(path.join(EXT_DIR, name), 'utf8')) describe('extension version consistency', () => { // Duplicates check (1) of ci.yml's extension-version job, deliberately. // That job is the gate that can't be bypassed; this spec is the one that // fails in a second on the developer's own CI lane with a readable diff. // The two version strings feed different systems and nothing else reconciles // them: // manifest.json -> what `web-ext sign` signs, so what Firefox installs // (package.json is in --ignore-files, not in the XPI) // package.json -> build.yml's AMO cache key, the ext- release // tag, the XPI filename, and therefore the version // /api/extension/manifest reports to the update prompt it('keeps manifest.json and package.json in lockstep', () => { const manifest = read('manifest.json') const pkg = read('package.json') expect(manifest.version).toBe(pkg.version) }) it('uses a plain dotted numeric version AMO will accept', () => { // AMO rejects exotic version strings, and build.yml embeds this value in a // release tag and a filename — so anything needing escaping breaks the // publish path rather than the extension. expect(read('package.json').version).toMatch(/^\d+(\.\d+)*$/) }) it('declares manifest v3', () => { expect(read('manifest.json').manifest_version).toBe(3) }) it('never lets the CI guard ignore a file that actually ships', () => { // ci.yml's extension-version job skips its bump check for paths it deems // non-shipping. If it excludes something web-ext DOES package, a real // change to shipped code passes the guard unnoticed — precisely the // silent-stale-ship the guard exists to stop. The reverse drift (guard // stricter than web-ext) only costs a needless bump, so it isn't asserted. const ci = readFileSync(path.join(EXT_DIR, '..', '.forgejo', 'workflows', 'ci.yml'), 'utf8') const lint = read('package.json').scripts.lint const after = lint.split('--ignore-files')[1] ?? '' const ignored = new Set( after .split(/\s+/) .filter((tok) => tok && !tok.startsWith('--')) .map((tok) => tok.replace(/^["']|["']$/g, '')) ) expect(ignored.size, 'parsed --ignore-files from the lint script').toBeGreaterThan(0) const guarded = [...ci.matchAll(/:\(exclude\)extension\/(\S+?)'/g)].map((m) => m[1]) expect(guarded.length, 'parsed :(exclude) entries from ci.yml').toBeGreaterThan(0) for (const entry of guarded) { expect(ignored, `ci.yml excludes "${entry}" but web-ext packages it`).toContain(entry) } }) it('lists every background script that exists, in dependency order', () => { // url.js must load BEFORE api.js: api.js calls normalizeApiUrl at // init()-time, and these are classic scripts sharing one scope, so a // reordering here is a runtime ReferenceError with no build-time signal. const scripts = read('manifest.json').background.scripts for (const rel of scripts) { expect(() => readFileSync(path.join(EXT_DIR, rel)), `missing ${rel}`).not.toThrow() } expect(scripts.indexOf('lib/url.js')).toBeLessThan(scripts.indexOf('lib/api.js')) }) })