Skip to content

Commit d6f4cfd

Browse files
fix: non-blocking manifest init, fetch timeout, stricter manifest parse
1 parent 6c450fd commit d6f4cfd

6 files changed

Lines changed: 47 additions & 6 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: seed-baselines-local update-baselines check-benchmarks clean-benchmark-artifacts
1+
.PHONY: seed-baselines-local update-baselines gen-tool-types-manifest check-benchmarks clean-benchmark-artifacts
22

33
# WARNING: captures timings on THIS machine. Production baselines must match ubuntu-latest CI.
44
# Prefer downloading benchmark-results.json from a CI artifact, then:

static/js/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ function handleRoute() {
4949

5050
// ==================== Bootstrap ====================
5151

52-
document.addEventListener('DOMContentLoaded', async () => {
52+
document.addEventListener('DOMContentLoaded', () => {
5353
applyTheme(localStorage.getItem('theme') || 'dark');
5454
const yearEl = document.getElementById('footer-year');
5555
if (yearEl) yearEl.textContent = new Date().getFullYear();
56-
await initToolTypesManifest();
56+
void initToolTypesManifest();
5757
handleRoute();
5858
window.addEventListener('hashchange', handleRoute);
5959

static/js/app.test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ describe('router (app.js)', () => {
3636
Element.prototype.scrollIntoView = vi.fn();
3737
await import('./app.js');
3838
document.dispatchEvent(new Event('DOMContentLoaded'));
39-
await Promise.resolve();
4039
});
4140

4241
afterAll(() => {

static/js/render/tool_types_manifest.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import { TOOL_USE_RENDERERS } from './registry.js';
22
import { setManifestToolTypes } from './tool_types_state.js';
33

44
const MANIFEST_URL = '/static/tool_types.json';
5+
const MANIFEST_FETCH_TIMEOUT_MS = 5000;
56

67
/**
78
* Load backend tool-type manifest and cross-check ``TOOL_USE_RENDERERS``.
89
* Logs ``console.warn`` when the backend list and frontend registry diverge.
910
*/
1011
export async function initToolTypesManifest() {
12+
const controller = new AbortController();
13+
const timeoutId = setTimeout(() => controller.abort(), MANIFEST_FETCH_TIMEOUT_MS);
1114
try {
12-
const res = await fetch(MANIFEST_URL);
15+
const res = await fetch(MANIFEST_URL, { signal: controller.signal });
1316
if (!res.ok) {
1417
console.warn(`[tool registry] Could not load ${MANIFEST_URL}: HTTP ${res.status}`);
1518
return;
@@ -34,6 +37,14 @@ export async function initToolTypesManifest() {
3437
}
3538
}
3639
} catch (err) {
40+
if (err?.name === 'AbortError') {
41+
console.warn(
42+
`[tool registry] Could not load ${MANIFEST_URL}: timed out after ${MANIFEST_FETCH_TIMEOUT_MS}ms`,
43+
);
44+
return;
45+
}
3746
console.warn('[tool registry] Could not load tool types manifest:', err);
47+
} finally {
48+
clearTimeout(timeoutId);
3849
}
3950
}

static/js/render/tool_types_manifest.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,29 @@ describe('initToolTypesManifest', () => {
4646
expect.any(Error),
4747
);
4848
});
49+
50+
it('warns when fetch times out', async () => {
51+
vi.useFakeTimers();
52+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
53+
vi.stubGlobal(
54+
'fetch',
55+
vi.fn((_url, init) =>
56+
new Promise((_resolve, reject) => {
57+
init?.signal?.addEventListener('abort', () => {
58+
reject(Object.assign(new Error('aborted'), { name: 'AbortError' }));
59+
});
60+
}),
61+
),
62+
);
63+
64+
const promise = initToolTypesManifest();
65+
await vi.advanceTimersByTimeAsync(5000);
66+
await promise;
67+
68+
expect(getManifestToolTypes()).toBeNull();
69+
expect(warn).toHaveBeenCalledWith(
70+
'[tool registry] Could not load /static/tool_types.json: timed out after 5000ms',
71+
);
72+
vi.useRealTimers();
73+
});
4974
});

tests/test_tool_dispatch_sync.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,13 @@ def _load_manifest_tool_types(path: Path) -> frozenset[str]:
9494
if not isinstance(raw, list):
9595
msg = f"Invalid tool_types in {path}: expected a JSON array"
9696
raise ValueError(msg)
97-
return frozenset(str(t) for t in raw)
97+
for i, item in enumerate(raw):
98+
if not isinstance(item, str):
99+
msg = (
100+
f"Invalid tool_types[{i}] in {path}: expected string, got {type(item).__name__}"
101+
)
102+
raise ValueError(msg)
103+
return frozenset(raw)
98104

99105

100106
def test_tool_types_manifest_matches_known_tool_types() -> None:

0 commit comments

Comments
 (0)