mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(client,sdk): the smaller silent failures, and a dead SDK test file
The SDK's parity suite had an unescaped apostrophe in an it() title, so the file did not parse and vitest reported it as "no tests". Both guards in it were dead - including the byte-identical egress-policy check the package's own notes call the worst possible bug in that module to lose. No CI job runs the SDK suite, so it landed quietly; the next npm publish would have failed on it, since prepublishOnly runs the tests. Also in the SDK: the dev preview answered trek:geolocation without checking the geolocation:read grant, so a plugin whose manifest is missing the permission works perfectly in dev and is dead once installed. That is the lenient direction, which is the one that ships broken plugins. Client side: the background-task poll never dismissed a 404'd import job and spun forever, "Update All" collapsed its busy flag after the first plugin settled, the lightbox had no cancellation so a slow signed URL rendered the wrong image, the plugin geolocation watch was torn down by any unrelated settings write, manual date entry accepted 31 February, cover previews were never revoked, and check-all issued one request per packing item.
This commit is contained in:
@@ -1862,3 +1862,43 @@ describe('AdminPluginsPanel signature fingerprints and dismissals', () => {
|
||||
await waitFor(() => expect(uploaded).toBe(false))
|
||||
})
|
||||
})
|
||||
|
||||
// FE-W5PLG-030 — Update all drives the same single-slot busy flag as a per-row update,
|
||||
// so the requests have to go out one after another.
|
||||
describe('AdminPluginsPanel — update all', () => {
|
||||
const updatableRow = (over: Record<string, unknown> = {}) => plugin({
|
||||
id: 'a-widget', name: 'Alpha Widget', type: 'widget', version: '1.0.0', operatorEgress: false, ...over,
|
||||
})
|
||||
|
||||
it('FE-W5PLG-030: Update all sends the second update only after the first came back', async () => {
|
||||
const started: string[] = []
|
||||
let release = () => {}
|
||||
const gate = new Promise<void>(resolve => { release = resolve })
|
||||
server.use(
|
||||
http.get('*/api/admin/plugins', () => HttpResponse.json({
|
||||
enabled: true, devLink: false,
|
||||
plugins: [updatableRow(), updatableRow({ id: 'b-widget', name: 'Beta Widget' })],
|
||||
})),
|
||||
http.get('*/api/admin/plugins/registry', () => HttpResponse.json([
|
||||
{ id: 'a-widget', name: 'Alpha Widget', author: 'acme', description: 'd', type: 'widget', latest: '2.0.0' },
|
||||
{ id: 'b-widget', name: 'Beta Widget', author: 'acme', description: 'd', type: 'widget', latest: '2.0.0' },
|
||||
])),
|
||||
http.post('*/api/admin/plugins/:id/update', async ({ params }) => {
|
||||
started.push(String(params.id))
|
||||
if (started.length === 1) await gate
|
||||
return HttpResponse.json({ version: '2.0.0', activated: true, newPermissions: [], newEgress: [] })
|
||||
}),
|
||||
)
|
||||
render(<AdminPluginsPanel />)
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /update all/i }))
|
||||
|
||||
await waitFor(() => expect(started).toHaveLength(1))
|
||||
// Give a parallel dispatch every chance to show up before releasing the first.
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(started).toEqual(['a-widget'])
|
||||
|
||||
release()
|
||||
await waitFor(() => expect(started).toEqual(['a-widget', 'b-widget']))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -641,7 +641,7 @@ export default function AdminPluginsPanel() {
|
||||
|
||||
const runUpdate = (p: PluginRow) => {
|
||||
setBusy(p.id); setMenu(null)
|
||||
adminApi.pluginUpdate(p.id)
|
||||
return adminApi.pluginUpdate(p.id)
|
||||
.then((r: { version: string; activated: boolean; newPermissions: string[]; newEgress: string[] }) => {
|
||||
if (r.activated || (r.newPermissions.length === 0 && r.newEgress.length === 0)) toast.success(t('admin.plugins.updated'))
|
||||
else setConsentQueue(qq => [...qq, { plugin: p, version: r.version, newPermissions: r.newPermissions, newEgress: r.newEgress }])
|
||||
@@ -676,6 +676,12 @@ export default function AdminPluginsPanel() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const updatable = useMemo(() => plugins.filter(updateAvailable), [plugins, latest])
|
||||
|
||||
// One after another: busy is a single slot, so parallel updates would leave the
|
||||
// other rows clickable mid-install and refresh once per plugin.
|
||||
const updateAll = async () => {
|
||||
for (const p of updatable) await runUpdate(p)
|
||||
}
|
||||
|
||||
// Installed list after search / type / status filters + sort.
|
||||
const shownInstalled = useMemo(() => {
|
||||
const term = q.trim().toLowerCase()
|
||||
@@ -865,7 +871,7 @@ export default function AdminPluginsPanel() {
|
||||
<div className="mx-1.5 sm:mx-3 mb-2 mt-1 flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl bg-warning-soft border border-warning/30">
|
||||
<ArrowUpCircle size={16} className="text-warning shrink-0" />
|
||||
<span className="text-xs text-content-secondary">{t('admin.plugins.updatesAvailable', { count: updatable.length })}</span>
|
||||
<button onClick={() => updatable.forEach(runUpdate)}
|
||||
<button onClick={() => void updateAll()}
|
||||
className="ml-auto text-xs font-semibold px-3 py-1.5 rounded-lg bg-warning text-white hover:opacity-90 transition-opacity">
|
||||
{t('admin.plugins.updateAll')}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user