From bc614f481d8decae63c4340710523fdf4b0ecf67 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Fri, 21 Aug 2026 19:24:08 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20username,?= =?UTF-8?q?=20name=20length,=20leaving=20mid-run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **The new-workspace username was never validated.** Step 2 shows the field when the instance does not derive one, but neither the Continue gate nor `planProblem` looked at it. `create_workspace` does not close that hole: `nw.username.ok_or(...)` accepts `Some("")` and never runs the `VALID_USERNAME` check `join_workspace` does, so a cleared field created a workspace whose owner has an empty username, and a digit-first one was stored verbatim. Both now refuse, using the same `validateUsername` the sibling creator has always run. **The name length was unchecked**, so a >50-char name walked through two more steps and failed at create. `WORKSPACE_NAME_MAX_LENGTH` sits next to the id limit and `planProblem` checks it. **Leaving mid-run did not stop the run.** The dialog promised "The import stops where it is. Coming back to this link picks it up again", but navigating away only unmounted the UI: the executor kept going, reached `done`, and called `clearParkedImport()` — so returning to the link tried to create the workspace again and failed with "already exists". Worse, the review drawer's teardown resolved the pending review to `false`, meaning "skip the migrations", and the orphan imported every item without the tables they need. Nothing can abort a request already in flight — `installProject` takes no signal — so `abandon()` stops the run at the next phase boundary and leaves the workspace parked, and the teardown now resolves `'abort'`, which stops the import rather than silently dropping the migrations. Also drops a stale JSDoc above `hubAppIcon` still describing the fetch-and- sanitize implementation that `ea31f73ed3` replaced. Adds the coverage the review asked for: the parking decision at the end of a run, and the two validation gates. Co-Authored-By: Claude Opus 5 (1M context) --- .../lib/components/ImportProjectStep.svelte | 22 ++++++--- frontend/src/lib/hubProject.ts | 18 -------- frontend/src/lib/importWizard/abandon.test.ts | 41 +++++++++++++++++ .../src/lib/importWizard/execution.svelte.ts | 27 +++++++++-- frontend/src/lib/importWizard/plan.test.ts | 46 +++++++++++++++++++ frontend/src/lib/importWizard/plan.ts | 17 ++++++- frontend/src/lib/utils/workspaceId.ts | 3 ++ .../projects/import/+page@(root).svelte | 17 ++++++- 8 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 frontend/src/lib/importWizard/abandon.test.ts diff --git a/frontend/src/lib/components/ImportProjectStep.svelte b/frontend/src/lib/components/ImportProjectStep.svelte index 01b5b2931a..2aef08b6e7 100644 --- a/frontend/src/lib/components/ImportProjectStep.svelte +++ b/frontend/src/lib/components/ImportProjectStep.svelte @@ -71,9 +71,10 @@ >([]) // Bumped per review session so the Monaco editors re-mount with the new SQL. let reviewGeneration = $state(0) - let reviewResolve: ((run: boolean) => void) | undefined + /** `abort` stops the whole import; `false` only skips the migrations. */ + let reviewResolve: ((run: boolean | 'abort') => void) | undefined - function openMigrationReview(migs: ProjectMigration[]): Promise { + function openMigrationReview(migs: ProjectMigration[]): Promise { reviewList = migs.map((m) => ({ datatable_name: m.datatable_name, sql: m.sql, @@ -82,7 +83,7 @@ })) reviewGeneration++ reviewDrawer?.openDrawer() - return new Promise((resolve) => (reviewResolve = resolve)) + return new Promise((resolve) => (reviewResolve = resolve)) } function closeMigrationReview(run: boolean) { // Capture + clear first so the `on:close` fired by closeDrawer() (which would @@ -117,6 +118,9 @@ const runnable = enabled.filter((m) => present.has(m.datatable_name)) if (runnable.length === 0) return [] const run = await openMigrationReview(runnable) + // `abort` is the teardown case: the step is gone, so stop rather than import the + // items without the tables the review was about. + if (run === 'abort') return null if (!run) return [] return reviewList .filter((r) => r.run && r.sql.trim() !== '') @@ -244,15 +248,21 @@ // Deliberately not re-read against `running`: the answer was about leaving, and a // run that finished in the meantime only makes leaving safer. leaveApproved = true + // Stop the run before navigating. Nothing can abort a request already in flight, + // so this stops it at the next phase boundary and keeps the workspace parked, so + // the link the message promises actually resumes instead of failing on create. + execution?.abandon() await goto(to) } finally { askingToLeave = false } } - // If this step is torn down while the review drawer is open, resolve the promise - // the executor is waiting on rather than leaving it pending forever. - $effect(() => () => reviewResolve?.(false)) + // Torn down with the review drawer open, the executor is still awaiting an answer. + // Abort rather than resolve: resolving to `false` means "skip the migrations", which + // would let the orphaned run import every item *without* the tables they need — the + // opposite of leaving it where it was. + $effect(() => () => reviewResolve?.('abort')) const deleteModal = createAsyncConfirmationModal() async function deleteWorkspace() { diff --git a/frontend/src/lib/hubProject.ts b/frontend/src/lib/hubProject.ts index 10294369a0..b58b338f7d 100644 --- a/frontend/src/lib/hubProject.ts +++ b/frontend/src/lib/hubProject.ts @@ -68,24 +68,6 @@ export async function fetchHubProject(slug: string): Promise: the hub's icons are - * `fill="currentColor"`, so inlining them lets the icon follow the page's theme. - * Returns undefined when the hub ships no icon for that slug (it 404s), which is - * the caller's cue to fall back to a placeholder. - * - * Sanitized before it is returned, because the markup is inlined into this - * authenticated origin and the hub is not necessarily ours: `hub_base_url` is an - * instance setting, and the import wizard can be pointed at any hub by URL. A - * hostile or compromised one answering with `` would otherwise run - * script here. SVG profile only — no HTML, and `svg` plus `svgFilters` namespaces. - * - * `style` and `image` are forbidden on top of that profile, because the profile - * allows both and neither is something an icon needs. An inline `Your username in it + {#if usernameProblem && username.trim()} + {usernameProblem} + {/if} {/if} {:else} @@ -467,7 +482,7 @@