mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] apps: a refused unpublish stops reporting success, so an app that is still live says so (ENG-283)
This commit is contained in:
@@ -137,7 +137,7 @@ const PublishModal: React.FC<Props> = ({ outputId, outputName, open, onClose })
|
||||
try {
|
||||
await unpublishApp(outputId);
|
||||
dispatch(setOutputPublishState({ id: outputId, published_slug: null, published_url: null, publish_status: null }));
|
||||
setToast('App unpublished');
|
||||
setToast('App unpublished. The public link can take a minute or two to go dark.');
|
||||
onClose();
|
||||
} catch (e: any) {
|
||||
setConfirmUnpublish(false);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// A refused takedown is not a takedown (ENG-283).
|
||||
//
|
||||
// /outputs/unpublish answers HTTP 200 with {ok:false,error} whenever the app could not actually be
|
||||
// pulled off the internet: signed out, offline, or the hosting service refused. This helper used to
|
||||
// read only res.ok, so every one of those cases toasted "App unpublished", cleared the card's
|
||||
// publish state, and left the app serving to anyone with the link, with no way to retry. Same class
|
||||
// as ENG-309 one surface over: never let "the request completed" stand in for "the thing happened".
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { unpublishApp } from './publishApi';
|
||||
|
||||
function stubFetch(res: { ok?: boolean; status?: number; body?: unknown; nonJson?: boolean }): () => void {
|
||||
const real = (globalThis as any).fetch;
|
||||
(globalThis as any).fetch = async () => ({
|
||||
ok: res.ok ?? true,
|
||||
status: res.status ?? 200,
|
||||
json: async () => {
|
||||
if (res.nonJson) throw new SyntaxError('Unexpected token');
|
||||
return res.body;
|
||||
},
|
||||
});
|
||||
return () => { (globalThis as any).fetch = real; };
|
||||
}
|
||||
|
||||
async function rejection(fn: () => Promise<unknown>): Promise<Error | null> {
|
||||
try {
|
||||
await fn();
|
||||
return null;
|
||||
} catch (e) {
|
||||
return e as Error;
|
||||
}
|
||||
}
|
||||
|
||||
test('an explicit ok:true resolves', async () => {
|
||||
const restore = stubFetch({ body: { ok: true } });
|
||||
try {
|
||||
await unpublishApp('out-1');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('a 200 that says ok:false throws with the server reason', async () => {
|
||||
const restore = stubFetch({ body: { ok: false, error: 'Sign in to your OpenSwarm account to manage published apps.' } });
|
||||
try {
|
||||
const err = await rejection(() => unpublishApp('out-1'));
|
||||
assert.ok(err, 'a refused takedown must not resolve; the app is still on the internet');
|
||||
assert.match(err!.message, /Sign in/, 'the user needs the actual reason, not a generic failure');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('a body with no ok field is treated as a failure, not a success', async () => {
|
||||
// Only an explicit success counts. Anything else means we do not know that the app came down.
|
||||
for (const body of [{}, { ok: 'yes' }, null, { error: 'boom' }]) {
|
||||
const restore = stubFetch({ body });
|
||||
try {
|
||||
assert.ok(await rejection(() => unpublishApp('out-1')), `ambiguous body ${JSON.stringify(body)} must not read as success`);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('an unparseable body fails closed', async () => {
|
||||
const restore = stubFetch({ nonJson: true });
|
||||
try {
|
||||
assert.ok(await rejection(() => unpublishApp('out-1')));
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('an HTTP error still throws, and says the app may still be live', async () => {
|
||||
const restore = stubFetch({ ok: false, status: 500, body: {} });
|
||||
try {
|
||||
const err = await rejection(() => unpublishApp('out-1'));
|
||||
assert.match(err!.message, /still be live/i);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
@@ -39,11 +39,22 @@ export async function publishApp(
|
||||
return (await res.json()) as PublishResult;
|
||||
}
|
||||
|
||||
/** Only an explicit {ok:true} is a takedown. The route answers HTTP 200 with {ok:false,error} when it
|
||||
* was refused (signed out, offline, cloud error), so reading res.ok alone told the user their app was
|
||||
* off the internet while it kept serving, and cleared the publish state so they could not retry. */
|
||||
export async function unpublishApp(outputId: string): Promise<void> {
|
||||
const res = await fetch(`${OUTPUTS_API}/unpublish`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ output_id: outputId }),
|
||||
});
|
||||
if (!res.ok) throw new Error("We couldn't unpublish this app.");
|
||||
const fallback = "We couldn't unpublish this app. It may still be live.";
|
||||
if (!res.ok) throw new Error(fallback);
|
||||
let body: { ok?: boolean; error?: string } | null = null;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch {
|
||||
throw new Error(fallback);
|
||||
}
|
||||
if (body?.ok !== true) throw new Error(body?.error || fallback);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user