fix: harden and de-slop the merged sidebar/chat/settings batch
Post-merge follow-ups for #2740 #2735 #2734 #2690 #2676 #2738 #2684 #2689 #2733 #2739 #2462 #2687 #2736 #2618 #2697, plus three regressions found while reviewing them: - ctrl/cmd+digit while typing no longer switches session tabs (#2503 was still open in practice: the guard only covered the mod+alt surface binding) - Shiki template-call sanitizer now covers every bundled grammar, including the js/ts aliases and embedding grammars; timed-out highlight requests are memoized and no longer cancel unrelated in-flight requests - settings flush on suspend uses keepalive and also fires on Capacitor appStateChange; keeps the selected model persisted across mode switches - remote-only branches fetch before checkout; range helpers fail clearly - git status invalidation now fires for runtime adapters too - settings number inputs and select triggers size in ch so they scale with the interface font - recent-activity timestamps tick from one list-level ticker - Markdown preview find goes through the shared find_in_file keybind with containment, no longer counts its own bar, and debounces observer runs - #2676 reverted; #2524 fixed by fading the sticky header's own background instead of overlaying the content below it - sticky group headers in the model picker and sidebar render again (oc-sticky-fade-scroller class restored after9b9d7069c) - project switcher names are left-aligned again (wrapper lost in26dbc2f30) - tool card quick-open icon is always visible and opens the same line as the expanded card's button - tautological tests replaced or removed; new oxlint findings fixed
This commit is contained in:
@@ -1,38 +1,54 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { bundledLanguages, createHighlighter, type LanguageRegistration } from 'shiki';
|
||||
import { bundledLanguages, type BundledLanguage, type LanguageRegistration } from 'shiki';
|
||||
|
||||
import {
|
||||
hasCatastrophicTemplateCall,
|
||||
isTemplateCallLanguageId,
|
||||
sanitizeTemplateCallGrammar,
|
||||
TEMPLATE_CALL_LANGUAGE_IDS,
|
||||
} from './sanitizeTemplateCallGrammar';
|
||||
import { hasCatastrophicTemplateCall, sanitizeTemplateCallGrammar } from './sanitizeTemplateCallGrammar';
|
||||
|
||||
type BundledLanguageModule = { default: LanguageRegistration[] };
|
||||
|
||||
const loadBundledGrammar = async (id: (typeof TEMPLATE_CALL_LANGUAGE_IDS)[number]): Promise<LanguageRegistration> => {
|
||||
// SAFETY: `id` comes from TEMPLATE_CALL_LANGUAGE_IDS, and every Shiki bundled
|
||||
// language module default-exports its grammar array.
|
||||
const loadBundledGrammars = async (id: BundledLanguage): Promise<LanguageRegistration[]> => {
|
||||
// SAFETY: `id` is a Shiki bundled-language key and every bundled language
|
||||
// module default-exports its grammar array.
|
||||
const mod = (await bundledLanguages[id]()) as BundledLanguageModule;
|
||||
return mod.default[0];
|
||||
return mod.default;
|
||||
};
|
||||
|
||||
describe('sanitizeTemplateCallGrammar', () => {
|
||||
test('detects template-call on bundled JS/TS grammars', async () => {
|
||||
for (const id of TEMPLATE_CALL_LANGUAGE_IDS) {
|
||||
const grammar = await loadBundledGrammar(id);
|
||||
expect(isTemplateCallLanguageId(id)).toBe(true);
|
||||
for (const id of ['javascript', 'typescript', 'jsx', 'tsx'] as const) {
|
||||
const [grammar] = await loadBundledGrammars(id);
|
||||
expect(hasCatastrophicTemplateCall(grammar)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('a bundled alias request yields sanitized grammars too', async () => {
|
||||
// `js` is a separate key in bundledLanguages resolving to the same grammar
|
||||
// module; the worker sanitizes whatever id was requested, so the alias must
|
||||
// come out clean as well.
|
||||
const grammars = await loadBundledGrammars('js');
|
||||
const patched = grammars.map((grammar) => sanitizeTemplateCallGrammar(grammar));
|
||||
|
||||
expect(grammars.some((grammar) => hasCatastrophicTemplateCall(grammar))).toBe(true);
|
||||
expect(patched.some((grammar) => hasCatastrophicTemplateCall(grammar))).toBe(false);
|
||||
});
|
||||
|
||||
test('an embedding grammar carries JS/TS entries that are sanitized as well', async () => {
|
||||
// `vue` ships the JS/TS grammars alongside its own, so gating on the
|
||||
// requested id alone would leave them unpatched.
|
||||
const grammars = await loadBundledGrammars('vue');
|
||||
const affected = grammars.filter((grammar) => hasCatastrophicTemplateCall(grammar));
|
||||
|
||||
expect(affected.length).toBeGreaterThan(0);
|
||||
const patched = grammars.map((grammar) => sanitizeTemplateCallGrammar(grammar));
|
||||
expect(patched.some((grammar) => hasCatastrophicTemplateCall(grammar))).toBe(false);
|
||||
});
|
||||
|
||||
test('clears template-call patterns without dropping the repository key', async () => {
|
||||
const grammar = await loadBundledGrammar('javascript');
|
||||
const [grammar] = await loadBundledGrammars('javascript');
|
||||
const patched = sanitizeTemplateCallGrammar(grammar);
|
||||
|
||||
expect(hasCatastrophicTemplateCall(patched)).toBe(false);
|
||||
expect(patched.repository?.['template-call']).toEqual({ patterns: [] });
|
||||
// Original left intact (structured clone / spread, not mutate-in-place).
|
||||
// Original left intact (spread, not mutate-in-place).
|
||||
expect(hasCatastrophicTemplateCall(grammar)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -45,34 +61,4 @@ describe('sanitizeTemplateCallGrammar', () => {
|
||||
} satisfies LanguageRegistration;
|
||||
expect(sanitizeTemplateCallGrammar(grammar)).toBe(grammar);
|
||||
});
|
||||
|
||||
test('highlights template-literal fixtures within a tight budget after sanitize', async () => {
|
||||
// SAFETY: the javascript bundle default-exports its grammar array.
|
||||
const mod = (await bundledLanguages.javascript()) as BundledLanguageModule;
|
||||
const patched = mod.default.map((grammar) => sanitizeTemplateCallGrammar(grammar));
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ['github-dark'],
|
||||
langs: patched,
|
||||
});
|
||||
|
||||
// Representative content from openchamber/openchamber#2587, scaled to ~14KB.
|
||||
const fixture = `const snapshot = { source: \`\${session.source}\`, fetchedAt: \`\${Date.now()}\` };
|
||||
const label = \`Account \${index + 1}\`;
|
||||
function render(account) {
|
||||
return html\`<div class="\${account.cls}">\${account.name}</div>\`;
|
||||
}
|
||||
`.repeat(80);
|
||||
|
||||
expect(fixture.length).toBeGreaterThan(10_000);
|
||||
|
||||
const started = performance.now();
|
||||
const html = highlighter.codeToHtml(fixture, { lang: 'javascript', theme: 'github-dark' });
|
||||
const elapsedMs = performance.now() - started;
|
||||
highlighter.dispose();
|
||||
|
||||
expect(html.length).toBeGreaterThan(0);
|
||||
// Catastrophic backtracking hangs for seconds–minutes; healthy tokenize is well under 1s.
|
||||
expect(elapsedMs).toBeLessThan(2_000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,11 +35,3 @@ export const sanitizeTemplateCallGrammar = <T extends TemplateCallGrammar>(gramm
|
||||
repository[TEMPLATE_CALL_KEY] = { patterns: [] };
|
||||
return { ...grammar, repository };
|
||||
};
|
||||
|
||||
/** Language ids whose bundled grammars ship the catastrophic `template-call` rule. */
|
||||
export const TEMPLATE_CALL_LANGUAGE_IDS = ['javascript', 'typescript', 'jsx', 'tsx'] as const;
|
||||
|
||||
export type TemplateCallLanguageId = (typeof TEMPLATE_CALL_LANGUAGE_IDS)[number];
|
||||
|
||||
export const isTemplateCallLanguageId = (lang: string): lang is TemplateCallLanguageId =>
|
||||
TEMPLATE_CALL_LANGUAGE_IDS.some((id) => id === lang);
|
||||
|
||||
Reference in New Issue
Block a user