diff --git a/skill/scripts/live/accept-css.mjs b/skill/scripts/live/accept-css.mjs index 749a24e76..e7c241190 100644 --- a/skill/scripts/live/accept-css.mjs +++ b/skill/scripts/live/accept-css.mjs @@ -535,11 +535,20 @@ function removeSelectorAt(source, start, end) { if (braceIdx === -1) return { changed: false, selector, source }; const bodyEnd = scanBlockEnd(source, braceIdx + 1); - // Prelude spans backward from the brace to the previous } ; { or style open. + // Prelude spans backward from the brace to the previous } ; { or the end + // of the `; + const { source, removed } = pruneUnusedSelectors(component, compile); + assert.deepEqual(removed, ['.orphan']); + assert.match(source, /\.wrap > \.item \{ font-weight: bold; \}/); + assert.match(source, /\.wrap \{ padding: 4px; \}/); + const { warnings } = compile(source, { generate: false }); + assert.deepEqual(warnings.filter((w) => w.code === 'css_unused_selector'), []); + }); + + it('removes a fully unused combinator rule without leaving a dangling fragment', () => { + // The corruption shape: after a mid-prelude cut, every remaining fragment + // equals the flagged selector, so the whole-rule branch deleted from the + // cut point and left `.wrap >` dangling in source. + const component = `

x

\n`; + const { source } = pruneUnusedSelectors(component, compile); + assert.doesNotMatch(source, /\.orphan/); + assert.doesNotMatch(source, /\.wrap >\s*\{/, 'no dangling combinator fragment'); + assert.doesNotMatch(source, /\.wrap >\s*$/m, 'no dangling combinator line'); + assert.match(source, /\.wrap \{ padding: 4px; \}/); + const { warnings } = compile(source, { generate: false }); + assert.deepEqual(warnings.filter((w) => w.code === 'css_unused_selector'), []); + }); }); describe('postcondition scanner', () => {