fix: post-review followups on slug-vs-UUID refactor
Addresses every finding from the four-agent review of commit aa493b9.
Dashboard — honor redirect_to on subtab loadApp() (closes the silent
historical-slug redirect UX gap):
- queues/+page.svelte
- queues/[name]/+page.svelte
- files/+page.svelte
- dead-letters/+page.svelte
After a rename, the URL bar now reflects the canonical slug instead
of silently rendering the renamed app's data under the stale URL.
Mirrors the established pattern at apps/[slug]/+page.svelte:619-623.
manager-core:
- queues_api.rs IntoResponse now uses the JSON envelope shape
`{"error": "..."}` consistent with every sibling admin api file.
- triggers_api::delete_trigger reordered: cap check fires BEFORE the
trigger load, closing the 404-vs-403 existence side channel an
unauthorized caller could otherwise probe.
- InMemoryAppRepo mocks in topics_api + triggers_api now implement
get_by_slug + get_by_slug_or_history (previously
`unimplemented!()`), unblocking handler-level slug-input tests.
- Added 4 slug-acceptance tests to topics_api and 2 to triggers_api
(slug-resolves, unknown-slug-404, historical-slug-resolves,
create-via-slug). Also added delete-without-cap-is-forbidden test
pinning the new cap-first order.
e2e:
- navigation/tabs.spec.ts split per-tab so a regression on one tab
no longer masks regressions on the others.
- Negative assertion widened: captures every /api/v1/admin/apps/*
response and fails on any 4xx/5xx — not just the literal "Cannot
parse" string. Catches a broader regression shape.
- networkidle replaced with `expect(<main>).toBeVisible()` —
networkidle is officially discouraged for SPAs and was at risk of
timing out behind the queues auto-refresh.
- Cleanup registration moved BEFORE the create-app API call so a
flaky create still gets swept up.
- Queue drilldown route /queues/[name] now covered.
- Stable `data-testid="queues-empty-state"` replaces fragile
UI-copy substring match for the positive assertion.
- Header comment now spells out what this spec does and doesn't
catch.
docs:
- serverless_cloud_blueprint.md: slug-history described as
"200 OK + redirect_to" JSON envelope rather than "301 redirect"
— matches what apps_api actually implements (SPA can't honor a
mid-tree HTTP redirect).
Unit-test gap (acknowledged): queues_api, files_api, secrets_api,
dead_letters_api have zero in-process tests. Adding them properly
needs a shared mock-repo helper crate — the standalone trait surface
(QueueRepo + TriggerRepo + ScriptRepository + AuthzRepo + repo-
specific) is ~30 methods per file. Documented inline in queues_api.rs
near the resolver. Integration coverage via crates/picloud/tests/ and
the new e2e spec cover the same paths end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { api, ApiError, type App, type DeadLetterRow } from '$lib/api';
|
||||
|
||||
@@ -17,6 +18,10 @@
|
||||
error = null;
|
||||
try {
|
||||
const a = await api.apps.get(slug);
|
||||
if (a.redirect_to && a.redirect_to !== slug) {
|
||||
await goto(`${base}/apps/${a.redirect_to}/dead-letters`, { replaceState: true });
|
||||
return;
|
||||
}
|
||||
app = a;
|
||||
const c = await api.deadLetters.count(slug);
|
||||
unresolved = c.unresolved;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { api, ApiError, type App, type FileMeta } from '$lib/api';
|
||||
import ConfirmModal from '$lib/ConfirmModal.svelte';
|
||||
@@ -22,7 +23,12 @@
|
||||
|
||||
async function loadApp() {
|
||||
try {
|
||||
app = await api.apps.get(slug);
|
||||
const fetched = await api.apps.get(slug);
|
||||
if (fetched.redirect_to && fetched.redirect_to !== slug) {
|
||||
await goto(`${base}/apps/${fetched.redirect_to}/files`, { replaceState: true });
|
||||
return;
|
||||
}
|
||||
app = fetched;
|
||||
} catch (e) {
|
||||
error = e instanceof ApiError ? e.message : String(e);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { api, ApiError, type App, type QueueSummary } from '$lib/api';
|
||||
|
||||
@@ -17,7 +18,15 @@
|
||||
|
||||
async function loadApp() {
|
||||
try {
|
||||
app = await api.apps.get(slug);
|
||||
const fetched = await api.apps.get(slug);
|
||||
// Mirror apps/[slug]/+page.svelte:619-623. Without this, an
|
||||
// operator who bookmarks /admin/apps/oldslug/queues continues
|
||||
// to see the renamed app's queues under the stale URL.
|
||||
if (fetched.redirect_to && fetched.redirect_to !== slug) {
|
||||
await goto(`${base}/apps/${fetched.redirect_to}/queues`, { replaceState: true });
|
||||
return;
|
||||
}
|
||||
app = fetched;
|
||||
} catch (e) {
|
||||
error = e instanceof ApiError ? e.message : String(e);
|
||||
}
|
||||
@@ -93,7 +102,7 @@
|
||||
{#if loading && queues.length === 0}
|
||||
<p class="muted">Loading…</p>
|
||||
{:else if queues.length === 0}
|
||||
<p class="muted">
|
||||
<p class="muted" data-testid="queues-empty-state">
|
||||
No queues in this app yet. A queue is created implicitly the first time a
|
||||
message is enqueued. Register a <code>queue:receive</code> trigger from the
|
||||
Triggers tab to consume messages.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { api, ApiError, type App, type QueueDetail } from '$lib/api';
|
||||
|
||||
@@ -14,7 +15,15 @@
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
app = await api.apps.get(slug);
|
||||
const fetched = await api.apps.get(slug);
|
||||
if (fetched.redirect_to && fetched.redirect_to !== slug) {
|
||||
await goto(
|
||||
`${base}/apps/${fetched.redirect_to}/queues/${encodeURIComponent(name)}`,
|
||||
{ replaceState: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
app = fetched;
|
||||
detail = await api.queues.get(slug, name);
|
||||
} catch (e) {
|
||||
error = e instanceof ApiError ? e.message : String(e);
|
||||
|
||||
Reference in New Issue
Block a user