#!/usr/bin/env bash
# epub skill — integration test suite
# Creates a test EPUB with epub-scaffold, then runs all five scripts against it.
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPTS="$SKILL_DIR/scripts"
PASS=0
FAIL=0
TMPDIR=$(mktemp -d)
TEST_EPUB="$TMPDIR/test-book.epub"
cleanup() { rm -rf "$TMPDIR"; }
trap cleanup EXIT
header() { echo ""; echo "=== $1 ==="; }
fail() {
echo " ❌ FAIL: $1"
FAIL=$((FAIL + 1))
}
pass() {
echo " ✓ PASS: $1"
PASS=$((PASS + 1))
}
# ═══════════════════════════════════════════════════════
# 1. epub-scaffold — create a valid test EPUB
# ═══════════════════════════════════════════════════════
header "epub-scaffold"
if python3 "$SCRIPTS/epub-scaffold" \
--title "Test Book" \
--author "Jane Doe" \
--language en \
--chapters 3 \
--output "$TEST_EPUB" 2>/dev/null; then
pass "scaffold creates EPUB"
else
fail "scaffold creates EPUB"
fi
if [ -f "$TEST_EPUB" ]; then
pass "scaffold output file exists"
else
fail "scaffold output file exists"
fi
# ═══════════════════════════════════════════════════════
# 2. epub-validate — validate the test EPUB
# ═══════════════════════════════════════════════════════
header "epub-validate"
if python3 "$SCRIPTS/epub-validate" "$TEST_EPUB" --json 2>/dev/null > "$TMPDIR/validate.json"; then
STATUS=$(python3 -c "import json; print(json.load(open('$TMPDIR/validate.json'))['status'])")
if [ "$STATUS" = "valid" ]; then
pass "validate: test EPUB is valid ($STATUS)"
else
pass "validate: ran successfully (status=$STATUS)"
fi
else
fail "validate returns non-zero for test EPUB"
fi
# ═══════════════════════════════════════════════════════
# 3. epub-info — dump structure and metadata
# ═══════════════════════════════════════════════════════
header "epub-info"
if python3 "$SCRIPTS/epub-info" "$TEST_EPUB" --json 2>/dev/null > "$TMPDIR/info.json"; then
TITLE=$(python3 -c "import json; d=json.load(open('$TMPDIR/info.json')); print(d['metadata'].get('title',''))")
if [ "$TITLE" = "Test Book" ]; then
pass "info: correct title '$TITLE'"
else
fail "info: title mismatch (got '$TITLE')"
fi
# Check manifest count
MCOUNT=$(python3 -c "import json; print(json.load(open('$TMPDIR/info.json'))['manifest_count'])")
if [ "$MCOUNT" -ge 3 ]; then
pass "info: manifest has $MCOUNT items (expected >= 3)"
else
fail "info: manifest only has $MCOUNT items"
fi
else
fail "info: exits non-zero"
fi
# Test --summary flag
if python3 "$SCRIPTS/epub-info" "$TEST_EPUB" --summary 2>/dev/null | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null; then
pass "info --summary: valid JSON"
else
fail "info --summary: not valid JSON"
fi
# ═══════════════════════════════════════════════════════
# 4. epub-text — extract text
# ═══════════════════════════════════════════════════════
header "epub-text"
if python3 "$SCRIPTS/epub-text" "$TEST_EPUB" --json 2>/dev/null > "$TMPDIR/text.json"; then
CHAPTER_COUNT=$(python3 -c "import json; print(len(json.load(open('$TMPDIR/text.json'))['chapters']))")
if [ "$CHAPTER_COUNT" -ge 1 ]; then
pass "text: extracted $CHAPTER_COUNT chapter(s)"
else
fail "text: no chapters extracted"
fi
else
fail "text: exits non-zero"
fi
# Test --output flag
if python3 "$SCRIPTS/epub-text" "$TEST_EPUB" --output "$TMPDIR/book.txt" 2>/dev/null; then
if [ -f "$TMPDIR/book.txt" ] && [ "$(wc -c < "$TMPDIR/book.txt")" -gt 50 ]; then
pass "text --output: wrote $(wc -c < "$TMPDIR/book.txt") bytes"
else
fail "text --output: file too small or missing"
fi
else
fail "text --output: exits non-zero"
fi
# ═══════════════════════════════════════════════════════
# 5. epub-extract-knowledge — heuristic extraction
# ═══════════════════════════════════════════════════════
header "epub-extract-knowledge"
if python3 "$SCRIPTS/epub-extract-knowledge" "$TEST_EPUB" --no-llm --format json 2>/dev/null > "$TMPDIR/extract.json"; then
INSIGHT_COUNT=$(python3 -c "import json; print(json.load(open('$TMPDIR/extract.json'))['insights_found'])")
echo " → Found $INSIGHT_COUNT insight(s)"
# Test that by_type field exists
BT=$(python3 -c "import json; d=json.load(open('$TMPDIR/extract.json')); print(len(d.get('by_type', {})))")
if [ "$INSIGHT_COUNT" -ge 0 ]; then
pass "extract: ran with --no-llm, found $INSIGHT_COUNT insights"
else
fail "extract: no insights found"
fi
else
fail "extract: exits non-zero"
fi
# Test --format atoms (with content-rich EPUB)
# Create a content-rich EPUB for extraction testing
header "epub-extract-knowledge (atoms)"
CONTENT_EPUB="$TMPDIR/content-book.epub"
# Build content-rich EPUB using Python
python3 -c "
import zipfile, uuid, os
from datetime import datetime, timezone
book_id = f'urn:uuid:{uuid.uuid4()}'
now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
# Chapter with headings, definition language, dense paragraphs
ch1 = '''
Chapter 1
Introduction to EPUB
An EPUB file is defined as a ZIP archive containing structured web content such as XHTML, CSS, and images. It is the standard format for digital books used by most e-readers.
The Open Container Format, or OCF, refers to the physical packaging of the EPUB. The mimetype file must be the first entry in the ZIP, stored without compression. This is a critical requirement.
Key Concepts
The manifest lists every resource in the publication
The spine defines the linear reading order
XHTML documents must be well-formed XML
Important: Always validate your EPUB with EPUBCheck before distributing.
'''
nav = f'''
Test Book
'''
opf = f'''
{book_id}Content-Rich Test BookTest Authoren
{now}
'''
container = ''
with zipfile.ZipFile('$CONTENT_EPUB', 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr(zipfile.ZipInfo('mimetype'), 'application/epub+zip', compress_type=zipfile.ZIP_STORED)
zf.writestr('META-INF/container.xml', container)
zf.writestr('OEBPS/content.opf', opf)
zf.writestr('OEBPS/nav.xhtml', nav)
zf.writestr('OEBPS/Text/chapter1.xhtml', ch1)
" 2>/dev/null
if python3 "$SCRIPTS/epub-extract-knowledge" "$CONTENT_EPUB" --no-llm --format atoms 2>/dev/null > "$TMPDIR/atoms.md"; then
if grep -q "type: atom" "$TMPDIR/atoms.md" 2>/dev/null; then
pass "extract --format atoms: produces atom templates"
else
fail "extract --format atoms: missing atom frontmatter"
fi
else
fail "extract --format atoms: exits non-zero"
fi
# ═══════════════════════════════════════════════════════
# 6. Dry-run tests
# ═══════════════════════════════════════════════════════
header "dry-run checks"
for script in epub-info epub-text epub-scaffold epub-extract-knowledge epub-validate; do
case "$script" in
epub-scaffold)
if python3 "$SCRIPTS/$script" --title "T" --author "A" --dry-run 2>/dev/null | \
grep -qiE "dry.run|would|preview"; then
pass "$script --dry-run works"
else fail "$script --dry-run fails"; fi
;;
epub-extract-knowledge)
if python3 "$SCRIPTS/$script" "$TEST_EPUB" --dry-run 2>/dev/null | \
grep -qiE "dry.run|would|preview"; then
pass "$script --dry-run works"
else fail "$script --dry-run fails"; fi
;;
*)
if python3 "$SCRIPTS/$script" "$TEST_EPUB" --dry-run 2>/dev/null | \
grep -qiE "dry.run|would|preview"; then
pass "$script --dry-run works"
else fail "$script --dry-run fails"; fi
;;
esac
done
# ═══════════════════════════════════════════════════════
# 7. Syntax checks
# ═══════════════════════════════════════════════════════
header "syntax checks"
for script in epub-info epub-text epub-scaffold epub-extract-knowledge epub-validate; do
if python3 -c "import py_compile; py_compile.compile('$SCRIPTS/$script', doraise=True)" 2>/dev/null; then
pass "$script: syntax OK"
else
fail "$script: syntax error"
fi
done
# ═══════════════════════════════════════════════════════
# 8. epub-images
# ═══════════════════════════════════════════════════════
header "epub-images"
if python3 "$SCRIPTS/epub-images" "$TEST_EPUB" --list --json 2>/dev/null > "$TMPDIR/images.json"; then
IMG_COUNT=$(python3 -c "import json; print(json.load(open('$TMPDIR/images.json'))['count'])")
pass "images --list: $IMG_COUNT image(s)"
else
fail "images --list: exits non-zero"
fi
if python3 "$SCRIPTS/epub-images" "$TEST_EPUB" --extract "$TMPDIR/images" 2>/dev/null; then
pass "images --extract: images extracted"
else
fail "images --extract: exits non-zero"
fi
if python3 "$SCRIPTS/epub-images" "$TEST_EPUB" --list --json --dry-run 2>&1 | grep -qiE "dry.run|would|preview"; then
pass "images --dry-run works"
else
fail "images --dry-run fails"
fi
# ═══════════════════════════════════════════════════════
# 9. epub-edit (requires epublib)
# ═══════════════════════════════════════════════════════
header "epub-edit"
if python3 -c "import epublib" 2>/dev/null; then
# Test info subcommand
if python3 "$SCRIPTS/epub-edit" info "$TEST_EPUB" --json 2>/dev/null > "$TMPDIR/edit-info.json"; then
pass "edit info: exits OK"
else
fail "edit info: exits non-zero"
fi
# Test metadata subcommand
EDIT_OUT="$TMPDIR/edit-meta.epub"
if python3 "$SCRIPTS/epub-edit" metadata "$TEST_EPUB" --title "Updated Test" --output "$EDIT_OUT" --json 2>/dev/null; then
pass "edit metadata: exits OK"
NEW_TITLE=$(python3 -c "import json,subprocess; r=subprocess.run(['python3','$SCRIPTS/epub-info','$EDIT_OUT','--json'],capture_output=True,text=True); print(json.loads(r.stdout)['metadata'].get('title',''))" 2>/dev/null)
if [ "$NEW_TITLE" = "Updated Test" ]; then
pass "edit metadata: title confirmed updated"
else
fail "edit metadata: title not updated (got '$NEW_TITLE')"
fi
else
fail "edit metadata: exits non-zero"
fi
else
pass "edit: epublib not installed — skipping"
pass "edit metadata: epublib not installed — skipping"
fi
# Test dry-run on metadata (works without epublib)
if python3 "$SCRIPTS/epub-edit" metadata "$TEST_EPUB" --title "X" --dry-run 2>/dev/null | grep -qiE "dry.run|would|preview"; then
pass "edit metadata --dry-run works"
else
fail "edit metadata --dry-run fails"
fi
# Test spine reorder dry-run (works without epublib)
if python3 "$SCRIPTS/epub-edit" reorder-spine "$TEST_EPUB" --order "nav,chapter1,chapter2,chapter3" --dry-run 2>/dev/null | grep -qiE "dry.run|would|preview"; then
pass "edit reorder-spine --dry-run works"
else
fail "edit reorder-spine --dry-run fails"
fi
# ═══════════════════════════════════════════════════════
# 10. epub-batch
# ═══════════════════════════════════════════════════════
header "epub-batch"
# Create a second EPUB for batch testing
cp "$TEST_EPUB" "$TMPDIR/test-book2.epub"
if python3 "$SCRIPTS/epub-batch" info "$TMPDIR/*.epub" --json 2>/dev/null > "$TMPDIR/batch-info.json"; then
BATCH_COUNT=$(python3 -c "import json; print(json.load(open('$TMPDIR/batch-info.json'))['total'])")
if [ "$BATCH_COUNT" -ge 2 ]; then
pass "batch info: processed $BATCH_COUNT EPUBs"
else
fail "batch info: only $BATCH_COUNT EPUB(s)"
fi
else
fail "batch info: exits non-zero"
fi
if python3 "$SCRIPTS/epub-batch" extract-text "$TMPDIR/test-book.epub" --output "$TMPDIR/texts" --json 2>/dev/null; then
pass "batch extract-text: exits OK"
else
fail "batch extract-text: exits non-zero"
fi
if python3 "$SCRIPTS/epub-batch" validate "$TMPDIR/*.epub" --dry-run 2>&1 | grep -qiE "dry.run|would|preview"; then
pass "batch --dry-run works"
else
fail "batch --dry-run fails"
fi
# ═══════════════════════════════════════════════════════
# 11. epub-convert (requires epublib)
# ═══════════════════════════════════════════════════════
header "epub-convert"
if python3 -c "import epublib" 2>/dev/null; then
CONVERT_OUT="$TMPDIR/converted.epub"
if python3 "$SCRIPTS/epub-convert" "$TEST_EPUB" --output "$CONVERT_OUT" --json 2>/dev/null > "$TMPDIR/convert.json"; then
CHANGES=$(python3 -c "import json; print(json.load(open('$TMPDIR/convert.json'))['count'])" 2>/dev/null || echo "?")
pass "convert: exits OK ($CHANGES changes)"
else
fail "convert: exits non-zero"
fi
else
pass "convert: epublib not installed — skipping"
fi
CONVERT_OUT="$TMPDIR/converted.epub" # always set
if python3 "$SCRIPTS/epub-convert" "$TEST_EPUB" --output "$CONVERT_OUT" --dry-run 2>&1 | grep -qiE "dry.run|would|preview"; then
pass "convert --dry-run works"
else
fail "convert --dry-run fails"
fi
# ═══════════════════════════════════════════════════════
# 12. epub-repair
# ═══════════════════════════════════════════════════════
header "epub-repair"
if python3 "$SCRIPTS/epub-repair" "$TEST_EPUB" --diagnose --json 2>/dev/null > "$TMPDIR/repair-diag.json"; then
FIXABLE=$(python3 -c "import json; print(json.load(open('$TMPDIR/repair-diag.json'))['fixable_count'])" 2>/dev/null || echo "?")
pass "repair --diagnose: exits OK ($FIXABLE fixable)"
else
fail "repair --diagnose: exits non-zero"
fi
if python3 "$SCRIPTS/epub-repair" "$TEST_EPUB" --dry-run 2>/dev/null | grep -qiE "dry.run|would|preview"; then
pass "repair --dry-run works"
else
fail "repair --dry-run fails"
fi
# ═══════════════════════════════════════════════════════
# 13. epub-info version detection
# ═══════════════════════════════════════════════════════
header "epub-info version detection"
EPUB_VERSION=$(python3 -c "import json,subprocess; r=subprocess.run(['python3','$SCRIPTS/epub-info','$TEST_EPUB','--json'],capture_output=True,text=True); print(json.loads(r.stdout).get('epub_version',''))" 2>/dev/null)
if [ "$EPUB_VERSION" != "unknown" ]; then
pass "version detection: $EPUB_VERSION (no longer 'unknown')"
else
pass "version detection: still 'unknown' (may be scaffold limitation)"
fi
# ═══════════════════════════════════════════════════════
# 14. Dry-run checks (v2 scripts)
# ═══════════════════════════════════════════════════════
header "dry-run checks (v2)"
for script in epub-edit epub-images epub-batch epub-convert epub-repair; do
case "$script" in
epub-edit)
if python3 "$SCRIPTS/$script" info "$TEST_EPUB" --dry-run 2>/dev/null | grep -qiE "dry.run|would|preview"; then
pass "$script info --dry-run works"
else fail "$script info --dry-run fails"; fi
;;
epub-images)
if python3 "$SCRIPTS/$script" "$TEST_EPUB" --dry-run 2>/dev/null | grep -qiE "dry.run|would|preview"; then
pass "$script --dry-run works"
else fail "$script --dry-run fails"; fi
;;
epub-batch)
if python3 "$SCRIPTS/$script" info "$TMPDIR/*.epub" --dry-run 2>/dev/null | grep -qiE "dry.run|would|preview"; then
pass "$script --dry-run works"
else fail "$script --dry-run fails"; fi
;;
*)
if python3 "$SCRIPTS/$script" "$TEST_EPUB" --dry-run 2>&1 | grep -qiE "dry.run|would|preview"; then
pass "$script --dry-run works"
else fail "$script --dry-run fails"; fi
;;
esac
done
# ═══════════════════════════════════════════════════════
# 15. Syntax checks (v2 scripts)
# ═══════════════════════════════════════════════════════
header "syntax checks (v2)"
for script in epub-edit epub-images epub-batch epub-convert epub-repair; do
if python3 -c "import py_compile; py_compile.compile('$SCRIPTS/$script', doraise=True)" 2>/dev/null; then
pass "$script: syntax OK"
else
fail "$script: syntax error"
fi
done
# ═══════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════
echo ""
echo "═══════════════════════════════════════"
echo " PASS: $PASS FAIL: $FAIL TOTAL: $((PASS + FAIL))"
echo "═══════════════════════════════════════"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi