fix(data): 增加全局保存失败保护

This commit is contained in:
Script Generator
2026-07-03 19:36:18 +08:00
parent 516a5f1b63
commit b4a3c990b4
17 changed files with 727 additions and 132 deletions

View File

@@ -1,7 +1,14 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { loadServerData, saveServerData, ServerDataConflictError } from './server-data';
import { __resetApiAvailabilityForTests } from './api';
import {
loadServerData,
saveServerData,
ServerDataConflictError,
SERVER_DATA_SAVE_ERROR_EVENT,
type ServerDataSaveErrorDetail,
} from './server-data';
type MockResponse = {
status: number;
@@ -172,3 +179,55 @@ test('server data reuses a fresh cached load for repeated AppData reads', async
globalThis.fetch = originalFetch;
}
});
test('server data save failures dispatch a browser-visible failure event', async () => {
const originalFetch = globalThis.fetch;
const originalWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'window');
const events: Array<CustomEvent<ServerDataSaveErrorDetail>> = [];
__resetApiAvailabilityForTests();
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
dispatchEvent: (event: Event) => {
events.push(event as CustomEvent<ServerDataSaveErrorDetail>);
return true;
},
},
});
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.endsWith('/config/ai')) {
return new Response(JSON.stringify({}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
if (url.endsWith('/data/requirements') && init?.method === 'PUT') {
return new Response(JSON.stringify({ message: 'offline' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
throw new Error(`Unexpected fetch ${init?.method ?? 'GET'} ${url}`);
}) as typeof fetch;
try {
await assert.rejects(() => saveServerData('requirements', []), /offline/);
assert.equal(events.length, 1);
assert.equal(events[0].type, SERVER_DATA_SAVE_ERROR_EVENT);
assert.deepEqual(events[0].detail, {
key: 'requirements',
message: 'offline',
});
} finally {
globalThis.fetch = originalFetch;
__resetApiAvailabilityForTests();
if (originalWindowDescriptor) {
Object.defineProperty(globalThis, 'window', originalWindowDescriptor);
} else {
Reflect.deleteProperty(globalThis, 'window');
}
}
});