fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors (#1350)

* fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors

* test(sdk,ci): smoke test + explicit sdk-tests CI gate

Smoke test covers the full public surface:
  openComposition → setStyle/setText/dispatch(moveElement) → serialize
  applyPatches + ORIGIN_APPLY_PATCHES tagging
  batch() coalescing + transactional rollback on throw
  undo/redo round-trip
  persist adapter write + persist:error surfacing
  T3 embedded mode: override-set apply on open + getOverrides round-trip

Adds sdk-tests CI job so SDK coverage is explicitly named and required —
prevents a repeat of the demo-next vitest-never-ran incident.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sdk): export adapter types, awaitable flush(), never-coalesce mode

- Export PersistAdapter, PreviewAdapter, PersistVersionEntry from package
  root — callers can now write typed fakes without reaching into internals
- Add flush(): Promise<void> to Composition interface + CompositionImpl —
  app-close handlers can await a clean drain of the persist queue
- coalesceMs <= 0 disables coalescing entirely in createHistory — enables
  deterministic test scenarios without per-entry timestamp manipulation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(sdk): p2 edge cases — setText no-text-node, override-remove non-existent, flush in smoke

- setText on element with no prior text node (firstTextIdx=-1 path)
- applyOverrideSet null removal on non-existent prop is a no-op (no throw)
- smoke persist test uses comp.flush() instead of setTimeout
- can() JSDoc clarifies Phase 3b false-return is intentional feature-detection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: trigger regression suite

* fix(ci): add packages/sdk/package.json to Dockerfile.test workspace copy

bun install --frozen-lockfile fails in the regression Docker build because
the lockfile references the sdk workspace member but its package.json was
not copied into the image before the install step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-06-11 14:07:49 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 69d67f1d69
commit a0ee97210b
14 changed files with 436 additions and 28 deletions
@@ -223,10 +223,35 @@ function isSafeAttributeValue(name: string, value: string): boolean {
return true;
}
// fallow-ignore-next-line complexity
function patchStyleAttrString(style: string, property: string, value: string | null): string {
const props = new Map<string, string>();
const order: string[] = [];
for (const decl of style.split(";")) {
// Tokenize declarations robustly: values can contain ';' inside quoted strings
// (e.g. content: ';') and ':' inside values (data URIs, url(), etc.).
// Split on ';' only when outside quotes and balanced parens; the first ':' in
// the resulting segment is the property/value separator (property names never
// contain ':').
let i = 0;
while (i < style.length) {
let depth = 0;
let inSingle = false;
let inDouble = false;
const start = i;
while (i < style.length) {
const ch = style[i];
if (ch === "'" && !inDouble) inSingle = !inSingle;
else if (ch === '"' && !inSingle) inDouble = !inDouble;
else if (!inSingle && !inDouble) {
if (ch === "(") depth++;
else if (ch === ")") depth = Math.max(0, depth - 1);
else if (ch === ";" && depth === 0) break;
}
i++;
}
const decl = style.slice(start, i).trim();
i++; // advance past ';'
if (!decl) continue;
const colon = decl.indexOf(":");
if (colon < 0) continue;
const key = decl.slice(0, colon).trim();