Skip to content

feat(library): seed bundled starter content into the library on first run - #743

Merged
byrongamatos merged 1 commit into
mainfrom
feat/starter-content-seed
Jul 3, 2026
Merged

byrongamatos merged 1 commit into
mainfrom
feat/starter-content-seed

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

A fresh feedBack install lands on an empty library. This seeds a bundled, public-domain Für Elise (keys) feedpak as starter content so the library isn't empty on first run.

server._seed_builtin_starter_content() copies bundled packs into DLC_DIR/starter/ exactly once, guarded by a marker in CONFIG_DIR. Unlike the always-reseeding diagnostic seed, this is a one-time welcome — if the user deletes the starter song it stays gone. starter/ is deliberately outside the diagnostics/tutorials library carve-out, so the song surfaces as an ordinary library entry.

How

  • Extract the shared symlink-safe, mtime-aware copy loop into _copy_builtin_packs() and route both the diagnostic and starter seeds through it. An update_existing flag keeps the diagnostic's refresh-on-newer-bundle behavior while starter content never overwrites a user's own file. Diagnostic behavior is unchanged (existing tests green).
  • New _write_builtin_pack() writes atomically (temp + os.replace). On POSIX the seed directory is pinned by an O_NOFOLLOW dir fd and every stat/create/replace goes through it, closing parent- and final-name symlink TOCTOUs; a path-based fallback covers platforms without dir_fd.
  • The completion marker is created with O_CREAT|O_EXCL|O_NOFOLLOW (never written through a symlink) and read as an lstat sentinel.
  • Bundled pack committed at content/starter/beethoven-fur_elise.feedpak (public-domain, DMCA-clean; keys arrangement).
  • tests/test_builtin_starter_seed.py covers seed-once, delete-stays-gone, deferral until a DLC is configured, never-overwrite-user-file, non-regular/symlink refusal, mtime preservation, and marker-not-written-on-incomplete-seed.

Desktop bundling of content/starter/ (and a diagnostic-name skew fix) ships in a companion feedBack-desktop PR.

Codex preflight: clean (NO ISSUES).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Starter content is now automatically added to the app’s DLC area on first run, making bundled content available without manual setup.
  • Bug Fixes

    • Improved content seeding to avoid overwriting user files, handle existing folders safely, and prevent content from escaping the intended directory.
    • Starter content and diagnostics bundles now refresh correctly when bundled files are updated, while preserving user changes and retrying if setup is incomplete.

… run

Ship a public-domain Für Elise (keys) feedpak as starter content so a fresh
install isn't an empty library. server._seed_builtin_starter_content() copies
bundled packs into DLC_DIR/starter/ exactly once, guarded by a marker in
CONFIG_DIR — unlike the always-reseeding diagnostic seed, a user who deletes
the starter song does not get it back. `starter/` is deliberately outside the
diagnostics/tutorials library carve-out so the song surfaces as a normal
library entry.

Extract the shared symlink-safe, mtime-aware copy loop into
_copy_builtin_packs() and route both the diagnostic and starter seeds through
it (diagnostic behavior unchanged; existing tests green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 3, 2026 19:29
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Refactors bundled diagnostics-pack seeding in server.py into shared symlink-safe, mtime-aware copy helpers (_copy_builtin_packs, _write_builtin_pack), then adds one-time starter-content seeding into DLC_DIR/starter/ guarded by a marker file, wired into background scan. A new test suite validates starter seeding behavior.

Changes

Builtin Pack Seeding Refactor

Layer / File(s) Summary
Symlink-safe generic copy helpers
server.py
Adds import stat and generic _copy_builtin_packs()/_write_builtin_pack() helpers performing lstat-based checks, symlink/non-regular refusal, mtime-aware refresh, and atomic replace-based writes.
Diagnostics seeding delegation
server.py
_seed_builtin_diagnostic_sloppaks() now delegates to the new generic copy helper, preserving prior skip/error-handling semantics.
One-time starter content seeding
server.py
Adds _seed_builtin_starter_content() seeding DLC_DIR/starter/ once, guarded by a CONFIG_DIR marker, refusing symlinks, retrying on incomplete seeds, and wires the call into _background_scan() before the library scan.
Starter seeding test suite
tests/test_builtin_starter_seed.py
Adds fixtures, helper functions, and nine tests covering creation, mtime preservation, listing carve-out exclusion, one-time/no-reseed behavior, deferred seeding, symlink refusal, non-overwrite of user files, directory collisions, and missing source handling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant BackgroundScan
    participant SeedStarter as _seed_builtin_starter_content
    participant Marker as CONFIG_DIR marker
    participant DLC as DLC_DIR/starter
    participant WritePack as _write_builtin_pack

    BackgroundScan->>SeedStarter: call with dlc dir
    SeedStarter->>Marker: check marker exists
    alt marker missing
        SeedStarter->>DLC: check destination state (lstat)
        alt destination safe and missing
            SeedStarter->>WritePack: copy bundled source atomically
            WritePack->>DLC: os.replace() temp file onto destination
            SeedStarter->>Marker: write marker if all sources present
        else destination unsafe or existing
            SeedStarter->>SeedStarter: skip, leave marker unwritten
        end
    else marker present
        SeedStarter->>SeedStarter: skip seeding
    end
    BackgroundScan->>BackgroundScan: continue library scan
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: first-run seeding of bundled starter content into the library.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/starter-content-seed

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR seeds bundled starter content into the library on first run by adding a one-time “starter content” seeding path alongside the existing diagnostic seed, with an emphasis on symlink-safe, atomic writes and mtime-aware behavior.

Changes:

  • Refactors bundled-pack seeding into a shared _copy_builtin_packs() helper and introduces _write_builtin_pack() for atomic temp+replace writes (with a POSIX dir_fd hardening path).
  • Adds one-time starter content seeding (_seed_builtin_starter_content) guarded by a marker file in CONFIG_DIR, and wires it into _background_scan().
  • Adds a new pytest suite covering starter seed semantics (seed-once, deletion stays gone, no overwrite, symlink/non-regular refusal, mtime preservation, marker behavior).

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
server.py Adds shared bundled-pack copy/write helpers, introduces one-time starter seeding with marker guard, and calls it during background scan.
tests/test_builtin_starter_seed.py Adds automated tests validating the starter content seeding contract and safety behaviors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server.py
Comment on lines +5610 to +5613
if dest_dir.is_symlink():
log.warning("%s: %s is a symlink, skipping all seeding", label, dest_dir.name)
return 0
dest_dir.mkdir(parents=True, exist_ok=True)
Comment thread server.py
Comment on lines +5739 to 5747
# Preserve the bundle mtime (copyfileobj doesn't) so the mtime-based
# refresh check matches the shutil.copy2 fallback path. Best-effort.
try:
shutil.copy2(source, dest)
log.info(
"Builtin diagnostic seed: %s %s -> %s/%s",
action,
source.name,
_BUILTIN_DIAGNOSTIC_SUBDIR,
os.utime(
dest_name,
ns=(src_stat.st_atime_ns, src_stat.st_mtime_ns),
dir_fd=dir_fd,
follow_symlinks=False,
)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_builtin_starter_seed.py (2)

66-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test doesn't exercise the actual carve-out predicate.

This test only asserts a hardcoded literal set ({"diagnostics-builtin", "tutorials-builtin"}), never calling the real _is_excluded_from_library closure from _background_scan(). If that predicate's set changes (e.g., "starter" accidentally gets added, or a rename introduces a mismatch), this test won't catch it — it will still pass because it never touches production code.

Since the predicate is a local closure, consider either extracting it to module scope for direct testability, or at minimum seeding a starter file and asserting via _background_scan()'s library listing that it appears (integration-style check) rather than duplicating the excluded set as a literal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_builtin_starter_seed.py` around lines 66 - 69, The current test is
only checking a hardcoded exclusion literal and never exercises the real library
carve-out logic in _background_scan() or its _is_excluded_from_library
predicate. Update the test to validate behavior through production code by
either extracting _is_excluded_from_library to module scope for direct unit
testing, or by seeding a starter file and asserting it appears in the library
listing returned by _background_scan(), so the test fails if the actual excluded
set changes.

112-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for a per-file destination symlink (only the seed-directory symlink case is tested).

_copy_builtin_packs also refuses to write when the destination file itself is a symlink (dest_islink branch), which is a distinct code path from the symlinked seed-directory case already tested at Line 112. Consider adding a test that pre-creates dest as a symlink (e.g., pointing outside dlc) and asserts it is left untouched and the marker stays unwritten.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_builtin_starter_seed.py` around lines 112 - 166, Add coverage for
the per-file symlink path in _copy_builtin_packs, since only the symlinked
seed-directory case is tested now. Create a test that pre-creates the
destination returned by _dest(...) as a symlink pointing outside the DLC tree,
then call _seed_builtin_starter_content(dlc) and assert the symlink target is
left untouched and the _STARTER_SEED_MARKER is not written. Use the existing
server_mod helpers and keep the new test aligned with the current
symlink/refusal behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_builtin_starter_seed.py`:
- Around line 131-148: The test test_seed_never_overwrites_an_existing_user_file
is missing the same source existence guard used by the sibling starter-seed
tests. Add a source.is_file() check with pytest.skip(...) before calling
_seed_builtin_starter_content so this case skips cleanly when the bundled pack
is absent, and keep the existing assertions on dest and
CONFIG_DIR/_STARTER_SEED_MARKER only for the present-source path.

---

Nitpick comments:
In `@tests/test_builtin_starter_seed.py`:
- Around line 66-69: The current test is only checking a hardcoded exclusion
literal and never exercises the real library carve-out logic in
_background_scan() or its _is_excluded_from_library predicate. Update the test
to validate behavior through production code by either extracting
_is_excluded_from_library to module scope for direct unit testing, or by seeding
a starter file and asserting it appears in the library listing returned by
_background_scan(), so the test fails if the actual excluded set changes.
- Around line 112-166: Add coverage for the per-file symlink path in
_copy_builtin_packs, since only the symlinked seed-directory case is tested now.
Create a test that pre-creates the destination returned by _dest(...) as a
symlink pointing outside the DLC tree, then call
_seed_builtin_starter_content(dlc) and assert the symlink target is left
untouched and the _STARTER_SEED_MARKER is not written. Use the existing
server_mod helpers and keep the new test aligned with the current
symlink/refusal behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03bd6b19-ded4-46ff-a322-56e15bcb4180

📥 Commits

Reviewing files that changed from the base of the PR and between 68e29a8 and 1c88354.

📒 Files selected for processing (3)
  • content/starter/beethoven-fur_elise.feedpak
  • server.py
  • tests/test_builtin_starter_seed.py

Comment on lines +131 to +148
def test_seed_never_overwrites_an_existing_user_file(tmp_path, server_mod):
"""One-time starter seeding must never replace a user's own file at the
destination, even if the bundled pack has a newer mtime."""
import os as _os

dlc = tmp_path / "dlc"
dlc.mkdir()
dest = _dest(server_mod, dlc)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(b"user's own edited pack")
_os.utime(dest, (1_000_000, 1_000_000)) # far older than the bundled source

server_mod._seed_builtin_starter_content(dlc)

assert dest.read_bytes() == b"user's own edited pack" # untouched
# counted as already-present, so the one-time seed considers itself done
assert (server_mod.CONFIG_DIR / server_mod._STARTER_SEED_MARKER).is_file()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing skip guard for absent bundled source, unlike sibling tests.

Every other test in this file guards on source.is_file() and calls pytest.skip(...) when the bundled feedpak isn't present in the checkout (e.g. lines 41-42, 58-59, 78-79). This test omits that guard.

Per _copy_builtin_packs's contract (source snippet), the source.is_file() check runs before the dest_exists/update_existing branch — if the source is missing, the pack is never counted as present, so the marker won't be written here either, and the assertion at Line 147 would fail with a confusing message instead of skipping like its siblings.

🔧 Proposed fix
     dlc = tmp_path / "dlc"
     dlc.mkdir()
+    source = _source(server_mod)
+    if not source.is_file():
+        pytest.skip(f"starter source not present in checkout: {source}")
     dest = _dest(server_mod, dlc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_seed_never_overwrites_an_existing_user_file(tmp_path, server_mod):
"""One-time starter seeding must never replace a user's own file at the
destination, even if the bundled pack has a newer mtime."""
import os as _os
dlc = tmp_path / "dlc"
dlc.mkdir()
dest = _dest(server_mod, dlc)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(b"user's own edited pack")
_os.utime(dest, (1_000_000, 1_000_000)) # far older than the bundled source
server_mod._seed_builtin_starter_content(dlc)
assert dest.read_bytes() == b"user's own edited pack" # untouched
# counted as already-present, so the one-time seed considers itself done
assert (server_mod.CONFIG_DIR / server_mod._STARTER_SEED_MARKER).is_file()
def test_seed_never_overwrites_an_existing_user_file(tmp_path, server_mod):
"""One-time starter seeding must never replace a user's own file at the
destination, even if the bundled pack has a newer mtime."""
import os as _os
dlc = tmp_path / "dlc"
dlc.mkdir()
source = _source(server_mod)
if not source.is_file():
pytest.skip(f"starter source not present in checkout: {source}")
dest = _dest(server_mod, dlc)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(b"user's own edited pack")
_os.utime(dest, (1_000_000, 1_000_000)) # far older than the bundled source
server_mod._seed_builtin_starter_content(dlc)
assert dest.read_bytes() == b"user's own edited pack" # untouched
# counted as already-present, so the one-time seed considers itself done
assert (server_mod.CONFIG_DIR / server_mod._STARTER_SEED_MARKER).is_file()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_builtin_starter_seed.py` around lines 131 - 148, The test
test_seed_never_overwrites_an_existing_user_file is missing the same source
existence guard used by the sibling starter-seed tests. Add a source.is_file()
check with pytest.skip(...) before calling _seed_builtin_starter_content so this
case skips cleanly when the bundled pack is absent, and keep the existing
assertions on dest and CONFIG_DIR/_STARTER_SEED_MARKER only for the
present-source path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants