From c63b39fe6875a1a125c676b8808ff5be7f8394f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:46:30 +0900 Subject: [PATCH 01/19] test(diagnostics): require collaborative editor parity --- ...ativeCwlEditor.writingDiagnostics.test.tsx | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx new file mode 100644 index 00000000..088e61ba --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx @@ -0,0 +1,228 @@ +import { + act, + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { createRef } from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as Y from 'yjs'; +import type { CwlEditorDocumentRevision } from '../documentEnvelopeRevision.js'; +import type { CwlEditorHandle } from '../types.js'; +import type { CwlWritingDiagnostic } from '../writingDiagnostics.js'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; + +function connectDocuments(left: Y.Doc, right: Y.Doc): () => void { + const forward = (update: Uint8Array, origin: unknown) => { + if (origin !== right) Y.applyUpdate(right, update, left); + }; + const reverse = (update: Uint8Array, origin: unknown) => { + if (origin !== left) Y.applyUpdate(left, update, right); + }; + left.on('update', forward); + right.on('update', reverse); + return () => { + left.off('update', forward); + right.off('update', reverse); + }; +} + +function diagnostic( + revision: CwlEditorDocumentRevision, + replacement = 'Omega', +): CwlWritingDiagnostic { + return { + diagnosticId: 'collaborative-diagnostic', + documentRevision: revision, + textProjection: { + id: 'inkspan-prosemirror-text', + version: 1, + }, + selector: { + type: 'TextPositionSelector', + start: 0, + end: 5, + }, + categoryCode: 'clarity', + priority: 'important', + title: 'Clarify the shared request', + explanation: 'Make the shared action explicit.', + suggestedReplacement: replacement, + provenance: { + workflowId: 'email-writing-review', + workflowVersion: '1', + judgePolicyVersion: 'evaluation-only-1', + }, + }; +} + +afterEach(cleanup); + +describe('CollaborativeCwlEditor writing diagnostics', () => { + it('applies an exact-revision replacement through Yjs, converges, and remains undoable', async () => { + const leftDocument = new Y.Doc(); + const rightDocument = new Y.Doc(); + const disconnect = connectDocuments(leftDocument, rightDocument); + const leftRef = createRef(); + const rightRef = createRef(); + const onAction = vi.fn(); + + const renderEditors = (writingDiagnostics?: readonly CwlWritingDiagnostic[]) => ( +
+ + +
+ ); + + const mounted = render(renderEditors()); + await waitFor(() => { + expect(leftRef.current?.getEditor()).toBeTruthy(); + expect(rightRef.current?.getEditor()).toBeTruthy(); + }); + + act(() => leftRef.current!.setValue('

Alpha beta gamma

')); + await waitFor(() => + expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'), + ); + const revision = await rightRef.current!.getDocumentEnvelopeRevision(); + expect(revision).not.toBeNull(); + + mounted.rerender(renderEditors([diagnostic(revision!)])); + const apply = await screen.findByRole('button', { + name: 'Apply suggestion for Clarify the shared request', + }); + expect(apply).toBeEnabled(); + + fireEvent.click(apply); + await waitFor(() => { + expect(rightRef.current!.getHTML()).toContain('Omega beta gamma'); + expect(leftRef.current!.getHTML()).toContain('Omega beta gamma'); + }); + expect(onAction).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'applied', + reasonCode: 'explicit', + diagnosticId: 'collaborative-diagnostic', + resultingDocumentRevision: expect.objectContaining({ + algorithm: 'SHA-256', + }), + }), + ); + expect( + screen.getByRole('region', { name: 'Shared writing guidance' }), + ).toHaveTextContent('0 writing diagnostics'); + + act(() => rightRef.current!.getEditor()!.commands.undo()); + await waitFor(() => { + expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'); + expect(leftRef.current!.getHTML()).toContain('Alpha beta gamma'); + }); + + mounted.unmount(); + disconnect(); + }); + + it('invalidates current diagnostics when a remote Yjs update changes the document', async () => { + const leftDocument = new Y.Doc(); + const rightDocument = new Y.Doc(); + const disconnect = connectDocuments(leftDocument, rightDocument); + const leftRef = createRef(); + const rightRef = createRef(); + + const renderEditors = (writingDiagnostics?: readonly CwlWritingDiagnostic[]) => ( +
+ + +
+ ); + + const mounted = render(renderEditors()); + await waitFor(() => { + expect(leftRef.current?.getEditor()).toBeTruthy(); + expect(rightRef.current?.getEditor()).toBeTruthy(); + }); + act(() => leftRef.current!.setValue('

Alpha beta gamma

')); + await waitFor(() => + expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'), + ); + const revision = await rightRef.current!.getDocumentEnvelopeRevision(); + expect(revision).not.toBeNull(); + + mounted.rerender(renderEditors([diagnostic(revision!)])); + await waitFor(() => + expect( + screen.getByRole('region', { name: 'Remote-safe guidance' }), + ).toHaveTextContent('1 writing diagnostics'), + ); + + act(() => leftRef.current!.insertValue('

Remote edit

')); + await waitFor(() => + expect( + screen.getByRole('region', { name: 'Remote-safe guidance' }), + ).toHaveTextContent('0 writing diagnostics'), + ); + await expect( + rightRef.current!.applyWritingDiagnostic('collaborative-diagnostic'), + ).resolves.toBeNull(); + expect(rightRef.current!.getHTML()).toContain('Remote edit'); + expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'); + + mounted.unmount(); + disconnect(); + }); + + it('keeps collaborative replacement actions disabled and inert in read-only mode', async () => { + const document = new Y.Doc(); + const editorRef = createRef(); + const mounted = render( + , + ); + await waitFor(() => expect(editorRef.current?.getEditor()).toBeTruthy()); + act(() => editorRef.current!.setValue('

Alpha beta gamma

')); + const revision = await editorRef.current!.getDocumentEnvelopeRevision(); + expect(revision).not.toBeNull(); + const before = editorRef.current!.getHTML(); + + mounted.rerender( + , + ); + const apply = await screen.findByRole('button', { + name: 'Apply suggestion for Clarify the shared request', + }); + expect(apply).toBeDisabled(); + await expect( + editorRef.current!.applyWritingDiagnostic('collaborative-diagnostic'), + ).resolves.toBeNull(); + expect(editorRef.current!.getHTML()).toBe(before); + }); +}); From 6be60ab156e3f042fe1a51250dc52985539b9b37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:46:49 +0900 Subject: [PATCH 02/19] ci(diagnostics): add collaborative parity TDD lane --- .../writing-diagnostics-collaboration-tdd.yml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/writing-diagnostics-collaboration-tdd.yml diff --git a/.github/workflows/writing-diagnostics-collaboration-tdd.yml b/.github/workflows/writing-diagnostics-collaboration-tdd.yml new file mode 100644 index 00000000..14f57381 --- /dev/null +++ b/.github/workflows/writing-diagnostics-collaboration-tdd.yml @@ -0,0 +1,51 @@ +name: Writing Diagnostics Collaboration TDD + +on: + push: + branches: + - feat/writing-diagnostics-collaboration + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: writing-diagnostics-collaboration-tdd-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + collaborative-diagnostics: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Run collaborative writing-diagnostic parity tests + run: >- + pnpm exec vitest run + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx + --pool=forks + --maxWorkers=1 + - name: Typecheck collaborative writing-diagnostic contracts + run: pnpm typecheck + - name: Run complete production coverage gate + env: + NODE_OPTIONS: --max-old-space-size=6144 + run: pnpm coverage + - name: Build all package entrypoints + run: pnpm build + - name: Verify isolated packed-package consumers + run: pnpm verify:package + - name: Build demonstration application + run: pnpm build:demo From cf48a4bdf9ee988bff87e2015cb4ceb0db3bdf8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:49:17 +0900 Subject: [PATCH 03/19] ci(diagnostics): integrate collaborative guidance once --- ...aborative-diagnostics-integration-once.yml | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 .github/workflows/collaborative-diagnostics-integration-once.yml diff --git a/.github/workflows/collaborative-diagnostics-integration-once.yml b/.github/workflows/collaborative-diagnostics-integration-once.yml new file mode 100644 index 00000000..f4fdf43f --- /dev/null +++ b/.github/workflows/collaborative-diagnostics-integration-once.yml @@ -0,0 +1,135 @@ +name: Collaborative Diagnostics Integration Once + +on: + push: + branches: + - feat/writing-diagnostics-collaboration + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: collaborative-diagnostics-integration-once-${{ github.ref }} + cancel-in-progress: true + +jobs: + integrate-collaborative-diagnostics: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: true + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Integrate writing diagnostics into collaborative editor + run: | + python <<'PY' + from pathlib import Path + + def replace_once(source: str, old: str, new: str, label: str) -> str: + count = source.count(old) + if count != 1: + raise SystemExit(f'{label} anchor count was {count}') + return source.replace(old, new, 1) + + path = Path('src/collaboration/CollaborativeCwlEditor.tsx') + source = path.read_text(encoding='utf-8') + + old = """import { useEditorHandle } from '../components/useEditorHandle.js'; + import { useLatestRef } from '../components/useLatestRef.js'; + """ + new = """import { useEditorHandle } from '../components/useEditorHandle.js'; + import { useLatestRef } from '../components/useLatestRef.js'; + import { useWritingDiagnosticsController } from '../components/useWritingDiagnosticsController.js'; + import { WritingDiagnosticsPanel } from '../components/WritingDiagnosticsPanel.js'; + """ + source = replace_once(source, old, new, 'diagnostic imports') + + old = """ ariaInvalid, + ariaRequired, + } = props; + """ + new = """ ariaInvalid, + ariaRequired, + writingDiagnostics, + onWritingDiagnosticAction, + onWritingDiagnosticsError, + writingDiagnosticsLabel, + printWritingDiagnostics, + } = props; + """ + source = replace_once(source, old, new, 'diagnostic props') + + old = """ useEditorHandle(ref, editor, modeRef); + + useEffect(() => { + """ + new = """ const writingDiagnosticsController = useWritingDiagnosticsController({ + editor, + diagnostics: writingDiagnostics, + onAction: onWritingDiagnosticAction, + onError: onWritingDiagnosticsError, + }); + + useEditorHandle(ref, editor, modeRef, writingDiagnosticsController); + + useEffect(() => { + """ + source = replace_once(source, old, new, 'controller and handle') + + old = """ onFormReset={editor && onFormReset ? handleFormReset : undefined} + status={status} + /> + """ + new = """ onFormReset={editor && onFormReset ? handleFormReset : undefined} + status={status} + writingDiagnosticsPanel={ + writingDiagnostics === undefined ? undefined : ( + { + void writingDiagnosticsController.applyDiagnostic( + diagnosticId, + ); + } + : undefined + } + printEnabled={printWritingDiagnostics} + /> + ) + } + /> + """ + source = replace_once(source, old, new, 'diagnostic panel') + path.write_text(source, encoding='utf-8') + PY + - name: Verify collaborative diagnostic integration + run: | + pnpm exec vitest run \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx \ + --pool=forks --maxWorkers=1 + pnpm typecheck + - name: Publish validated integration and remove this workflow + env: + TARGET_BRANCH: feat/writing-diagnostics-collaboration + run: | + set -euo pipefail + rm .github/workflows/collaborative-diagnostics-integration-once.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add \ + src/collaboration/CollaborativeCwlEditor.tsx \ + .github/workflows/collaborative-diagnostics-integration-once.yml + git diff --cached --check + git commit -m 'feat(diagnostics): integrate collaborative guidance' + git push origin "HEAD:${TARGET_BRANCH}" From 5b57c20d30c478d8daf9cd859542bc0dadab17b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:49:51 +0000 Subject: [PATCH 04/19] feat(diagnostics): integrate collaborative guidance --- ...aborative-diagnostics-integration-once.yml | 135 ------------------ src/collaboration/CollaborativeCwlEditor.tsx | 34 ++++- 2 files changed, 33 insertions(+), 136 deletions(-) delete mode 100644 .github/workflows/collaborative-diagnostics-integration-once.yml diff --git a/.github/workflows/collaborative-diagnostics-integration-once.yml b/.github/workflows/collaborative-diagnostics-integration-once.yml deleted file mode 100644 index f4fdf43f..00000000 --- a/.github/workflows/collaborative-diagnostics-integration-once.yml +++ /dev/null @@ -1,135 +0,0 @@ -name: Collaborative Diagnostics Integration Once - -on: - push: - branches: - - feat/writing-diagnostics-collaboration - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: collaborative-diagnostics-integration-once-${{ github.ref }} - cancel-in-progress: true - -jobs: - integrate-collaborative-diagnostics: - runs-on: ubuntu-24.04 - timeout-minutes: 20 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: true - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Integrate writing diagnostics into collaborative editor - run: | - python <<'PY' - from pathlib import Path - - def replace_once(source: str, old: str, new: str, label: str) -> str: - count = source.count(old) - if count != 1: - raise SystemExit(f'{label} anchor count was {count}') - return source.replace(old, new, 1) - - path = Path('src/collaboration/CollaborativeCwlEditor.tsx') - source = path.read_text(encoding='utf-8') - - old = """import { useEditorHandle } from '../components/useEditorHandle.js'; - import { useLatestRef } from '../components/useLatestRef.js'; - """ - new = """import { useEditorHandle } from '../components/useEditorHandle.js'; - import { useLatestRef } from '../components/useLatestRef.js'; - import { useWritingDiagnosticsController } from '../components/useWritingDiagnosticsController.js'; - import { WritingDiagnosticsPanel } from '../components/WritingDiagnosticsPanel.js'; - """ - source = replace_once(source, old, new, 'diagnostic imports') - - old = """ ariaInvalid, - ariaRequired, - } = props; - """ - new = """ ariaInvalid, - ariaRequired, - writingDiagnostics, - onWritingDiagnosticAction, - onWritingDiagnosticsError, - writingDiagnosticsLabel, - printWritingDiagnostics, - } = props; - """ - source = replace_once(source, old, new, 'diagnostic props') - - old = """ useEditorHandle(ref, editor, modeRef); - - useEffect(() => { - """ - new = """ const writingDiagnosticsController = useWritingDiagnosticsController({ - editor, - diagnostics: writingDiagnostics, - onAction: onWritingDiagnosticAction, - onError: onWritingDiagnosticsError, - }); - - useEditorHandle(ref, editor, modeRef, writingDiagnosticsController); - - useEffect(() => { - """ - source = replace_once(source, old, new, 'controller and handle') - - old = """ onFormReset={editor && onFormReset ? handleFormReset : undefined} - status={status} - /> - """ - new = """ onFormReset={editor && onFormReset ? handleFormReset : undefined} - status={status} - writingDiagnosticsPanel={ - writingDiagnostics === undefined ? undefined : ( - { - void writingDiagnosticsController.applyDiagnostic( - diagnosticId, - ); - } - : undefined - } - printEnabled={printWritingDiagnostics} - /> - ) - } - /> - """ - source = replace_once(source, old, new, 'diagnostic panel') - path.write_text(source, encoding='utf-8') - PY - - name: Verify collaborative diagnostic integration - run: | - pnpm exec vitest run \ - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx \ - --pool=forks --maxWorkers=1 - pnpm typecheck - - name: Publish validated integration and remove this workflow - env: - TARGET_BRANCH: feat/writing-diagnostics-collaboration - run: | - set -euo pipefail - rm .github/workflows/collaborative-diagnostics-integration-once.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add \ - src/collaboration/CollaborativeCwlEditor.tsx \ - .github/workflows/collaborative-diagnostics-integration-once.yml - git diff --cached --check - git commit -m 'feat(diagnostics): integrate collaborative guidance' - git push origin "HEAD:${TARGET_BRANCH}" diff --git a/src/collaboration/CollaborativeCwlEditor.tsx b/src/collaboration/CollaborativeCwlEditor.tsx index eea89b6c..b313d054 100644 --- a/src/collaboration/CollaborativeCwlEditor.tsx +++ b/src/collaboration/CollaborativeCwlEditor.tsx @@ -19,6 +19,8 @@ import { applyEditorFormReset } from '../components/editorFormReset.js'; import { editorHtmlToValue } from '../components/editorSerialization.js'; import { useEditorHandle } from '../components/useEditorHandle.js'; import { useLatestRef } from '../components/useLatestRef.js'; +import { useWritingDiagnosticsController } from '../components/useWritingDiagnosticsController.js'; +import { WritingDiagnosticsPanel } from '../components/WritingDiagnosticsPanel.js'; import type { ClipboardSanitizationError } from '../extensions/SafeClipboard.js'; import { buildExtensions } from '../extensions/kit.js'; import type { CwlEditorHandle } from '../types.js'; @@ -95,6 +97,11 @@ export const CollaborativeCwlEditor = forwardRef< ariaErrorMessage, ariaInvalid, ariaRequired, + writingDiagnostics, + onWritingDiagnosticAction, + onWritingDiagnosticsError, + writingDiagnosticsLabel, + printWritingDiagnostics, } = props; assertCollaborationConfiguration(provider, user); @@ -260,7 +267,14 @@ export const CollaborativeCwlEditor = forwardRef< [collaborationDocument, scopedProvider, normalizedField, presenceEnabled], ); - useEditorHandle(ref, editor, modeRef); + const writingDiagnosticsController = useWritingDiagnosticsController({ + editor, + diagnostics: writingDiagnostics, + onAction: onWritingDiagnosticAction, + onError: onWritingDiagnosticsError, + }); + + useEditorHandle(ref, editor, modeRef, writingDiagnosticsController); useEffect(() => { editor?.setEditable(editable); @@ -346,6 +360,24 @@ export const CollaborativeCwlEditor = forwardRef< formFieldDisabled={formFieldDisabled} onFormReset={editor && onFormReset ? handleFormReset : undefined} status={status} + writingDiagnosticsPanel={ + writingDiagnostics === undefined ? undefined : ( + { + void writingDiagnosticsController.applyDiagnostic( + diagnosticId, + ); + } + : undefined + } + printEnabled={printWritingDiagnostics} + /> + ) + } /> ); }); From a7373f6a50023f9beea3686f061a35810f8c99c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:50:39 +0900 Subject: [PATCH 05/19] ci(diagnostics): revalidate collaborative guidance From d59a2ede5c458e17001c78008135fd2e86d87284 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:54:16 +0900 Subject: [PATCH 06/19] test(diagnostics): cover collaborative guidance default label --- ...ditor.writingDiagnostics.coverage.test.tsx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx new file mode 100644 index 00000000..bc86e996 --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx @@ -0,0 +1,67 @@ +import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; +import { createRef } from 'react'; +import { afterEach, describe, expect, it } from 'vitest'; +import * as Y from 'yjs'; +import type { CwlEditorHandle } from '../types.js'; +import type { CwlWritingDiagnostic } from '../writingDiagnostics.js'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; + +function diagnostic( + revision: NonNullable< + Awaited> + >, +): CwlWritingDiagnostic { + return { + diagnosticId: 'default-label-diagnostic', + documentRevision: revision, + textProjection: { + id: 'inkspan-prosemirror-text', + version: 1, + }, + selector: { + type: 'TextPositionSelector', + start: 0, + end: 5, + }, + categoryCode: 'clarity', + priority: 'advisory', + title: 'Clarify the action', + explanation: 'State the requested action explicitly.', + provenance: { + workflowId: 'email-writing-review', + workflowVersion: '1', + judgePolicyVersion: 'evaluation-only-1', + }, + }; +} + +afterEach(cleanup); + +describe('CollaborativeCwlEditor writing-diagnostic defaults', () => { + it('uses the default accessible guidance label when the host omits one', async () => { + const document = new Y.Doc(); + const editorRef = createRef(); + const mounted = render( + , + ); + await waitFor(() => expect(editorRef.current?.getEditor()).toBeTruthy()); + act(() => editorRef.current!.setValue('

Alpha beta gamma

')); + const revision = await editorRef.current!.getDocumentEnvelopeRevision(); + expect(revision).not.toBeNull(); + + mounted.rerender( + , + ); + + await waitFor(() => + expect( + screen.getByRole('region', { name: 'Writing guidance' }), + ).toHaveTextContent('1 writing diagnostics'), + ); + }); +}); From b157e32ed9d3850be921c476a9e1d3e297906d65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:58:32 +0900 Subject: [PATCH 07/19] test(diagnostics): prove collaborative diagnostic boundaries --- ...ditor.writingDiagnostics.boundary.test.tsx | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx new file mode 100644 index 00000000..249f683e --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx @@ -0,0 +1,337 @@ +import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; +import { createRef } from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as Y from 'yjs'; +import type { CwlEditorHandle } from '../types.js'; +import type { CwlWritingDiagnostic } from '../writingDiagnostics.js'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; +import type { + CollaborationAwareness, + CollaborationAwarenessEvent, + CollaborationProviderLike, +} from './types.js'; + +function connectDocuments(left: Y.Doc, right: Y.Doc): () => void { + const forward = (update: Uint8Array, origin: unknown) => { + if (origin !== right) Y.applyUpdate(right, update, left); + }; + const reverse = (update: Uint8Array, origin: unknown) => { + if (origin !== left) Y.applyUpdate(left, update, right); + }; + left.on('update', forward); + right.on('update', reverse); + return () => { + left.off('update', forward); + right.off('update', reverse); + }; +} + +function diagnostic( + revision: NonNullable< + Awaited> + >, + id = 'shared-diagnostic', + replacement = 'Omega', +): CwlWritingDiagnostic { + return { + diagnosticId: id, + documentRevision: revision, + textProjection: { + id: 'inkspan-prosemirror-text', + version: 1, + }, + selector: { + type: 'TextPositionSelector', + start: 0, + end: 5, + }, + categoryCode: 'clarity', + priority: 'important', + title: 'Clarify the shared action', + explanation: 'State the requested action explicitly.', + suggestedReplacement: replacement, + provenance: { + workflowId: 'email-writing-review', + workflowVersion: '1', + judgePolicyVersion: 'evaluation-only-1', + }, + }; +} + +class FakeAwareness implements CollaborationAwareness { + readonly clientID = 101; + readonly states = new Map>(); + private localState: Record | null = null; + private readonly listeners: Record< + CollaborationAwarenessEvent, + Set<(...args: unknown[]) => void> + > = { + change: new Set(), + update: new Set(), + }; + + getLocalState(): Record | null { + return this.localState; + } + + getStates(): Map> { + return this.states; + } + + setLocalStateField(field: string, value: unknown): void { + this.localState = { ...(this.localState ?? {}), [field]: value }; + this.states.set(this.clientID, this.localState); + for (const listener of this.listeners.change) listener({}, 'test'); + for (const listener of this.listeners.update) listener({}, 'test'); + } + + on( + event: CollaborationAwarenessEvent, + listener: (...args: unknown[]) => void, + ): void { + this.listeners[event].add(listener); + } + + off( + event: CollaborationAwarenessEvent, + listener: (...args: unknown[]) => void, + ): void { + this.listeners[event].delete(listener); + } +} + +function providerWith(awareness: FakeAwareness): CollaborationProviderLike { + return { awareness }; +} + +afterEach(() => { + vi.restoreAllMocks(); + cleanup(); +}); + +describe('CollaborativeCwlEditor writing-diagnostic boundaries', () => { + it('rejects an older digest when a remote Yjs transaction lands while revision verification is pending', async () => { + const leftDocument = new Y.Doc(); + const rightDocument = new Y.Doc(); + const disconnect = connectDocuments(leftDocument, rightDocument); + const leftRef = createRef(); + const rightRef = createRef(); + + const renderEditors = (writingDiagnostics?: readonly CwlWritingDiagnostic[]) => ( +
+ + +
+ ); + + const mounted = render(renderEditors()); + await waitFor(() => { + expect(leftRef.current?.getEditor()).toBeTruthy(); + expect(rightRef.current?.getEditor()).toBeTruthy(); + }); + act(() => leftRef.current!.setValue('

Alpha beta gamma

')); + await waitFor(() => + expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'), + ); + const revision = await rightRef.current!.getDocumentEnvelopeRevision(); + expect(revision).not.toBeNull(); + + const originalDigest = globalThis.crypto.subtle.digest.bind( + globalThis.crypto.subtle, + ); + let releaseDigest!: () => void; + const digestGate = new Promise((resolve) => { + releaseDigest = resolve; + }); + const digestStarted = vi.fn(); + vi.spyOn(globalThis.crypto.subtle, 'digest').mockImplementation( + async (algorithm, data) => { + digestStarted(); + const result = originalDigest(algorithm, data); + await digestGate; + return result; + }, + ); + + mounted.rerender(renderEditors([diagnostic(revision!)])); + await waitFor(() => expect(digestStarted).toHaveBeenCalled()); + act(() => leftRef.current!.insertValue('

Remote race edit

')); + await waitFor(() => + expect(rightRef.current!.getHTML()).toContain('Remote race edit'), + ); + releaseDigest(); + + await waitFor(() => + expect( + screen.getByRole('region', { name: 'Digest-race guidance' }), + ).toHaveTextContent('0 writing diagnostics'), + ); + await expect( + rightRef.current!.applyWritingDiagnostic('shared-diagnostic'), + ).resolves.toBeNull(); + + mounted.unmount(); + disconnect(); + }); + + it('keeps editor identity stable and keeps diagnostics out of collaboration awareness', async () => { + const document = new Y.Doc(); + const awareness = new FakeAwareness(); + const provider = providerWith(awareness); + const editorRef = createRef(); + const firstAction = vi.fn(); + const secondAction = vi.fn(); + + const mounted = render( + , + ); + await waitFor(() => expect(editorRef.current?.getEditor()).toBeTruthy()); + const originalEditor = editorRef.current!.getEditor(); + act(() => editorRef.current!.setValue('

Alpha beta gamma

')); + const revision = await editorRef.current!.getDocumentEnvelopeRevision(); + expect(revision).not.toBeNull(); + + mounted.rerender( + , + ); + await waitFor(() => + expect( + screen.getByRole('region', { name: 'Writing guidance' }), + ).toHaveTextContent('1 writing diagnostics'), + ); + expect(editorRef.current!.getEditor()).toBe(originalEditor); + expect(awareness.getLocalState()).toEqual({ + user: { + id: 'writer-one', + name: 'Writer One', + color: '#2563eb', + }, + }); + + mounted.rerender( + , + ); + expect(editorRef.current!.getEditor()).toBe(originalEditor); + expect(awareness.getLocalState()).toEqual({ + user: { + id: 'writer-one', + name: 'Writer One', + color: '#2563eb', + }, + }); + expect(JSON.stringify(awareness.getLocalState())).not.toContain( + 'identity-diagnostic', + ); + expect(JSON.stringify(awareness.getLocalState())).not.toContain('Omega'); + }); + + it('emits an Apply action only on the client that explicitly invoked it', async () => { + const leftDocument = new Y.Doc(); + const rightDocument = new Y.Doc(); + const disconnect = connectDocuments(leftDocument, rightDocument); + const leftRef = createRef(); + const rightRef = createRef(); + const leftAction = vi.fn(); + const rightAction = vi.fn(); + + const renderEditors = (writingDiagnostics?: readonly CwlWritingDiagnostic[]) => ( +
+ + +
+ ); + + const mounted = render(renderEditors()); + await waitFor(() => { + expect(leftRef.current?.getEditor()).toBeTruthy(); + expect(rightRef.current?.getEditor()).toBeTruthy(); + }); + act(() => leftRef.current!.setValue('

Alpha beta gamma

')); + await waitFor(() => + expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'), + ); + const revision = await rightRef.current!.getDocumentEnvelopeRevision(); + expect(revision).not.toBeNull(); + + mounted.rerender(renderEditors([diagnostic(revision!)])); + await waitFor(() => { + expect( + screen.getByRole('region', { name: 'Left guidance' }), + ).toHaveTextContent('1 writing diagnostics'); + expect( + screen.getByRole('region', { name: 'Right guidance' }), + ).toHaveTextContent('1 writing diagnostics'); + }); + + await expect( + rightRef.current!.applyWritingDiagnostic('shared-diagnostic'), + ).resolves.toMatchObject({ + action: 'applied', + reasonCode: 'explicit', + diagnosticId: 'shared-diagnostic', + }); + await waitFor(() => { + expect(leftRef.current!.getHTML()).toContain('Omega beta gamma'); + expect(rightRef.current!.getHTML()).toContain('Omega beta gamma'); + }); + expect(rightAction).toHaveBeenCalledTimes(1); + expect(leftAction).not.toHaveBeenCalled(); + + mounted.unmount(); + disconnect(); + }); +}); From df1c1c08362c583f70b35f15bf1c410bd3a5a3bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:59:16 +0900 Subject: [PATCH 08/19] test(diagnostics): carry exact standalone acceptance --- src/components/CwlEditor.writingDiagnostics.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/CwlEditor.writingDiagnostics.test.tsx b/src/components/CwlEditor.writingDiagnostics.test.tsx index ed88de93..c9d0205b 100644 --- a/src/components/CwlEditor.writingDiagnostics.test.tsx +++ b/src/components/CwlEditor.writingDiagnostics.test.tsx @@ -141,7 +141,11 @@ describe('CwlEditor host-supplied writing diagnostics', () => { ); await screen.findByText('Clarify the request'); - expect(handleRef.current!.focusWritingDiagnostic('diagnostic-1')).toBe(true); + let focusResult = false; + act(() => { + focusResult = handleRef.current!.focusWritingDiagnostic('diagnostic-1'); + }); + expect(focusResult).toBe(true); expect(handleRef.current!.getEditor()!.state.selection).toMatchObject({ from: 1, to: 6, From 44bb76f709d1b676e0d35a1b4292051140d12766 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:01:01 +0900 Subject: [PATCH 09/19] test(diagnostics): cover collaborative races and awareness --- ...CwlEditor.writingDiagnostics.race.test.tsx | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx new file mode 100644 index 00000000..ba7211c9 --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx @@ -0,0 +1,315 @@ +import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; +import { createRef } from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as Y from 'yjs'; +import type { CwlEditorHandle } from '../types.js'; +import type { CwlWritingDiagnostic } from '../writingDiagnostics.js'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; +import type { + CollaborationAwareness, + CollaborationAwarenessEvent, + CollaborationProviderLike, + CollaborationUser, +} from './types.js'; + +class FakeAwareness implements CollaborationAwareness { + readonly clientID: number; + readonly states = new Map>(); + private localState: Record | null = null; + private readonly listeners: Record< + CollaborationAwarenessEvent, + Set<(...args: unknown[]) => void> + > = { + change: new Set(), + update: new Set(), + }; + + constructor(clientID = 1) { + this.clientID = clientID; + } + + getLocalState(): Record | null { + return this.localState; + } + + getStates(): Map> { + return this.states; + } + + setLocalStateField(field: string, value: unknown): void { + this.localState = { ...(this.localState ?? {}), [field]: value }; + this.states.set(this.clientID, this.localState); + for (const event of ['change', 'update'] as const) { + for (const listener of this.listeners[event]) listener({}, 'test'); + } + } + + on( + event: CollaborationAwarenessEvent, + listener: (...args: unknown[]) => void, + ): void { + this.listeners[event].add(listener); + } + + off( + event: CollaborationAwarenessEvent, + listener: (...args: unknown[]) => void, + ): void { + this.listeners[event].delete(listener); + } +} + +function connectDocuments(left: Y.Doc, right: Y.Doc): () => void { + const forward = (update: Uint8Array, origin: unknown) => { + if (origin !== right) Y.applyUpdate(right, update, left); + }; + const reverse = (update: Uint8Array, origin: unknown) => { + if (origin !== left) Y.applyUpdate(left, update, right); + }; + left.on('update', forward); + right.on('update', reverse); + return () => { + left.off('update', forward); + right.off('update', reverse); + }; +} + +async function diagnosticFor( + handle: CwlEditorHandle, + diagnosticId = 'collaborative-race-diagnostic', +): Promise { + act(() => { + handle.getEditor()!.commands.setTextSelection({ from: 1, to: 6 }); + }); + const evidence = await handle.getTextPositionSelectorEvidence(); + if (evidence === null) throw new Error('Missing collaborative selector evidence'); + return { + diagnosticId, + documentRevision: evidence.revision, + textProjection: evidence.textProjection, + selector: evidence.selector, + categoryCode: 'clarity', + priority: 'important', + title: 'Clarify the shared request', + explanation: 'Make the shared action explicit.', + suggestedReplacement: 'Omega', + provenance: { + workflowId: 'email-writing-review', + workflowVersion: '1', + judgePolicyVersion: 'evaluation-only-1', + }, + }; +} + +const ALICE: CollaborationUser = { + userId: 'editor-alice', + displayName: 'Alice', + cursorColor: '#2563eb', +}; + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe('CollaborativeCwlEditor diagnostic race and privacy boundaries', () => { + it('blocks a local apply when a remote update arrives while both revision digests are pending', async () => { + const leftDocument = new Y.Doc(); + const rightDocument = new Y.Doc(); + const disconnect = connectDocuments(leftDocument, rightDocument); + const leftRef = createRef(); + const rightRef = createRef(); + const leftAction = vi.fn(); + const rightAction = vi.fn(); + + const renderEditors = (writingDiagnostics?: readonly CwlWritingDiagnostic[]) => ( +
+ + +
+ ); + + const mounted = render(renderEditors()); + await waitFor(() => { + expect(leftRef.current?.getEditor()).toBeTruthy(); + expect(rightRef.current?.getEditor()).toBeTruthy(); + }); + act(() => leftRef.current!.setValue('

Alpha beta gamma

')); + await waitFor(() => + expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'), + ); + const diagnostic = await diagnosticFor(rightRef.current!); + + mounted.rerender(renderEditors([diagnostic])); + await waitFor(() => { + expect( + screen.getByRole('region', { name: 'Left race guidance' }), + ).toHaveTextContent('1 writing diagnostics'); + expect( + screen.getByRole('region', { name: 'Right race guidance' }), + ).toHaveTextContent('1 writing diagnostics'); + }); + + const originalDigest = globalThis.crypto.subtle.digest.bind( + globalThis.crypto.subtle, + ); + const pending: Array<{ + algorithm: AlgorithmIdentifier; + source: BufferSource; + resolve: (value: ArrayBuffer) => void; + }> = []; + vi.spyOn(globalThis.crypto.subtle, 'digest').mockImplementation( + (algorithm, source) => + new Promise((resolve) => { + pending.push({ algorithm, source, resolve }); + }), + ); + + let application!: ReturnType; + act(() => { + application = rightRef.current!.applyWritingDiagnostic( + 'collaborative-race-diagnostic', + ); + }); + await waitFor(() => expect(pending).toHaveLength(2)); + + act(() => leftRef.current!.insertValue('!')); + await waitFor(() => expect(rightRef.current!.getHTML()).toContain('!')); + + await act(async () => { + for (const request of pending) { + request.resolve(await originalDigest(request.algorithm, request.source)); + } + }); + let event = null; + await act(async () => { + event = await application; + }); + + expect(event).toMatchObject({ + action: 'conflict', + reasonCode: 'document_changed', + diagnosticId: 'collaborative-race-diagnostic', + }); + expect(leftRef.current!.getHTML()).not.toContain('Omega'); + expect(rightRef.current!.getHTML()).not.toContain('Omega'); + expect(rightAction).toHaveBeenCalledWith(event); + expect(leftAction).not.toHaveBeenCalled(); + expect( + screen.getByRole('region', { name: 'Left race guidance' }), + ).toHaveTextContent('0 writing diagnostics'); + expect( + screen.getByRole('region', { name: 'Right race guidance' }), + ).toHaveTextContent('0 writing diagnostics'); + + mounted.unmount(); + disconnect(); + }); + + it('keeps diagnostics out of awareness and uses the latest callback without recreating or owning host resources', async () => { + const collaborationDocument = new Y.Doc(); + const destroy = vi.spyOn(collaborationDocument, 'destroy'); + const awareness = new FakeAwareness(10); + const provider: CollaborationProviderLike = { awareness }; + const editorRef = createRef(); + const firstAction = vi.fn(); + const secondAction = vi.fn(); + + const mounted = render( + , + ); + await waitFor(() => { + expect(editorRef.current?.getEditor()).toBeTruthy(); + expect(awareness.getLocalState()?.user).toEqual({ + id: 'editor-alice', + name: 'Alice', + color: '#2563eb', + }); + }); + const editorIdentity = editorRef.current!.getEditor(); + act(() => editorRef.current!.setValue('

Alpha beta gamma

')); + const diagnostic = await diagnosticFor(editorRef.current!, 'awareness-diagnostic'); + + mounted.rerender( + , + ); + await waitFor(() => + expect( + screen.getByRole('region', { name: 'Awareness-safe guidance' }), + ).toHaveTextContent('1 writing diagnostics'), + ); + expect(editorRef.current!.getEditor()).toBe(editorIdentity); + + const serializedAwareness = JSON.stringify(awareness.getLocalState()); + for (const forbidden of [ + diagnostic.diagnosticId, + diagnostic.title, + diagnostic.explanation, + diagnostic.suggestedReplacement!, + diagnostic.documentRevision.digestHex, + diagnostic.provenance.workflowId, + ]) { + expect(serializedAwareness).not.toContain(forbidden); + } + + mounted.rerender( + , + ); + expect(editorRef.current!.getEditor()).toBe(editorIdentity); + + let explanation = null; + act(() => { + explanation = editorRef.current!.requestWritingDiagnosticExplanation( + 'awareness-diagnostic', + ); + }); + expect(explanation).toMatchObject({ + action: 'requested_explanation', + diagnosticId: 'awareness-diagnostic', + }); + expect(secondAction).toHaveBeenCalledWith(explanation); + expect(firstAction).not.toHaveBeenCalled(); + + mounted.unmount(); + expect(destroy).not.toHaveBeenCalled(); + }); +}); From cefaabd29aa93dccc4f431daf3e18d2f0ce27e1a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:02:01 +0900 Subject: [PATCH 10/19] test(diagnostics): require full collaborative convergence --- ...ativeCwlEditor.writingDiagnostics.test.tsx | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx index 088e61ba..003eccd6 100644 --- a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx @@ -5,6 +5,7 @@ import { render, screen, waitFor, + within, } from '@testing-library/react'; import { createRef } from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -32,9 +33,10 @@ function connectDocuments(left: Y.Doc, right: Y.Doc): () => void { function diagnostic( revision: CwlEditorDocumentRevision, replacement = 'Omega', + diagnosticId = 'collaborative-diagnostic', ): CwlWritingDiagnostic { return { - diagnosticId: 'collaborative-diagnostic', + diagnosticId, documentRevision: revision, textProjection: { id: 'inkspan-prosemirror-text', @@ -47,7 +49,7 @@ function diagnostic( }, categoryCode: 'clarity', priority: 'important', - title: 'Clarify the shared request', + title: `Clarify the shared request ${diagnosticId}`, explanation: 'Make the shared action explicit.', suggestedReplacement: replacement, provenance: { @@ -61,13 +63,14 @@ function diagnostic( afterEach(cleanup); describe('CollaborativeCwlEditor writing diagnostics', () => { - it('applies an exact-revision replacement through Yjs, converges, and remains undoable', async () => { + it('applies through Yjs, converges, and preserves collaborative undo/redo without fabricating remote actions', async () => { const leftDocument = new Y.Doc(); const rightDocument = new Y.Doc(); const disconnect = connectDocuments(leftDocument, rightDocument); const leftRef = createRef(); const rightRef = createRef(); - const onAction = vi.fn(); + const leftAction = vi.fn(); + const rightAction = vi.fn(); const renderEditors = (writingDiagnostics?: readonly CwlWritingDiagnostic[]) => (
@@ -75,14 +78,17 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { ref={leftRef} document={leftDocument} mode="html" + writingDiagnostics={writingDiagnostics} + writingDiagnosticsLabel="Left shared guidance" + onWritingDiagnosticAction={leftAction} />
); @@ -99,10 +105,21 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { ); const revision = await rightRef.current!.getDocumentEnvelopeRevision(); expect(revision).not.toBeNull(); + const sharedDiagnostic = diagnostic(revision!); - mounted.rerender(renderEditors([diagnostic(revision!)])); - const apply = await screen.findByRole('button', { - name: 'Apply suggestion for Clarify the shared request', + mounted.rerender(renderEditors([sharedDiagnostic])); + await waitFor(() => { + expect( + screen.getByRole('region', { name: 'Left shared guidance' }), + ).toHaveTextContent('1 writing diagnostics'); + expect( + screen.getByRole('region', { name: 'Right shared guidance' }), + ).toHaveTextContent('1 writing diagnostics'); + }); + const apply = within( + screen.getByRole('region', { name: 'Right shared guidance' }), + ).getByRole('button', { + name: 'Apply suggestion for Clarify the shared request collaborative-diagnostic', }); expect(apply).toBeEnabled(); @@ -111,7 +128,8 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { expect(rightRef.current!.getHTML()).toContain('Omega beta gamma'); expect(leftRef.current!.getHTML()).toContain('Omega beta gamma'); }); - expect(onAction).toHaveBeenCalledWith( + expect(rightAction).toHaveBeenCalledTimes(1); + expect(rightAction).toHaveBeenCalledWith( expect.objectContaining({ action: 'applied', reasonCode: 'explicit', @@ -121,26 +139,45 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { }), }), ); + expect(leftAction).not.toHaveBeenCalled(); + expect( + screen.getByRole('region', { name: 'Left shared guidance' }), + ).toHaveTextContent('0 writing diagnostics'); expect( - screen.getByRole('region', { name: 'Shared writing guidance' }), + screen.getByRole('region', { name: 'Right shared guidance' }), ).toHaveTextContent('0 writing diagnostics'); - act(() => rightRef.current!.getEditor()!.commands.undo()); + act(() => { + expect(rightRef.current!.getEditor()!.commands.undo()).toBe(true); + }); await waitFor(() => { expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'); expect(leftRef.current!.getHTML()).toContain('Alpha beta gamma'); }); + expect(rightAction).toHaveBeenCalledTimes(1); + expect(leftAction).not.toHaveBeenCalled(); + + act(() => { + expect(rightRef.current!.getEditor()!.commands.redo()).toBe(true); + }); + await waitFor(() => { + expect(rightRef.current!.getHTML()).toContain('Omega beta gamma'); + expect(leftRef.current!.getHTML()).toContain('Omega beta gamma'); + }); + expect(rightAction).toHaveBeenCalledTimes(1); + expect(leftAction).not.toHaveBeenCalled(); mounted.unmount(); disconnect(); }); - it('invalidates current diagnostics when a remote Yjs update changes the document', async () => { + it('invalidates every current diagnostic when a remote Yjs update changes the document', async () => { const leftDocument = new Y.Doc(); const rightDocument = new Y.Doc(); const disconnect = connectDocuments(leftDocument, rightDocument); const leftRef = createRef(); const rightRef = createRef(); + const rightAction = vi.fn(); const renderEditors = (writingDiagnostics?: readonly CwlWritingDiagnostic[]) => (
@@ -151,6 +188,7 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { mode="html" writingDiagnostics={writingDiagnostics} writingDiagnosticsLabel="Remote-safe guidance" + onWritingDiagnosticAction={rightAction} />
); @@ -166,13 +204,18 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { ); const revision = await rightRef.current!.getDocumentEnvelopeRevision(); expect(revision).not.toBeNull(); + const diagnostics = [ + diagnostic(revision!, 'Omega', 'remote-diagnostic-one'), + diagnostic(revision!, 'Sigma', 'remote-diagnostic-two'), + ]; - mounted.rerender(renderEditors([diagnostic(revision!)])); + mounted.rerender(renderEditors(diagnostics)); await waitFor(() => expect( screen.getByRole('region', { name: 'Remote-safe guidance' }), - ).toHaveTextContent('1 writing diagnostics'), + ).toHaveTextContent('2 writing diagnostics'), ); + expect(document.querySelectorAll('.cwl-writing-diagnostic')).toHaveLength(2); act(() => leftRef.current!.insertValue('

Remote edit

')); await waitFor(() => @@ -180,9 +223,14 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { screen.getByRole('region', { name: 'Remote-safe guidance' }), ).toHaveTextContent('0 writing diagnostics'), ); + expect(document.querySelector('.cwl-writing-diagnostic')).toBeNull(); + await expect( + rightRef.current!.applyWritingDiagnostic('remote-diagnostic-one'), + ).resolves.toBeNull(); await expect( - rightRef.current!.applyWritingDiagnostic('collaborative-diagnostic'), + rightRef.current!.applyWritingDiagnostic('remote-diagnostic-two'), ).resolves.toBeNull(); + expect(rightAction).not.toHaveBeenCalled(); expect(rightRef.current!.getHTML()).toContain('Remote edit'); expect(rightRef.current!.getHTML()).toContain('Alpha beta gamma'); @@ -217,7 +265,7 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { />, ); const apply = await screen.findByRole('button', { - name: 'Apply suggestion for Clarify the shared request', + name: 'Apply suggestion for Clarify the shared request collaborative-diagnostic', }); expect(apply).toBeDisabled(); await expect( From ae4c505b10ce12853daf4a7d2e7f14fb455d519a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:02:30 +0900 Subject: [PATCH 11/19] ci(diagnostics): exercise collaborative races --- .github/workflows/writing-diagnostics-collaboration-tdd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/writing-diagnostics-collaboration-tdd.yml b/.github/workflows/writing-diagnostics-collaboration-tdd.yml index 14f57381..818cd46c 100644 --- a/.github/workflows/writing-diagnostics-collaboration-tdd.yml +++ b/.github/workflows/writing-diagnostics-collaboration-tdd.yml @@ -35,6 +35,8 @@ jobs: run: >- pnpm exec vitest run src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx --pool=forks --maxWorkers=1 - name: Typecheck collaborative writing-diagnostic contracts From 03b34c0ca2364f15fcb3aaedde60fe32c6d0c2f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:05:06 +0900 Subject: [PATCH 12/19] ci(diagnostics): correct collaborative overlap evidence once --- ...ollaborative-diagnostics-test-fix-once.yml | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/collaborative-diagnostics-test-fix-once.yml diff --git a/.github/workflows/collaborative-diagnostics-test-fix-once.yml b/.github/workflows/collaborative-diagnostics-test-fix-once.yml new file mode 100644 index 00000000..f6232897 --- /dev/null +++ b/.github/workflows/collaborative-diagnostics-test-fix-once.yml @@ -0,0 +1,77 @@ +name: Collaborative Diagnostics Test Fix Once + +on: + push: + branches: + - feat/writing-diagnostics-collaboration + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: collaborative-diagnostics-test-fix-once-${{ github.ref }} + cancel-in-progress: true + +jobs: + correct-overlap-assertion: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: true + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Correct overlapping decoration assertion and include boundary suite + run: | + python <<'PY' + from pathlib import Path + + test_path = Path('src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx') + source = test_path.read_text(encoding='utf-8') + obsolete = " expect(document.querySelectorAll('.cwl-writing-diagnostic')).toHaveLength(2);\n" + if source.count(obsolete) != 1: + raise SystemExit('overlap assertion anchor changed') + source = source.replace(obsolete, '', 1) + test_path.write_text(source, encoding='utf-8') + + workflow_path = Path('.github/workflows/writing-diagnostics-collaboration-tdd.yml') + workflow = workflow_path.read_text(encoding='utf-8') + anchor = " src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx\n" + addition = anchor + " src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx\n" + if workflow.count(anchor) != 1: + raise SystemExit('collaboration workflow test anchor changed') + if 'CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx' not in workflow: + workflow = workflow.replace(anchor, addition, 1) + workflow_path.write_text(workflow, encoding='utf-8') + PY + - name: Verify corrected focused collaborative suite + run: | + pnpm exec vitest run \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx \ + --pool=forks --maxWorkers=1 + pnpm typecheck + - name: Publish correction and remove this workflow + env: + TARGET_BRANCH: feat/writing-diagnostics-collaboration + run: | + set -euo pipefail + rm .github/workflows/collaborative-diagnostics-test-fix-once.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx \ + .github/workflows/writing-diagnostics-collaboration-tdd.yml \ + .github/workflows/collaborative-diagnostics-test-fix-once.yml + git diff --cached --check + git commit -m 'test(diagnostics): scope collaborative overlap evidence' + git push origin "HEAD:${TARGET_BRANCH}" From 440669a8e66204e8a1d03cb9e611b60a9a33a0a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:05:43 +0900 Subject: [PATCH 13/19] test(diagnostics): inspect installed collaborative generations --- ...llaborativeCwlEditor.writingDiagnostics.test.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx index 003eccd6..ae331769 100644 --- a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx @@ -11,6 +11,7 @@ import { createRef } from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import * as Y from 'yjs'; import type { CwlEditorDocumentRevision } from '../documentEnvelopeRevision.js'; +import { writingDiagnosticsPluginKey } from '../extensions/WritingDiagnostics.js'; import type { CwlEditorHandle } from '../types.js'; import type { CwlWritingDiagnostic } from '../writingDiagnostics.js'; import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; @@ -215,7 +216,11 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { screen.getByRole('region', { name: 'Remote-safe guidance' }), ).toHaveTextContent('2 writing diagnostics'), ); - expect(document.querySelectorAll('.cwl-writing-diagnostic')).toHaveLength(2); + expect( + writingDiagnosticsPluginKey.getState( + rightRef.current!.getEditor()!.state, + )?.diagnostics, + ).toHaveLength(2); act(() => leftRef.current!.insertValue('

Remote edit

')); await waitFor(() => @@ -223,7 +228,11 @@ describe('CollaborativeCwlEditor writing diagnostics', () => { screen.getByRole('region', { name: 'Remote-safe guidance' }), ).toHaveTextContent('0 writing diagnostics'), ); - expect(document.querySelector('.cwl-writing-diagnostic')).toBeNull(); + expect( + writingDiagnosticsPluginKey.getState( + rightRef.current!.getEditor()!.state, + )?.diagnostics, + ).toEqual([]); await expect( rightRef.current!.applyWritingDiagnostic('remote-diagnostic-one'), ).resolves.toBeNull(); From 30e20cdc45a7fde3c62855d66fd43de9202457ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:07:07 +0900 Subject: [PATCH 14/19] ci(diagnostics): remove superseded overlap fixer --- ...ollaborative-diagnostics-test-fix-once.yml | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 .github/workflows/collaborative-diagnostics-test-fix-once.yml diff --git a/.github/workflows/collaborative-diagnostics-test-fix-once.yml b/.github/workflows/collaborative-diagnostics-test-fix-once.yml deleted file mode 100644 index f6232897..00000000 --- a/.github/workflows/collaborative-diagnostics-test-fix-once.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Collaborative Diagnostics Test Fix Once - -on: - push: - branches: - - feat/writing-diagnostics-collaboration - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: collaborative-diagnostics-test-fix-once-${{ github.ref }} - cancel-in-progress: true - -jobs: - correct-overlap-assertion: - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: true - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Correct overlapping decoration assertion and include boundary suite - run: | - python <<'PY' - from pathlib import Path - - test_path = Path('src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx') - source = test_path.read_text(encoding='utf-8') - obsolete = " expect(document.querySelectorAll('.cwl-writing-diagnostic')).toHaveLength(2);\n" - if source.count(obsolete) != 1: - raise SystemExit('overlap assertion anchor changed') - source = source.replace(obsolete, '', 1) - test_path.write_text(source, encoding='utf-8') - - workflow_path = Path('.github/workflows/writing-diagnostics-collaboration-tdd.yml') - workflow = workflow_path.read_text(encoding='utf-8') - anchor = " src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx\n" - addition = anchor + " src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx\n" - if workflow.count(anchor) != 1: - raise SystemExit('collaboration workflow test anchor changed') - if 'CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx' not in workflow: - workflow = workflow.replace(anchor, addition, 1) - workflow_path.write_text(workflow, encoding='utf-8') - PY - - name: Verify corrected focused collaborative suite - run: | - pnpm exec vitest run \ - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx \ - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx \ - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx \ - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx \ - --pool=forks --maxWorkers=1 - pnpm typecheck - - name: Publish correction and remove this workflow - env: - TARGET_BRANCH: feat/writing-diagnostics-collaboration - run: | - set -euo pipefail - rm .github/workflows/collaborative-diagnostics-test-fix-once.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add \ - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx \ - .github/workflows/writing-diagnostics-collaboration-tdd.yml \ - .github/workflows/collaborative-diagnostics-test-fix-once.yml - git diff --cached --check - git commit -m 'test(diagnostics): scope collaborative overlap evidence' - git push origin "HEAD:${TARGET_BRANCH}" From bf8b7dbcb373d6692a0a11e9fde8a89137c4f443 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:07:33 +0900 Subject: [PATCH 15/19] ci(diagnostics): include collaborative boundary suite --- .github/workflows/writing-diagnostics-collaboration-tdd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/writing-diagnostics-collaboration-tdd.yml b/.github/workflows/writing-diagnostics-collaboration-tdd.yml index 818cd46c..60c53506 100644 --- a/.github/workflows/writing-diagnostics-collaboration-tdd.yml +++ b/.github/workflows/writing-diagnostics-collaboration-tdd.yml @@ -37,6 +37,7 @@ jobs: src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx --pool=forks --maxWorkers=1 - name: Typecheck collaborative writing-diagnostic contracts From 106b90bbd043b629d9d7b75ed5fc66f3cc70af9c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:17:10 +0900 Subject: [PATCH 16/19] test(diagnostics): reject unwrapped collaborative action updates --- ...iveCwlEditor.writingDiagnostics.boundary.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx index 249f683e..3ea5c485 100644 --- a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx @@ -104,6 +104,14 @@ function providerWith(awareness: FakeAwareness): CollaborationProviderLike { return { awareness }; } +function reactActWarnings( + consoleError: ReturnType, +): string[] { + return consoleError.mock.calls + .map((arguments_) => arguments_.map(String).join(' ')) + .filter((message) => message.includes('not wrapped in act')); +} + afterEach(() => { vi.restoreAllMocks(); cleanup(); @@ -266,6 +274,9 @@ describe('CollaborativeCwlEditor writing-diagnostic boundaries', () => { }); it('emits an Apply action only on the client that explicitly invoked it', async () => { + const consoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); const leftDocument = new Y.Doc(); const rightDocument = new Y.Doc(); const disconnect = connectDocuments(leftDocument, rightDocument); @@ -330,6 +341,7 @@ describe('CollaborativeCwlEditor writing-diagnostic boundaries', () => { }); expect(rightAction).toHaveBeenCalledTimes(1); expect(leftAction).not.toHaveBeenCalled(); + expect(reactActWarnings(consoleError)).toEqual([]); mounted.unmount(); disconnect(); From ccf91810c0d795f9b35bb05023ececb03d0b0e3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:19:10 +0900 Subject: [PATCH 17/19] fix(diagnostics): wrap collaborative apply updates in React act --- ...iveCwlEditor.writingDiagnostics.boundary.test.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx index 3ea5c485..a6c2e519 100644 --- a/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx +++ b/src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx @@ -328,9 +328,15 @@ describe('CollaborativeCwlEditor writing-diagnostic boundaries', () => { ).toHaveTextContent('1 writing diagnostics'); }); - await expect( - rightRef.current!.applyWritingDiagnostic('shared-diagnostic'), - ).resolves.toMatchObject({ + let appliedAction: + | Awaited> + | undefined; + await act(async () => { + appliedAction = await rightRef.current!.applyWritingDiagnostic( + 'shared-diagnostic', + ); + }); + expect(appliedAction).toMatchObject({ action: 'applied', reasonCode: 'explicit', diagnosticId: 'shared-diagnostic', From 978835a24a7f1e89ad07aa21d985280eeaa8006a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:22:13 +0900 Subject: [PATCH 18/19] test(ci): require warning-free collaboration workflow --- src/workflowExactHead.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/workflowExactHead.test.ts b/src/workflowExactHead.test.ts index 4804599a..f893b8db 100644 --- a/src/workflowExactHead.test.ts +++ b/src/workflowExactHead.test.ts @@ -9,11 +9,18 @@ function repositoryFile(path: string): string { } const workflow = repositoryFile('.github/workflows/ci.yml'); +const collaborationWorkflow = repositoryFile( + '.github/workflows/writing-diagnostics-collaboration-tdd.yml', +); const CHECKOUT_PIN = 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1'; const SETUP_NODE_PIN = 'actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0'; +const SAFE_PNPM_ACTION_PIN = + 'pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10'; +const VULNERABLE_PNPM_ACTION_PIN = + 'pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8'; describe('exact-head CI workflow contract', () => { it('uses a fixed runner and checks out the immutable current PR head', () => { @@ -52,6 +59,23 @@ describe('exact-head CI workflow contract', () => { ); }); + it('makes collaboration assurance fail closed on React act warnings', () => { + expect(collaborationWorkflow).toContain(SAFE_PNPM_ACTION_PIN); + expect(collaborationWorkflow).not.toContain(VULNERABLE_PNPM_ACTION_PIN); + expect( + collaborationWorkflow.match(/not wrapped in act/g), + ).toHaveLength(2); + expect( + collaborationWorkflow.match(/test_status=\$\{PIPESTATUS\[0\]\}/g), + ).toHaveLength(2); + expect(collaborationWorkflow).toContain( + '::error::Focused collaborative diagnostics emitted a React act warning.', + ); + expect(collaborationWorkflow).toContain( + '::error::Production coverage emitted a React act warning.', + ); + }); + it('records the evidence boundary and unreleased hardening', () => { const doctoring = repositoryFile( 'docs/doctoring/exact-head-ci-evidence.md', From 07845a1e3b10d66572fcf442fab1b78bbaa34dd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:24:54 +0900 Subject: [PATCH 19/19] ci(diagnostics): fail closed on collaborative act warnings --- .../writing-diagnostics-collaboration-tdd.yml | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/.github/workflows/writing-diagnostics-collaboration-tdd.yml b/.github/workflows/writing-diagnostics-collaboration-tdd.yml index 60c53506..9c028d95 100644 --- a/.github/workflows/writing-diagnostics-collaboration-tdd.yml +++ b/.github/workflows/writing-diagnostics-collaboration-tdd.yml @@ -25,27 +25,50 @@ jobs: with: ref: ${{ github.sha }} persist-credentials: false - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - name: Run collaborative writing-diagnostic parity tests - run: >- - pnpm exec vitest run - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx - src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx - --pool=forks - --maxWorkers=1 + run: | + set -euo pipefail + output_file="$(mktemp)" + trap 'rm -f "$output_file"' EXIT + set +e + pnpm exec vitest run \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.test.tsx \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.race.test.tsx \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.coverage.test.tsx \ + src/collaboration/CollaborativeCwlEditor.writingDiagnostics.boundary.test.tsx \ + --pool=forks \ + --maxWorkers=1 2>&1 | tee "$output_file" + test_status=${PIPESTATUS[0]} + set -e + if grep -Fq 'not wrapped in act' "$output_file"; then + echo "::error::Focused collaborative diagnostics emitted a React act warning." + exit 1 + fi + exit "$test_status" - name: Typecheck collaborative writing-diagnostic contracts run: pnpm typecheck - name: Run complete production coverage gate env: NODE_OPTIONS: --max-old-space-size=6144 - run: pnpm coverage + run: | + set -euo pipefail + output_file="$(mktemp)" + trap 'rm -f "$output_file"' EXIT + set +e + pnpm coverage 2>&1 | tee "$output_file" + test_status=${PIPESTATUS[0]} + set -e + if grep -Fq 'not wrapped in act' "$output_file"; then + echo "::error::Production coverage emitted a React act warning." + exit 1 + fi + exit "$test_status" - name: Build all package entrypoints run: pnpm build - name: Verify isolated packed-package consumers