generated from coulomb/repo-seed
Add release smoke path for pre-store verification (WP-0002)
Introduce npm run smoke with automated build, test, lint, dist, and metadata-only compliance checks. Document manual Chrome steps in RELEASE_SMOKE.md and fix unused imports blocking lint.
This commit is contained in:
107
RELEASE_SMOKE.md
Normal file
107
RELEASE_SMOKE.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Release Smoke Checklist
|
||||||
|
|
||||||
|
Repeatable verification before Chrome Web Store submission. Run after every release candidate build.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs all **automated** checks (build, test, lint, dist verification, metadata-only compliance) and prints the **manual** Chrome steps below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automated checks
|
||||||
|
|
||||||
|
| Step | Command | Pass criteria |
|
||||||
|
|------|---------|---------------|
|
||||||
|
| Type-check | `tsc --noEmit` | No TypeScript errors |
|
||||||
|
| Lint | `eslint src/**/*.{js,ts}` | No lint errors |
|
||||||
|
| Unit tests | `jest` | All tests pass (includes metadata-only compliance) |
|
||||||
|
| Production build | `webpack --mode production` | `dist/` created without errors |
|
||||||
|
| Dist artifacts | `scripts/release-smoke.sh` | manifest, background, popup, tracking, icons present |
|
||||||
|
| MV3 manifest | smoke script | `manifest_version: 3`, required permissions |
|
||||||
|
| Metadata-only | smoke script + `tests/metadata-only.test.ts` | No PDF bytes persisted to `chrome.storage` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Chrome verification
|
||||||
|
|
||||||
|
Requires Chrome desktop and Binect test credentials.
|
||||||
|
|
||||||
|
### 1. Load unpacked extension
|
||||||
|
|
||||||
|
1. Run `npm run build` (or `npm run smoke` which builds automatically).
|
||||||
|
2. Open `chrome://extensions/`.
|
||||||
|
3. Enable **Developer mode**.
|
||||||
|
4. Click **Load unpacked** and select the `dist/` directory.
|
||||||
|
5. Confirm BinectChrome appears with version from `public/manifest.json`.
|
||||||
|
6. Accept any new permission prompts.
|
||||||
|
|
||||||
|
**Pass:** Extension loads without service-worker registration errors. Click **service worker** — console shows `BinectChrome service worker loaded`.
|
||||||
|
|
||||||
|
### 2. Trigger PDF detection
|
||||||
|
|
||||||
|
**Recommended (viewer detection):**
|
||||||
|
|
||||||
|
1. Open: `https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf`
|
||||||
|
2. Click the BinectChrome toolbar icon.
|
||||||
|
|
||||||
|
**Pass:** Popup shows `dummy.pdf`, source domain, and **Send PDF to Binect** button.
|
||||||
|
|
||||||
|
**Optional (download detection):**
|
||||||
|
|
||||||
|
1. Download: `https://www.africau.edu/images/default/sample.pdf`
|
||||||
|
2. Check service worker console for `[PDF Detector] PDF detected!`
|
||||||
|
3. Badge shows `1` (when service worker is awake).
|
||||||
|
|
||||||
|
**Pass:** Popup finds the PDF (directly or via recent-downloads fallback).
|
||||||
|
|
||||||
|
### 3. Verify Binect upload (metadata-only path)
|
||||||
|
|
||||||
|
1. Sign in with Binect credentials in the popup.
|
||||||
|
2. Click **Send PDF to Binect**.
|
||||||
|
3. Wait for **Uploading…** → **Success!** with a document ID.
|
||||||
|
|
||||||
|
**Pass:** Upload completes; popup shows document in basket (`in_basket` status).
|
||||||
|
|
||||||
|
### 4. Confirm zero-retention storage
|
||||||
|
|
||||||
|
1. Right-click popup → **Inspect** → **Application** → **Storage** → **Extension Storage**.
|
||||||
|
2. Inspect stored keys:
|
||||||
|
|
||||||
|
| Key | Expected content |
|
||||||
|
|-----|------------------|
|
||||||
|
| `documentProxies` | Filename, URL, size, hash, Binect status — **no PDF bytes** |
|
||||||
|
| `credentials` | Encrypted username/password — **no PDF data** |
|
||||||
|
| `transferTracking` | Timestamp, domain, size, result — **no PDF content** |
|
||||||
|
|
||||||
|
**Pass:** No key contains base64 PDF content or raw byte arrays. Only metadata and encrypted credentials.
|
||||||
|
|
||||||
|
### 5. Tracking page
|
||||||
|
|
||||||
|
1. Click **?** in the popup footer.
|
||||||
|
2. Confirm the transfer appears with timestamp, domain, size, and success/failure.
|
||||||
|
|
||||||
|
**Pass:** Transfer logged locally; CSV export works.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Failure triage
|
||||||
|
|
||||||
|
| Symptom | Check |
|
||||||
|
|---------|-------|
|
||||||
|
| Service worker won't register | `dist/background.js` exists; reload extension |
|
||||||
|
| PDF not detected | Try viewer URL first; check popup console |
|
||||||
|
| Upload fails | Credentials, network, popup Network tab for `api.binect.de` |
|
||||||
|
| Storage has PDF bytes | **Blocker** — violates zero-retention; do not ship |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related docs
|
||||||
|
|
||||||
|
- `QUICK_TEST_GUIDE.md` — detailed debug steps
|
||||||
|
- `DEVELOPMENT.md` — dev workflow and Chrome URLs
|
||||||
|
- `SCOPE.md` — in-scope metadata-only proxy model
|
||||||
|
- `dev-helper.sh verify` — quick dist check without full smoke
|
||||||
@@ -36,6 +36,7 @@ usage() {
|
|||||||
echo " open-chrome - Open Chrome extension management"
|
echo " open-chrome - Open Chrome extension management"
|
||||||
echo " open-sw - Open Chrome service worker internals"
|
echo " open-sw - Open Chrome service worker internals"
|
||||||
echo " verify - Verify dist build is valid"
|
echo " verify - Verify dist build is valid"
|
||||||
|
echo " smoke - Full release smoke (build, test, lint, dist, metadata-only)"
|
||||||
echo " help - Show this help"
|
echo " help - Show this help"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Quick Test-Fix Loop:"
|
echo "Quick Test-Fix Loop:"
|
||||||
@@ -184,6 +185,9 @@ case "${1:-}" in
|
|||||||
verify)
|
verify)
|
||||||
verify
|
verify
|
||||||
;;
|
;;
|
||||||
|
smoke)
|
||||||
|
bash "$(dirname "$0")/scripts/release-smoke.sh"
|
||||||
|
;;
|
||||||
help|--help|-h)
|
help|--help|-h)
|
||||||
usage
|
usage
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"lint": "eslint src/**/*.{js,ts}",
|
"lint": "eslint src/**/*.{js,ts}",
|
||||||
"lint:fix": "eslint src/**/*.{js,ts} --fix",
|
"lint:fix": "eslint src/**/*.{js,ts} --fix",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit",
|
||||||
|
"smoke": "bash scripts/release-smoke.sh"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"chrome-extension",
|
"chrome-extension",
|
||||||
|
|||||||
143
scripts/release-smoke.sh
Executable file
143
scripts/release-smoke.sh
Executable file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Release smoke path — automated pre-flight checks before Chrome manual verification.
|
||||||
|
# Run: npm run smoke or ./scripts/release-smoke.sh
|
||||||
|
#
|
||||||
|
# Automates: install, type-check, lint, test, production build, dist verification,
|
||||||
|
# and static metadata-only compliance checks.
|
||||||
|
# Manual Chrome steps are printed at the end (see RELEASE_SMOKE.md).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
DIST="$ROOT/dist"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
step=0
|
||||||
|
pass() { echo -e "${GREEN}✅ [$((++step))] $1${NC}"; }
|
||||||
|
fail() { echo -e "${RED}❌ $1${NC}"; exit 1; }
|
||||||
|
info() { echo -e "${BLUE} $1${NC}"; }
|
||||||
|
warn() { echo -e "${YELLOW}⚠️ $1${NC}"; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "BinectChrome — Release Smoke (automated)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
# --- Prerequisites ---
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
fail "Node.js is required"
|
||||||
|
fi
|
||||||
|
if ! command -v npm >/dev/null 2>&1; then
|
||||||
|
fail "npm is required"
|
||||||
|
fi
|
||||||
|
if [ ! -d "../binect-js" ]; then
|
||||||
|
fail "Sibling repo ../binect-js not found — required for @binect/js dependency"
|
||||||
|
fi
|
||||||
|
pass "Prerequisites (node, npm, binect-js)"
|
||||||
|
|
||||||
|
# --- Dependencies ---
|
||||||
|
if [ ! -d node_modules ] || [ ! -f node_modules/.bin/jest ]; then
|
||||||
|
info "Installing dependencies..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
pass "Dependencies ready"
|
||||||
|
|
||||||
|
# --- Static analysis ---
|
||||||
|
info "Running type-check..."
|
||||||
|
npm run type-check
|
||||||
|
pass "Type-check"
|
||||||
|
|
||||||
|
info "Running lint..."
|
||||||
|
npm run lint
|
||||||
|
pass "Lint"
|
||||||
|
|
||||||
|
info "Running unit tests..."
|
||||||
|
npm test
|
||||||
|
pass "Unit tests (24+)"
|
||||||
|
|
||||||
|
# --- Production build ---
|
||||||
|
info "Building production dist/..."
|
||||||
|
npm run build
|
||||||
|
pass "Production build"
|
||||||
|
|
||||||
|
# --- Dist verification ---
|
||||||
|
REQUIRED=(
|
||||||
|
"$DIST/manifest.json"
|
||||||
|
"$DIST/background.js"
|
||||||
|
"$DIST/popup.html"
|
||||||
|
"$DIST/popup.js"
|
||||||
|
"$DIST/tracking.html"
|
||||||
|
"$DIST/tracking.js"
|
||||||
|
"$DIST/icons/icon-128.png"
|
||||||
|
"$DIST/_locales/en/messages.json"
|
||||||
|
)
|
||||||
|
for f in "${REQUIRED[@]}"; do
|
||||||
|
[ -f "$f" ] || fail "Missing dist artifact: $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
MANIFEST="$DIST/manifest.json"
|
||||||
|
grep -q '"manifest_version": 3' "$MANIFEST" || fail "manifest.json must be MV3"
|
||||||
|
for perm in downloads storage alarms activeTab; do
|
||||||
|
grep -q "\"$perm\"" "$MANIFEST" || fail "manifest.json missing permission: $perm"
|
||||||
|
done
|
||||||
|
grep -q 'https://api.binect.de' "$MANIFEST" || fail "manifest.json missing Binect API host permission"
|
||||||
|
|
||||||
|
VERSION=$(grep -o '"version": "[^"]*"' "$MANIFEST" | head -1 | cut -d'"' -f4)
|
||||||
|
pass "Dist verification (v${VERSION})"
|
||||||
|
|
||||||
|
# --- Metadata-only compliance (static) ---
|
||||||
|
info "Checking metadata-only storage compliance..."
|
||||||
|
|
||||||
|
# Proxies must not define PDF content fields
|
||||||
|
if grep -qE 'pdfContent|pdfBytes|contentBase64|base64Content' "$ROOT/src/utils/pdf-queue.ts"; then
|
||||||
|
fail "pdf-queue.ts must not store PDF content fields"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# No chrome.storage writes of PDF byte data in src/ (same-line check)
|
||||||
|
STORAGE_OFFENDERS=$(rg -n 'chrome\.storage.*\.set' "$ROOT/src" \
|
||||||
|
| rg 'pdfBytes|pdfData|base64Content|pdfContent' || true)
|
||||||
|
if [ -n "$STORAGE_OFFENDERS" ]; then
|
||||||
|
fail "Source persists PDF bytes to chrome.storage — violates zero-retention"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload path: bytes fetched in-memory only, not written to storage
|
||||||
|
rg -q 'fetchPDFBytes' "$ROOT/src/popup/popup.ts" \
|
||||||
|
|| fail "popup.ts must fetch PDF bytes at send time"
|
||||||
|
rg -q 'uploadPDF' "$ROOT/src/popup/popup.ts" \
|
||||||
|
|| fail "popup.ts must call uploadPDF for Binect dispatch"
|
||||||
|
|
||||||
|
pass "Metadata-only compliance (no PDF persistence in storage)"
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
|
echo -e "${GREEN}Automated smoke checks: ALL PASSED${NC}"
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Dist ready at: $DIST"
|
||||||
|
echo " background.js $(du -h "$DIST/background.js" | cut -f1)"
|
||||||
|
echo " popup.js $(du -h "$DIST/popup.js" | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Manual Chrome verification (required before store submission):${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " 1. Open chrome://extensions/ → Developer mode ON"
|
||||||
|
echo " 2. Load unpacked → select: $DIST"
|
||||||
|
echo " 3. Open test PDF:"
|
||||||
|
echo " https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
|
||||||
|
echo " 4. Click extension icon → confirm PDF detected (filename, domain)"
|
||||||
|
echo " 5. Sign in with Binect credentials → Send PDF to Binect"
|
||||||
|
echo " 6. Confirm upload success and document ID in popup"
|
||||||
|
echo " 7. Open popup DevTools → Application → Storage → Extension Storage"
|
||||||
|
echo " Verify keys are metadata-only (documentProxies, credentials, transferTracking)"
|
||||||
|
echo " — no PDF byte content stored"
|
||||||
|
echo " 8. Open tracking page (?) → confirm transfer logged with size, not content"
|
||||||
|
echo ""
|
||||||
|
echo "Full checklist: RELEASE_SMOKE.md"
|
||||||
|
echo ""
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
import './popup.css';
|
import './popup.css';
|
||||||
import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } from '../utils/storage';
|
import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } from '../utils/storage';
|
||||||
import { uploadPDF, testConnection, BinectAPIError, Document } from '../utils/binect-api';
|
import { uploadPDF, testConnection, BinectAPIError } from '../utils/binect-api';
|
||||||
import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector';
|
import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector';
|
||||||
import { addTrackingEntry } from '../tracking/tracker';
|
import { addTrackingEntry } from '../tracking/tracker';
|
||||||
import { DocumentProxy, PDFQueueEntry, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue';
|
import { DocumentProxy, PDFStatus, PDFStatusMeta } from '../utils/pdf-queue';
|
||||||
import { computeContentFingerprint } from '../utils/hash';
|
import { computeContentFingerprint } from '../utils/hash';
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
|
|||||||
103
tests/metadata-only.test.ts
Normal file
103
tests/metadata-only.test.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Release smoke — metadata-only compliance checks
|
||||||
|
*
|
||||||
|
* Verifies the extension never persists PDF content to chrome.storage.
|
||||||
|
* Run as part of `npm test` and `npm run smoke`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const SRC = path.join(__dirname, '..', 'src');
|
||||||
|
|
||||||
|
const PDF_CONTENT_FIELD_PATTERN =
|
||||||
|
/\b(pdfContent|pdfBytes|contentBase64|base64Content|pdfData)\b/;
|
||||||
|
|
||||||
|
function readSourceFiles(dir: string): string[] {
|
||||||
|
const files: string[] = [];
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...readSourceFiles(full));
|
||||||
|
} else if (entry.name.endsWith('.ts')) {
|
||||||
|
files.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('metadata-only storage compliance', () => {
|
||||||
|
test('DocumentProxy queue does not define PDF content fields', () => {
|
||||||
|
const queueSource = fs.readFileSync(
|
||||||
|
path.join(SRC, 'utils', 'pdf-queue.ts'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
expect(queueSource).not.toMatch(PDF_CONTENT_FIELD_PATTERN);
|
||||||
|
expect(queueSource).toContain('contentHash');
|
||||||
|
expect(queueSource).not.toContain('pdfContent');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DetectedPDF interface is metadata-only', () => {
|
||||||
|
const detectorSource = fs.readFileSync(
|
||||||
|
path.join(SRC, 'utils', 'pdf-detector.ts'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const interfaceMatch = detectorSource.match(
|
||||||
|
/export interface DetectedPDF \{([^}]+)\}/
|
||||||
|
);
|
||||||
|
expect(interfaceMatch).not.toBeNull();
|
||||||
|
const fields = interfaceMatch![1];
|
||||||
|
expect(fields).toMatch(/filename/);
|
||||||
|
expect(fields).toMatch(/url/);
|
||||||
|
expect(fields).toMatch(/size/);
|
||||||
|
expect(fields).not.toMatch(PDF_CONTENT_FIELD_PATTERN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no source file persists PDF bytes via chrome.storage', () => {
|
||||||
|
const offenders: string[] = [];
|
||||||
|
for (const file of readSourceFiles(SRC)) {
|
||||||
|
const content = fs.readFileSync(file, 'utf-8');
|
||||||
|
if (!content.includes('chrome.storage')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lines = content.split('\n');
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (
|
||||||
|
line.includes('chrome.storage') &&
|
||||||
|
line.includes('.set') &&
|
||||||
|
PDF_CONTENT_FIELD_PATTERN.test(line)
|
||||||
|
) {
|
||||||
|
offenders.push(`${path.relative(SRC, file)}:${i + 1}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(offenders).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('upload path fetches bytes in-memory and delegates to Binect API', () => {
|
||||||
|
const popupSource = fs.readFileSync(
|
||||||
|
path.join(SRC, 'popup', 'popup.ts'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
expect(popupSource).toMatch(/fetchPDFBytes/);
|
||||||
|
expect(popupSource).toMatch(/uploadPDF/);
|
||||||
|
expect(popupSource).toMatch(/computeContentFingerprint/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manifest declares required MV3 permissions', () => {
|
||||||
|
const manifest = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(__dirname, '..', 'public', 'manifest.json'),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(manifest.manifest_version).toBe(3);
|
||||||
|
for (const perm of ['downloads', 'storage', 'alarms', 'activeTab']) {
|
||||||
|
expect(manifest.permissions).toContain(perm);
|
||||||
|
}
|
||||||
|
expect(manifest.host_permissions).toEqual(
|
||||||
|
expect.arrayContaining(['https://api.binect.de/*'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,11 +4,11 @@ type: workplan
|
|||||||
title: "Extension release smoke path"
|
title: "Extension release smoke path"
|
||||||
domain: communication
|
domain: communication
|
||||||
repo: binect-chrome
|
repo: binect-chrome
|
||||||
status: ready
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: communication
|
topic_slug: communication
|
||||||
created: "2026-06-22"
|
created: "2026-06-22"
|
||||||
updated: "2026-06-22"
|
updated: "2026-06-24"
|
||||||
state_hub_workstream_id: "085de6b3-4627-4678-a1ce-e03e4cbab445"
|
state_hub_workstream_id: "085de6b3-4627-4678-a1ce-e03e4cbab445"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,9 +20,11 @@ Establish repeatable build, load-unpacked, and PDF-send smoke verification befor
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: BINECT-CHROME-WP-0002-T01
|
id: BINECT-CHROME-WP-0002-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "436e1136-a94e-454e-b8ca-e1e2817e9796"
|
state_hub_task_id: "436e1136-a94e-454e-b8ca-e1e2817e9796"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Result 2026-06-24: Added `npm run smoke`, `scripts/release-smoke.sh`, `RELEASE_SMOKE.md`, and `tests/metadata-only.test.ts`. Automated checks cover build, test, lint, dist verification, and metadata-only compliance; manual Chrome checklist documents load-unpacked, PDF detection, and upload verification.
|
||||||
|
|
||||||
Document and automate smoke steps: build, load `dist/`, trigger PDF detection, verify Binect upload metadata-only path.
|
Document and automate smoke steps: build, load `dist/`, trigger PDF detection, verify Binect upload metadata-only path.
|
||||||
|
|||||||
Reference in New Issue
Block a user