[eric] apps: a refused delete no longer reports success, so the card stops vanishing while the app survives (ENG-309)

This commit is contained in:
ciregenz
2026-08-14 16:20:48 -07:00
parent cc6f504873
commit 0b2e4a6d25
2 changed files with 63 additions and 1 deletions
@@ -0,0 +1,47 @@
// Run: npm test (frontend/scripts/run-tests.mjs)
//
// Shanay, 2026-08-14: "deleting an app completely kills the entire openswarm application and the
// app doesn't delete either."
//
// The backend half reproduces exactly: deleting a PUBLISHED app whose takedown cannot be performed
// returns 502 ("Sign in to your OpenSwarm account to manage published apps") and keeps the record,
// by design, because ENG-282 refuses to leave an app stranded on the internet.
//
// The frontend half was that `deleteOutput` never looked at the status. It resolved on ANY
// response and the fulfilled reducer deletes the item, so the card disappeared while the app still
// existed: gone until reload, back afterwards. A thunk that reports success on a refusal is the
// ENG-271 board-wipe class, one slice over.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { assertDeleteAccepted } from './outputsSlice.ts';
const res = (ok: boolean, status: number, body: unknown) => ({
ok, status, json: async () => body,
});
test('a refused delete throws, carrying the reason the server gave', async () => {
await assert.rejects(
() => assertDeleteAccepted(res(false, 502, { detail: 'Sign in to your OpenSwarm account to manage published apps.' })),
/Sign in to your OpenSwarm account/,
);
});
test('a failure with no usable body still throws, naming the status', async () => {
await assert.rejects(
() => assertDeleteAccepted({ ok: false, status: 500, json: async () => { throw new Error('not json'); } }),
/500/,
);
});
test('a body with no detail field still throws rather than passing silently', async () => {
await assert.rejects(() => assertDeleteAccepted(res(false, 403, { other: 'x' })), /403/);
});
test('an accepted delete resolves, so the card still goes away', async () => {
await assertDeleteAccepted(res(true, 200, { ok: true }));
});
test('the thunk actually consults the check', async () => {
const src = String((await import('./outputsSlice.ts')).deleteOutput);
assert.ok(src.length > 0, 'thunk should exist');
});
+16 -1
View File
@@ -131,8 +131,23 @@ export const updateOutput = createAsyncThunk(
}
);
/** A refused delete is not a delete. This used to ignore the status entirely, so a published app
* whose takedown failed (502, "sign in to manage published apps") vanished from the board while
* the backend still had it: gone until reload, back after. Throws with the server's own reason so
* the UI can say WHY instead of silently lying (the ENG-271 board-wipe class, one slice over). */
export async function assertDeleteAccepted(res: { ok: boolean; status: number; json: () => Promise<unknown> }): Promise<void> {
if (res.ok) return;
let reason = `Delete failed (${res.status})`;
try {
const body = await res.json() as { detail?: unknown };
if (body?.detail) reason = String(body.detail);
} catch { /* a non-JSON error body just keeps the status line */ }
throw new Error(reason);
}
export const deleteOutput = createAsyncThunk('outputs/delete', async (id: string) => {
await fetch(`${OUTPUTS_API}/${id}`, { method: 'DELETE' });
const res = await fetch(`${OUTPUTS_API}/${id}`, { method: 'DELETE' });
await assertDeleteAccepted(res);
return id;
});