Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
cc2da42
feat(tutorial): tailor leave-editor copy and route success modal to s…
Ethanlita Jun 23, 2026
c3b510a
feat(tutorial): skip leave confirmation for course navigation
Ethanlita Jun 23, 2026
d22a085
fix(tutorial): time-bound the leave-confirm skip to avoid a stuck flag
Ethanlita Jun 23, 2026
8fda240
refactor(tutorial): address review feedback on success modal
Ethanlita Jun 24, 2026
c2a7a2e
refactor(editor): introduce editor-owned leave-confirm extension point
Ethanlita Jun 25, 2026
546eebd
feat(editor): add Copilot in-editor code guides
Ethanlita Jun 26, 2026
c2e14a7
feat(editor): let copilot narrow the API references panel during tuto…
Ethanlita Jul 6, 2026
626212a
refactor(tutorial): scope leave-confirm skip to success-modal navigation
Ethanlita Jul 6, 2026
ee3d06d
refactor(editor): unify leave-confirmation copy and drop message over…
Ethanlita Jul 7, 2026
d4da68d
Merge PR #3305 (feat/tutorial-leave-and-series-nav) into tutorial-ref…
Ethanlita Jul 9, 2026
657a8ae
Merge PR #3304 (feat/api-reference-filter) into tutorial-refractor
Ethanlita Jul 9, 2026
08a248c
Merge PR #3314 (feat/copilot-code-guide) into tutorial-refractor
Ethanlita Jul 9, 2026
394bf8f
chore: remove accidentally committed node_modules artifact
Ethanlita Jul 9, 2026
27229d4
feat(tutorial): focused editor workspace with copilot-controlled hidi…
Ethanlita Jul 9, 2026
6786223
feat(tutorial): show course title in the navbar course entry dropdown
Ethanlita Jul 9, 2026
b5393e7
feat(tutorial): course restart action & story video URL override
Ethanlita Jul 9, 2026
6a41e44
feat(tutorial): course opening sequence, success evaluation & no copy…
Ethanlita Jul 9, 2026
a95fbe9
feat(tutorial): capability-first copilot guidance blocks
Ethanlita Jul 9, 2026
ddd2248
fix(copilot): keep the context message within the backend length limit
Ethanlita Jul 9, 2026
34fc962
feat(tutorial): constrain guide-modal to one short plain-text sentenc…
Ethanlita Jul 9, 2026
b6ce117
feat(tutorial): gradual intervention ladder & knowledge-point videos …
Ethanlita Jul 9, 2026
0f98adc
feat(tutorial): silent course opening & silence-first example dialogue
Ethanlita Jul 9, 2026
889f14f
feat(tutorial): forbid setup narration in the course-opening reply
Ethanlita Jul 10, 2026
c9a2d16
feat(copilot): respect an explicit collapse — user events no longer a…
Ethanlita Jul 10, 2026
122b5c7
feat(tutorial): docked copilot at the bottom-right corner & auto-play…
Ethanlita Jul 10, 2026
1422de6
feat(tutorial): periodic auto perception while nothing of the copilot…
Ethanlita Jul 10, 2026
e721aad
fix(tutorial): auto perception treats invisible-element replies as no…
Ethanlita Jul 10, 2026
9b6725d
feat(tutorial): auto-play each API video once per session & block-loo…
Ethanlita Jul 10, 2026
e54611a
feat(tutorial): typed user messages always deserve a response
Ethanlita Jul 10, 2026
372222b
fix(copilot): a stay-silent decision hides the whole reply, even with…
Ethanlita Jul 10, 2026
3e88a01
fix(tutorial): empty setup-element attributes are no-ops instead of r…
Ethanlita Jul 10, 2026
27eb318
fix(copilot): docked panel is never dimmed by the floating out-of-bou…
Ethanlita Jul 10, 2026
8564d6d
feat(tutorial): demo explainer video everywhere & API hover cards pla…
Ethanlita Jul 10, 2026
8e03a40
feat(tutorial): course authors declare the story video in the course …
Ethanlita Jul 10, 2026
13bd5a2
feat(tutorial): explicit intervention levels & prompt completion chec…
Ethanlita Jul 10, 2026
5cdbe73
feat(tutorial): remind the copilot of completion & setup rules every …
Ethanlita Jul 10, 2026
b5e2bf9
feat(tutorial): intervention level gates which guidance tools exist, …
Ethanlita Jul 13, 2026
db85ceb
feat(tutorial): course author declares workspace setup, not the copilot
Ethanlita Jul 13, 2026
449bd65
feat(tutorial): jsonc config hides panels, copilot narrows the API li…
Ethanlita Jul 13, 2026
e3bdf6d
feat(tutorial): perceive the user through real editor events, drop th…
Ethanlita Jul 13, 2026
4539027
fix(tutorial): align protocol text with the intervention-level gating
Ethanlita Jul 16, 2026
1cb7cda
feat(tutorial): drive the intervention level from model-reported prog…
Ethanlita Jul 16, 2026
4024572
fix(tutorial): reconcile stay-silent with the progress verdicts
Ethanlita Jul 16, 2026
5e828ce
feat(copilot): keep the user's message last, protect critical context…
Ethanlita Jul 16, 2026
b099f89
feat(copilot): persist proactive spotlights, keep hidden copilot running
Ethanlita Jul 16, 2026
fb441ae
feat(editor): hide the API-reference category sidebar when a filter i…
Ethanlita Jul 16, 2026
494ecbc
docs(tutorial): record the runtime course-verification recipe
Ethanlita Jul 16, 2026
7965cca
fix(copilot): don't surface batched-away cancellations; show tutorial…
Ethanlita Jul 16, 2026
83f3766
feat(copilot): scrollable history that hides silent rounds, auto-grow…
Ethanlita Jul 16, 2026
2808683
feat(copilot): show the user's own messages, keep the chat to the con…
Ethanlita Jul 16, 2026
89de719
feat(tutorial): show the guidance level in the course menu; end the o…
Ethanlita Jul 16, 2026
d631f85
fix(tutorial): wake the copilot on game output, not just errors; ters…
Ethanlita Jul 17, 2026
a24ab64
feat(tutorial): start the course copilot collapsed, running in the ba…
Ethanlita Jul 17, 2026
f81a5a8
feat(tutorial): a ruler for measuring distances on the stage
Ethanlita Jul 17, 2026
7dc42d5
feat(tutorial): the ruler reads turn angles
Ethanlita Jul 17, 2026
b7c923f
feat(tutorial): background-first topics; courses can start with the c…
Ethanlita Jul 17, 2026
59f4b35
feat(tutorial): real hosted explainer video for step
Ethanlita Jul 17, 2026
37ae05b
feat(tutorial): programmatic course-code runner for dev verification
Ethanlita Jul 17, 2026
5087b6a
feat(tutorial): load local .xbp in the course runner, and drive it ac…
Ethanlita Jul 17, 2026
75ab737
docs(tutorial): what the rebuild adds, and what the Code: Lita series…
Ethanlita Jul 20, 2026
1306725
docs(tutorial): 教程文档改用中文
Ethanlita Jul 20, 2026
b51356e
docs(tutorial): English version of the change-surface overview
Ethanlita Jul 20, 2026
4fd806d
Refactor tutorial structure and intervention design
Ethanlita Jul 20, 2026
b33edbe
docs(tutorial): sync the English overview with the reworked Chinese one
Ethanlita Jul 20, 2026
4aa41e5
fix: fill focused stage with project backdrop
qingqing-ux Jul 22, 2026
7b5090d
Revert "fix: fill focused stage with project backdrop"
qingqing-ux Jul 22, 2026
78107eb
feat(tutorial): simplify the focused-mode editor per the design
Ethanlita Jul 21, 2026
6badae4
feat(tutorial): explanatory-only API hover card in focused mode
Ethanlita Jul 21, 2026
7407d50
feat(tutorial): focused-mode 1:2.5:3 columns and a single run/stop co…
Ethanlita Jul 21, 2026
ebc5586
feat(tutorial): a control center listing the whole series
Ethanlita Jul 21, 2026
c163dac
feat(tutorial): instant completion via a runtime sentinel, comment async
Ethanlita Jul 21, 2026
fe636c4
feat(tutorial): teach the copilot the two completion paths
Ethanlita Jul 21, 2026
f19994e
fix(tutorial): anchor the sprite-name label to its transform box
Ethanlita Jul 22, 2026
dc8f5c1
feat(tutorial): align the focused editor controls with the redesign
Ethanlita Jul 22, 2026
6f6c312
fix(tutorial): align control center, ruler, and copilot with the design
Ethanlita Jul 22, 2026
7e3ae48
fix(tutorial): pill-style navbar entry and series-page return
Ethanlita Jul 22, 2026
bebbce8
refactor(copilot): editor-owned docked shell over a shared chat body
Ethanlita Jul 22, 2026
0d30675
perf(tutorial): apply course-start setup locally instead of via copilot
Ethanlita Jul 22, 2026
cb85603
feat(tutorial): configurable completion signal for code-judged courses
Ethanlita Jul 22, 2026
c809a9d
fix(tutorial): load tutorial videos in CORS mode for COEP pages
Ethanlita Jul 23, 2026
983664e
chore(tutorial): point the demo video fallback at the hosted step clip
Ethanlita Jul 23, 2026
86e2827
feat(tutorial): allow course story videos from the tutorial asset host
Ethanlita Jul 23, 2026
48b5e0a
feat(tutorial): gate the input helper by type in block style
Ethanlita Jul 23, 2026
eda1535
docs(tutorial): refresh what's-new for the latest branch state
Ethanlita Jul 23, 2026
9ba694f
feat(tutorial): author-ordered course opening with spotlight steps
Ethanlita Jul 23, 2026
a4d870a
fix(tutorial): deliver the completion comment despite event supersession
Ethanlita Jul 23, 2026
5dfcc84
fix(copilot): drop ambient events while a copilot artifact is on screen
Ethanlita Jul 24, 2026
fe57277
feat(editor): content-fitting, drag-resizable API panel in block style
Ethanlita Jul 24, 2026
f7fce6a
feat(editor): keep the ruler in place while running; align the run stage
Ethanlita Jul 24, 2026
e8a1c3e
feat(tutorial): direction slots keep their picker; terser prelude button
Ethanlita Jul 24, 2026
4415264
feat(tutorial): real turn video, and a ruler video the copilot can play
Ethanlita Jul 24, 2026
6e55435
fix(tutorial): size video dialogs to the video instead of pinning 16:9
Ethanlita Jul 24, 2026
986ec19
Merge remote-tracking branch 'upstream/dev' into tutorial-refractor
Ethanlita Jul 29, 2026
84c4ff7
feat(tooling): add xbcs-package skill for course-package surgery
Ethanlita Jul 30, 2026
d453725
fix(tutorial): keep the opening in the editor, and the progress a fra…
Ethanlita Jul 30, 2026
8252cd3
feat(editor): click the selected sprite's name label to insert it in …
Ethanlita Jul 30, 2026
6d343ea
feat(tutorial): match the video modal's player to the hover-card style
Ethanlita Jul 30, 2026
d42bbf7
docs(tutorial): sync whats-new with the latest branch work
Ethanlita Jul 30, 2026
5342413
feat(tutorial): hover-card-style player for the story video too
Ethanlita Jul 31, 2026
85100b9
fix(tutorial): tolerate course payloads without `references`
Ethanlita Jul 31, 2026
054edae
feat(skill): catch sprites whose animations consume every costume
Ethanlita Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions .claude/skills/xbcs-package/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
name: xbcs-package
description: Unpack, edit, validate, and repack XBuilder course-series packages (`.xbcs.zip`) and the project files inside them (`.xbp`). Use this whenever the user mentions a course package, `.xbcs`, `.xbp`, 课程包, exporting or importing a course series, a course-package upload that failed or errored, a course that looks wrong after import, or wants to change a course's prompt / config / stage / assets and hand a package back for upload. Reach for it before repacking anything — rebuilding a `.xbp` with `zip -r`, Finder's Compress, or any GUI archiver silently corrupts it in a way the importer cannot survive, and the bundled script is the only safe way to rebuild one.
---

# Course-series packages (`.xbcs.zip`)

A course series round-trips through a single archive: **export → edit offline → import**. Editing
happens outside the app because a series is 30-odd courses × 40-odd files each, and nobody is going
to click through that. The archive is therefore the real authoring surface, and the importer trusts
it almost completely — there is one format check and then everything is taken at face value.

That trust is why this skill exists. A malformed archive does not get rejected with a helpful
message; it either dies mid-import having already overwritten half the projects, or imports
"successfully" into something broken. So: **always validate before handing a package back**, and
never rebuild a `.xbp` with a general-purpose zip tool.

## The trap, and why it matters

`xbpHelpers.load` ([spx-gui/src/models/common/xbp.ts](../../../spx-gui/src/models/common/xbp.ts))
turns **every zip entry** into a project file:

```ts
Object.keys(unzipped).map(async (path) => { ... files[path] = file })
```

The app's exporter builds `.xbp` from an in-memory file map via fflate, so it emits file entries and
nothing else. `zip -r`, Finder's Compress, WinRAR, Keka, 360压缩 and friends walk a real filesystem
and additionally write **directory entries** — `assets/`, `assets/sprites/Lita/` — which the loader
happily ingests as project files with a path ending in `/`, zero bytes, and an empty `File.name`.
Each one is then uploaded to storage and written into the project's file index. A 31-course package
picks up ~330 of them. That is the failure mode behind "the designer's upload doesn't work".

Two fingerprints tell you a `.xbp` was repacked by a filesystem tool: directory entries, and the
UTF-8 filename flag (general-purpose bit 11) missing on non-ASCII names. The app forces UTF-8
decoding so the flag alone is harmless, but it confirms the diagnosis.

`scripts/xbcs.py pack` writes only file entries, in the paths the manifest declares. Use it.

## Workflow

```bash
# 1. explode the archive — every .xbp becomes a directory you can edit
python3 .claude/skills/xbcs-package/scripts/xbcs.py unpack "in.xbcs.zip" work/

# 2. see what you're dealing with before touching anything
python3 .claude/skills/xbcs-package/scripts/xbcs.py inspect work/

# 3. edit files under work/ with normal tools

# 4. rebuild — reads the manifest and emits exactly the declared paths
python3 .claude/skills/xbcs-package/scripts/xbcs.py pack work/ "out.xbcs.zip"

# 5. prove it before handing it over
python3 .claude/skills/xbcs-package/scripts/xbcs.py validate "out.xbcs.zip"
```

`validate` also accepts an unpacked directory, so you can check your edits before packing. Run it on
the **input** too when you're diagnosing someone else's package — that is usually the whole job.

The unpacked layout mirrors the archive, with each `projects/N.xbp` exploded into `projects/N/`:

```
work/
├── course-series.json # the manifest: series meta + courses[] + projects[]
├── thumbnails/course-series.jpeg
├── thumbnails/courses/0.jpg # index = position in courses[]
└── projects/0/ # was projects/0.xbp
├── builder-meta.json
├── main.spx, Lita.spx, … # the code the learner sees
└── assets/… # index.json + costumes/sounds/backdrops
```

`pack` derives each `.xbp` path from `projects[].path` in the manifest rather than from the directory
listing, so a stray folder can't silently become a project and a renamed folder fails loudly instead
of producing a package that's missing an entry.

## What you can and can't change

Most edits are safe. These are the ones that bite:

- **`course-series.json` is the source of truth for paths.** If you add, remove, or reorder courses,
the `courses[].thumbnail.path` and `projects[].path` values must keep matching real entries — the
importer looks them up verbatim, case- and separator-sensitive, and a miss aborts the import.
- **`projects[].fullName` must cover every course's entrypoint project.** The importer rewrites
`/editor/<owner>/<name>/…` entrypoints to point at the newly imported copies, but only for owners
it has a mapping for. An entrypoint whose `<owner>/<name>` is absent from `projects[]` is left
untouched, so the imported course silently points at the *original* owner's project and 404s at
course-start time. `validate` catches this.
- **Costume, animation, sound and sprite names are referenced by the `.spx` code.** `setCostume "萝卜"`
breaks the moment you rename that costume in `index.json`. Rename in both places or neither.
- **`builder-meta.json.type` must be `game`**; anything else is a hard import failure. Keep
`displayName` equal to the manifest's `projects[].name` — a stale one (the usual leftover from
duplicating a project) shows the wrong course number in the editor.
- **Length limits are server-enforced** and surface as an opaque `40001`: course title ≤200, course
prompt ≤4000, series title ≤200, series description ≤400, project name ≤100 and `^[\w-]+$`.
- **The output filename must end in `.xbcs.zip`.** The import file picker filters on that exact
suffix, so a `.zip` or `.xbcs` can't even be selected — an "upload problem" that never reaches any
code.

For the course `prompt` itself — the ```jsonc config block, `opening`, `judge`/`complete`, `apis`,
`<course-story-video>` — follow [docs/product/course-authoring.md](../../../docs/product/course-authoring.md).
That document is the authority; don't restate its rules here.

## The edits that pass validation and still break the course

`validate` reasons about the file. It cannot know whether a course still *works*, and the most
damaging edits are the ones that leave the archive perfectly well-formed. A course is three things
that have to agree — the config, the project the config describes, and the prose the learner reads —
and an edit request usually names only one of them.

`complete.count` is the clearest case. It counts distinct output lines containing `complete.log`
within one run, so it is a claim about the project: setting it to 6 in a course whose stage holds
four carrots means the success dialog can never open, and the prose that says 「收集四个萝卜」 now
contradicts it. Nothing about that is detectable from the manifest.

The same shape recurs elsewhere: narrowing `apis` hides a panel entry the reference answer still
uses; an `opening` spotlight targets a landmark the course's `hide` list just removed; renaming a
sprite orphans an `entrypoint` that names it in its path.

So when an instruction changes one of the three, check the other two and say what you found. Carrying
out the edit and flagging the inconsistency is right; silently "fixing" the project or rewriting the
prose to match is not — which of the three was wrong is the author's call.

## Diagnosing a package someone else built

When a package fails to import, the useful question is not just "what is broken" but "what did they
actually change". Fetch or locate the last known-good export of the same series and compare: manifest
field by field, and each `.xbp`'s file map entry by entry.

That separates the two kinds of difference, which want opposite responses. Intended edits — a new
thumbnail, a reworded prompt — you preserve. Packaging damage — directory entries, dropped UTF-8
flags, `.DS_Store` — you discard wholesale by repacking. Without the comparison it is easy to
"repair" a package into something that quietly drops the author's work, or to preserve junk because
it looked deliberate.

It also answers a question the author will ask: whether this round contained any real change at all.
A package whose only differences are three broken hand-edits and a bad repack has no content to
preserve, and saying so plainly is more useful than handing back a fixed archive.

Know which failures are NOT the package's, and say so early instead of digging deeper into the
archive. The tells: the import succeeded but **every course misbehaves identically** (a bad package
breaks specifically — one course, one asset, one config), or the console shows a TypeError deep in
app code rather than an import error. That shape means the app↔backend contract changed underneath
the data — the backend serving fields the frontend doesn't expect, or omitting ones it requires —
and a `.xbcs` diff cannot explain it. Check the network response for one course against the
frontend's `Course` type before touching the package again; a package that `validate` passes and
that imported cleanly has done its job.

Deeper structure (manifest schema field by field, `.xbp` anatomy, sprite `index.json` layout, the
full list of import failure paths) lives in [references/format.md](references/format.md). Read it
when an edit goes beyond swapping text or images.

## Reading the validator

Output is `ERROR` / `WARN` lines plus a one-line verdict, and the exit code is non-zero when there
is at least one error.

- **ERROR** — the import will fail, or will succeed into something broken. Fix before handing over.
- **WARN** — legal but suspicious: a stale `displayName`, a thumbnail whose extension disagrees with
its actual bytes, an asset extension the app has no MIME for, a zero-byte asset. Judge each on its
merits; some are intentional (empty `main.spx` in a course with no starting code is normal).

When a package fails to import and `validate` is clean, the remaining suspects are not in the file:

- The person importing must be **signed in as the account that owns the series**. The importer looks
up each `projects[].name` under the *signed-in* user; a non-owner gets 31 fresh projects created
under their own account and then a 403 on the final series update.
- Import **overwrites** same-named projects under that account and forces them public, cutting a new
release for each. That is by design, and the confirm dialog is the only warning.
- It is serial: N projects × ~40 file uploads, no transaction and no rollback. A rate limit or a
network blip partway through leaves projects overwritten and courses orphaned. Retrying is safe
(it's idempotent per project), but a partial run is a real state to reason about.

Say which of these you've ruled out rather than just reporting "the file is fine".

## Verifying behavior, not just structure

`validate` proves the package will import. It says nothing about whether a course *works* — whether
the stage lays out right, whether the code the learner is meant to write actually finishes, whether
`complete.log` / `complete.count` match what the project prints.

For that, drive the real engine through the dev-only harness at `/devtools/course-runner` (registered
only under `import.meta.env.DEV`, so local dev server only):

```js
await courseRunner.loadXbp('/0.xbp') // served from spx-gui/public/
const r = await courseRunner.run({ code: { Lita: 'step 100\nturn Right' }, timeoutMs: 15000 })
r.logs // [{ level: 'INFO', msg: '捡到萝卜 Radish', … }]
```

A page session only survives a handful of engine instances — reload between runs rather than
concluding the course broke.
54 changes: 54 additions & 0 deletions .claude/skills/xbcs-package/evals/evals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"skill_name": "xbcs-package",
"notes": "Fixtures live in /Users/lita/lita-workdir/xbcs-package-workspace/fixtures. 'Code_ Lita (designer upload).xbcs.zip' is a 31-course package repacked the way a filesystem zip tool does it (directory entries in every .xbp, a stray .DS_Store) plus three hand-edit faults: course 3's thumbnail path points at a non-existent 'thumbnails/courses/2.PNG', course 5's entrypoint names a project absent from projects[], and course 7's jsonc config block has a syntax error. 'Code_ Lita.xbcs.zip' is the same series, clean.",
"evals": [
{
"id": 0,
"name": "diagnose-and-fix-failed-upload",
"prompt": "设计师改了课程以后上传失败了,包在 /Users/lita/lita-workdir/xbcs-package-workspace/fixtures/Code_ Lita (designer upload).xbcs.zip 。帮我看看问题出在哪里。能修的话就修好,给我一个能直接上传的包,放到 OUTPUT_DIR 。",
"expected_output": "A diagnosis naming directory entries inside the .xbp payloads as the reason the import breaks (each becomes a bogus zero-byte project file), plus the three manifest faults, and a repacked .xbcs.zip in OUTPUT_DIR with all of them fixed and the course content otherwise untouched.",
"files": ["fixtures/Code_ Lita (designer upload).xbcs.zip"],
"assertions": [
"Identifies directory entries inside the .xbp files as the cause of the failed upload",
"Output package exists in OUTPUT_DIR and its filename ends in .xbcs.zip",
"No .xbp in the output contains any directory entry",
"The stray projects/.DS_Store entry is gone from the output",
"Course 3's declared thumbnail path resolves to an entry that exists in the output",
"Course 5's entrypoint project appears in the manifest's projects[] list",
"Course 7's jsonc config block parses as JSON after comment stripping",
"The output still has 31 courses and 31 projects",
"Course prompts and titles are unchanged apart from course 7's syntax fix"
]
},
{
"id": 1,
"name": "edit-course-config-and-repack",
"prompt": "基于 /Users/lita/lita-workdir/xbcs-package-workspace/fixtures/Code_ Lita.xbcs.zip 改两处课程配置:第 13 课的 complete.count 改成 6;第 5 课的 apis 加上 turnTo(原来的 step 和 turn 要保留)。改完重新打包成 OUTPUT_DIR/Code_ Lita.xbcs.zip,我要上传。",
"expected_output": "A repacked archive where course 13's complete.count is 6 and course 5's apis contains step, turn and turnTo, with everything else — including all 31 project payloads — byte-for-byte unchanged and no directory entries introduced.",
"files": ["fixtures/Code_ Lita.xbcs.zip"],
"assertions": [
"Output archive exists at OUTPUT_DIR/Code_ Lita.xbcs.zip",
"Course 13's config block has complete.count == 6",
"Course 5's apis list contains turnTo and still contains step and turn",
"No other course's jsonc config block changed",
"No .xbp in the output contains any directory entry",
"Every project payload's file set and bytes match the input package"
]
},
{
"id": 2,
"name": "replace-thumbnail-and-repack",
"prompt": "设计师重做了第 3 课的封面,新图在 /Users/lita/lita-workdir/xbcs-package-workspace/fixtures/course-3-new-cover.png 。把它换进 /Users/lita/lita-workdir/xbcs-package-workspace/fixtures/Code_ Lita.xbcs.zip 的第 3 课缩略图,重新打包到 OUTPUT_DIR/ 。",
"expected_output": "A repacked archive whose course-3 thumbnail is the new PNG, with the manifest's declared path and the entry's extension both reflecting PNG (the extension is what decides the uploaded MIME type, so leaving it as .jpeg mislabels the file). No dangling old thumbnail entry, nothing else changed.",
"files": ["fixtures/Code_ Lita.xbcs.zip", "fixtures/course-3-new-cover.png"],
"assertions": [
"Output archive exists in OUTPUT_DIR with a .xbcs.zip filename",
"The entry at course 3's declared thumbnail path is byte-identical to course-3-new-cover.png",
"Course 3's declared thumbnail path ends in .png, matching the actual PNG bytes",
"The old thumbnails/courses/2.jpeg entry is not left in the archive unreferenced",
"No other course's thumbnail bytes changed",
"No .xbp in the output contains any directory entry"
]
}
]
}
Loading