Repository navigation
fix(tutorial): report a run's start before the project's code runs - #3766
Conversation
The editor emitted didStart only after the runner's run() resolved, that is after startGame and file hashing, while the project's top-level code already runs and prints inside startGame. Courses therefore received editor.runtime.start after the first logs of the same run. ProjectRunner now emits start right before calling startGame, and the editor preview forwards it as didStart. Logs are still forwarded as they are printed. Fixes goplus#3744 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #3766 +/- ##
==========================================
+ Coverage 59.14% 59.95% +0.81%
==========================================
Files 771 772 +1
Lines 27701 27728 +27
Branches 5229 5231 +2
==========================================
+ Hits 16383 16625 +242
+ Misses 9829 9590 -239
- Partials 1489 1513 +24
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Review: report a run's start before the project's code runs
Solid, well-scoped change. Moving emit('start') to just before startGame() in ProjectRunner.vue and relaying it synchronously up to runtime.emit('didStart') correctly upholds the new ordering guarantee (start before any of that run's log output or exit), since logs/exit originate inside the subsequent await startGame(). Tests cover the happy path, the "fails after start" path, and ordering of start/log/exit. The old post-success emit was cleanly removed, and start is a zero-payload signal — no data or secrets flow through it.
Two items below; the rest (performance, security) found nothing.
Reviewed areas: code quality, performance, security, documentation accuracy.
… game ProjectRunner had no tests, and the editor preview's tests use a stubbed runner, so nothing guarded where start is emitted. The new test runs the runner against a fake runner page whose startGame prints a line, and expects start, then startGame, then that line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The didStart documentation now states both sides: a run that fails or is
stopped before the project's code starts emits no start, and one that
fails or is stopped after it keeps the start it emitted. The comment on
emit('start') now reads as a full sentence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #3744.
Summary
A course now receives
editor.runtime.startbefore everyeditor.runtime.logand theeditor.runtime.exitof the same run. Until now, the lines a learner's project printed from its top-level code reachedEditor.Runtime.onLogbeforeEditor.Runtime.onStart, in every run.Where the start was emitted
EditorPreview.executeRunemitteddidStartoncesurface.run()resolved (EditorPreview.vue).run()isProjectRunner.runInternal, which resolves only afterstartGamehas returned and the project's files have been hashed (ProjectRunner.vue).didStartaseditor.runtime.start(program.ts).Why top-level logs arrived first
The project's code does not wait for
startGameto return. It runs inside it:startGamerestarts the engine and then callswindow.ispx_start, a synchronous call into Go.ispxStartstarts the game in a goroutine and returns.handleEvent). The game's bootstrap therefore runs inside that call: the stage's top-level code, then each sprite's from the bottom layer up, each until its first wait.console.log, whichProjectRunnerreplaces (ProjectRunner.vue). The line is emitted aslogright away, appended byEditorPreview.handleLog(EditorPreview.vue), and posted to the course program aseditor.runtime.logas soon as the call returns to JavaScript (program.ts).startGameresolve, the files get hashed, anddidStartget emitted.So a course saw the top-level lines, then the start, then the lines printed in the project's own
onStartcallbacks, which run from the first frame. On the test environment all 10 runs had this order, with the start 0.9–1.5 ms after the first line (details in #3744). A course that resets per-run state inEditor.Runtime.onStarterased what those lines had just recorded. Code: Lita lessons 34, 37–41 and 43 could never be completed because of it.The fix
ProjectRunneremits a newstartevent right before it callsstartGame, the last point before the project's code runs that builder controls.ProjectRunnerSurfaceforwards it, andEditorPreviewemitsdidStarton it instead of afterrun()resolves.didStart, and so foreditor.runtime.start:filesHashare updated whenrun()resolves, as before.startGameemits no start, as before. A run that fails insidestartGamehas already emitted one, so a course can receive a start with no log or exit after it.startGamespends restarting the engine: about 70–90 ms on a rerun on the test environment, and longer on the first run after the page loads.rerun()stops the previous run first, and stopping emits that run's exit, so a course sees the old exit, the new start, then the new run's logs.onStartcallbacks, and the order of the code inside the project, are unchanged.Runtime.didStart,Runtime.OnStartintools/tutorial,module_SpxProjectEditor.ts,module_TutorialFramework.ts,tutorial-class-framework.go,tutorial-lifecycle-implementation.md, and theEditor.Runtime.onStartrow ofcourse-program.md, which goplus/xbuilder-courses syncs into itsplayground-courseskill.Validation
EditorPreview.test.tsandsession.test.tspassed (41 tests). Three cases inEditorPreview.test.tscover the new behavior: a start the runner reports is emitted at once, while still initializing; a run's start comes before its output and exit; and a run that fails after reporting its start keeps it. All three fail without this change.ProjectRunner.test.tsruns the runner against a fake runner page whosestartGameprints a line, and expectsstart, thenstartGame, then that line. It fails withemit('start')moved afterstartGame.vue-tsc --build, ESLint and Prettier passed.TestGuideCoursesBuildintools/xgoexec-bundlepassed.本次局面 甲at the top level, and its sprite prints one line at the top level and one inonStart. The course program clears the situation inEditor.Runtime.onStart, records it from that line inonLog, and shows the order the events arrived in. On this preview all 3 runs kept the situation, in the order start,本次局面 甲, the sprite's top-level line, itsonStartline. On thedevpreview (same course) the runs lost it: the start arrived after both top-level lines.editor.runtime.*events posted to the course program's worker on another staging course, "两朵蘑菇不一样" (11615), showed the start first in all 25 runs on this preview, 50–170 ms before the first line.