diff --git a/docs/README.md b/docs/README.md index 2b617f7c..6877a550 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ This directory is the discoverable index for Inkspan's product, technical, secur | [`TRD.md`](TRD.md) | Technical invariants, runtime boundaries, failure semantics and release evidence | | [`CONTRACTS.md`](CONTRACTS.md) | Public package/API/event/schema/plugin/collaboration and host-integration contracts | | [`package-distribution.md`](package-distribution.md) | Buyer-facing public npm package entrypoints, packaged contents, runtime dependency boundaries, and consumer verification | +| [`performance-envelope.md`](performance-envelope.md) | Proposed synthetic large-document browser baseline; no support budget is claimed before repeated measurements | | [`email-output.md`](email-output.md) | Deterministic email fragment/full-document authority, language/direction metadata, accessibility and host-owned transport boundary | | [`print-output.md`](print-output.md) | Browser print/paged-media presentation, accessibility/fidelity limits, host-owned governed-export boundary, and rollback | | [`UML.md`](UML.md) | Component, sequence, state and authority-flow diagrams | diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 34e903fc..69dac7c7 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -26,6 +26,13 @@ Where browser fragment parsing or serialization participates in a security bound The protected gate pins **Playwright 1.62.0** in an isolated immutable browser-test lock and runs the same versioned synthetic corpus through the supported TipTap/ProseMirror `transformPastedHTML` path in named Chromium, Firefox, and WebKit projects on one **exact source head**. Evidence binds the corpus version, browser-test lock digest, Playwright version, actual browser versions, operating-system identity, exact source head, fresh run identity, and exact packed npm artifact digest. The corpus covers active/resource/form content, hidden/Office/popover semantics, safe and unsafe links, malformed fragments, tables/lists, SVG/MathML, interactive/native fallback, byte/node/depth ceilings, hostile DOM capability failures, and a representative Word-like performance alarm. +Issue #375's proposed first slice separately runs the deterministic synthetic +`inkspan-large-document-v1` corpus through the real `CwlEditor` browser harness +for mount, snapshot, envelope serialization, and revision timing. This is +PR-CI smoke evidence only: it reports no document body or digest and claims no +support budget until repeated exact-head hardware/runtime measurements are +available. + Differences are not normalized away merely to produce parity. The default gate has no generic normalization or broad engine allowlist; a permitted difference requires a focused regression fixture, authoritative standards basis, threat analysis, exact affected engine/version evidence, canonical interpretation, compatibility impact, and rollback. Missing, skipped, cancelled, incomplete, stale-run, stale-lock, package-mismatched, or divergent required browser evidence must fail closed rather than becoming successful release evidence. A source movement after evidence generation requires the new exact release candidate to re-prove the gate. ### Office artifact tests diff --git a/docs/adr/0028-large-document-performance-baseline.md b/docs/adr/0028-large-document-performance-baseline.md new file mode 100644 index 00000000..84e54b58 --- /dev/null +++ b/docs/adr/0028-large-document-performance-baseline.md @@ -0,0 +1,97 @@ +# ADR 0028: Large-document performance baseline before support budgets + +- Status: Proposed +- Date: 2026-08-21 +- Owners: Inkspan editor/conversion maintainers +- Scope: Issue #375 PR-CI baseline only + +## Context + +Inkspan has correctness, package, browser, coverage, and deterministic +conversion checks, but protected `main` does not publish a measured large- +document latency or memory envelope. A favorable threshold chosen before a +reproducible baseline would be an unsupported buyer claim and could hide a +regression. + +## Alternatives considered + +1. Publish a document-size or 100 ms budget immediately. Rejected because no + repeated exact-head hardware/runtime baseline exists. +2. Add an unbounded benchmark that records complete document bodies or host + metadata. Rejected because benchmark evidence must remain synthetic and + privacy-minimized. +3. Add a bounded synthetic browser smoke and defer budgets until measurements + exist. Selected because it establishes the executable path with minimal new + authority. + +## Decision + +The initial performance lane uses a versioned synthetic multilingual corpus and +the packed-package browser harness to measure editor mount, detached snapshot, +canonical envelope serialization, and revision derivation in Chromium, Firefox, +and WebKit. It records only profile/count/length/timing metadata in test +attachments. The smoke requires finite non-negative measurements and successful +revision derivation but accepts no performance threshold. + +Future budgets require repeated samples, named hardware and runtime profiles, +operation-specific p50/p75/p95/maximum values, peak-memory methodology where +trustworthy, and an explicit regression rule. They must be proposed in a later +ADR update rather than inferred from this smoke. + +## Consequences + +The repository gains a repeatable multilingual browser measurement path without +making a new runtime or package API. Buyers receive an executable baseline +signal, while a supported large-document envelope remains intentionally +unclaimed until stronger evidence exists. + +## Failure and recovery + +An unavailable benchmark operation must fail the active evidence run rather +than become an unmeasured success. A runner failure is an evidence failure, not +proof that Inkspan supports the profile. Maintainers rerun the same exact +source and corpus or record the environment defect before changing the +contract. + +## Security and privacy impact + +Inkspan owns deterministic local measurement of its editor/conversion calls. +Hosts own production telemetry, document classification, tenant policy, +authorization, retention, and any durable benchmark storage. The corpus and +attachments contain no customer content, credentials, prompts, comment bodies, +revision digests, or network authority. No benchmark code adds transport, +persistence, model, provider, or database authority. + +## Compatibility and migration + +The smoke uses existing public editor handles and does not change runtime +document semantics or package exports. A browser runner that cannot execute the +probe fails the active PR check; it is not treated as an unmeasured success. If +the corpus or measured operation changes, increment the corpus/contract record +and regenerate evidence on the exact source head. No document-schema or package +migration is introduced by this baseline. + +## Verification + +- `tests/browser/performanceCorpus.ts` is deterministic and multilingual. +- `tests/browser/harness.ts` measures the real `CwlEditor` and handle path. +- `tests/browser/specs/performance.browser.spec.ts` runs the profiles through + dependency-locked Chromium, Firefox, and WebKit with external requests + blocked. +- Existing TypeScript, Vitest, package, and release browser documentation + contracts remain required. + +## Rollback or supersession + +Remove or supersede this Proposed baseline if a later accepted performance +contract replaces it. Rolling back the smoke must not be described as evidence +of a supported performance envelope; the remaining correctness and release +gates continue to apply. + +## Research basis + +World Wide Web Consortium. (2026, March 19). *Event Timing API* (Working +Draft). https://www.w3.org/TR/event-timing/ + +Isik, P. (2025, June 24). *Faster, lighter, and more reliable DOCX +import/export with Tiptap*. Tiptap. https://tiptap.dev/blog/release-notes/faster-lighter-and-more-reliable-docx-import-export-with-tiptap diff --git a/docs/adr/README.md b/docs/adr/README.md index df8b7b80..d4fa24ce 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -32,6 +32,7 @@ This index records durable architectural decisions. Protected-main implementatio | [0024](0024-bounded-docx-paragraph-alignment.md) | Accepted | Bounded paragraph alignment in deterministic DOCX output | | [0025](0025-bounded-docx-heading-alignment.md) | Accepted | Bounded heading alignment in deterministic DOCX output | | [0026](0026-bounded-docx-external-hyperlinks.md) | Accepted | Bounded external hyperlinks in deterministic DOCX rich text | +| [0028](0028-large-document-performance-baseline.md) | Proposed | Synthetic large-document performance baseline before support budgets | ## Decision discipline diff --git a/docs/performance-envelope.md b/docs/performance-envelope.md new file mode 100644 index 00000000..a9b20a9a --- /dev/null +++ b/docs/performance-envelope.md @@ -0,0 +1,62 @@ +# Large-document performance baseline + +Status: Proposed active-PR baseline for Issue #375; not a protected-main support claim + +Protected `main` remains the implementation authority. This first slice adds a +deterministic, synthetic browser smoke for editor mount, snapshot, canonical +envelope serialization, and SHA-256 revision derivation across Chromium, +Firefox, and WebKit. It deliberately does not publish a maximum document size, +latency budget, memory ceiling, device support statement, or Office conversion +budget before repeated exact-head measurements exist. + +## Corpus and privacy boundary + +`tests/browser/performanceCorpus.ts` defines the versioned +`inkspan-large-document-v1` corpus with English, Korean, Japanese, Simplified +and Traditional Chinese, Vietnamese, headings, and deterministic paragraph +profiles. It contains no customer content, tenant identifiers, prompts, +credentials, comment bodies, or network-loaded resources. + +The browser probe reports only profile identity, paragraph count, source length, +snapshot length, revision availability, and bounded elapsed-time measurements. +It never records document text, serialized envelopes, revision digests, or host +identity. Playwright attachments are measurement artifacts for the active run, +not release or telemetry authority. + +## Measured operations + +For each profile and browser engine, the smoke records: + +- React editor mount through `CwlEditor`; +- detached snapshot generation; +- canonical envelope JSON serialization; and +- SHA-256 revision derivation. + +The current gate checks that measurements are finite, non-negative, revision +derivation succeeds, and snapshot output is non-empty. It does not compare +against an invented threshold. A later baseline PR must add repeated samples, +p50/p75/p95/maximum aggregation, trusted input and IME operations, autosave, +collaboration, print, and Office measurements before proposing support budgets. + +## Evidence boundary and next step + +This is PR-CI smoke evidence only. It is not the protected release browser +evidence contract, and it does not establish support for a browser, device, +Node runtime, Python runtime, document size, or memory envelope. The next +version must bind repeated measurements to exact hardware/runtime/browser +identity and immutable fixture hashes, then propose an accepted ADR and +regression rule from observed data. + +Event Timing is used only as standards context for why input latency should be +measured; its current document is a W3C Working Draft, not an Inkspan +certification threshold (World Wide Web Consortium, 2026). Tiptap's public DOCX +performance report is market evidence that large-document conversion warrants +measurement, not an Inkspan performance claim (Isik, 2025). + +## References + +Isik, P. (2025, June 24). *Faster, lighter, and more reliable DOCX +import/export with Tiptap*. Tiptap. https://tiptap.dev/blog/release-notes/faster-lighter-and-more-reliable-docx-import-export-with-tiptap + +World Wide Web Consortium. (2026, March 19). *Event Timing API* (Working +Draft). https://www.w3.org/TR/event-timing/ diff --git a/scripts/revision-evidence-consumer-config.test.mjs b/scripts/revision-evidence-consumer-config.test.mjs index ccc0cfac..17f31546 100644 --- a/scripts/revision-evidence-consumer-config.test.mjs +++ b/scripts/revision-evidence-consumer-config.test.mjs @@ -88,7 +88,10 @@ test('copies the locked pnpm dependency tree into the independent consumer', () true, ); const stagedPackage = realpathSync(join(targetNodeModules, 'example')); - assert.equal(relative(targetNodeModules, stagedPackage).startsWith('..'), false); + assert.equal( + relative(realpathSync(targetNodeModules), stagedPackage).startsWith('..'), + false, + ); } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/scripts/verify-framework-free-revision-evidence-package.mjs b/scripts/verify-framework-free-revision-evidence-package.mjs index 118cadb0..8f3eacf0 100644 --- a/scripts/verify-framework-free-revision-evidence-package.mjs +++ b/scripts/verify-framework-free-revision-evidence-package.mjs @@ -41,7 +41,10 @@ function run(command, argumentsList, cwd = repositoryRoot) { /** Assert that a resolved path remains inside the framework-free consumer. */ function assertInsideConsumer(resolvedPath, description) { - const relativePath = relative(consumerDirectory, realpathSync(resolvedPath)); + const relativePath = relative( + realpathSync(consumerDirectory), + realpathSync(resolvedPath), + ); assert.equal(isAbsolute(relativePath), false, description); assert.equal( relativePath === '..' || relativePath.startsWith(`..${sep}`), diff --git a/scripts/verify-revision-evidence-package.mjs b/scripts/verify-revision-evidence-package.mjs index c0a5f0e3..dade16ca 100644 --- a/scripts/verify-revision-evidence-package.mjs +++ b/scripts/verify-revision-evidence-package.mjs @@ -77,7 +77,10 @@ function readInstalledDependencyVersion(packageName) { /** Assert that a resolved path cannot escape the independent consumer tree. */ function assertPathInsideConsumer(resolvedPath, description) { - const relativePath = relative(verificationDirectory, resolvedPath); + const relativePath = relative( + realpathSync(verificationDirectory), + realpathSync(resolvedPath), + ); assert.equal( isAbsolute(relativePath), false, @@ -258,7 +261,7 @@ import { isAbsolute, relative, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as editor from '${packageJson.name}'; -const consumerDirectory = ${JSON.stringify(verificationDirectory)}; +const consumerDirectory = ${JSON.stringify(realpathSync(verificationDirectory))}; function assertInsideConsumer(resolvedPath, description) { const resolvedRelative = relative(consumerDirectory, resolvedPath); assert.equal(isAbsolute(resolvedRelative), false, description); @@ -270,7 +273,7 @@ function assertInsideConsumer(resolvedPath, description) { } const resolvedEntry = fileURLToPath(import.meta.resolve('${packageJson.name}')); assertInsideConsumer(resolvedEntry, 'packed ESM entry escaped consumer tree'); -const packageRelative = relative(${JSON.stringify(packageDirectory)}, resolvedEntry); +const packageRelative = relative(${JSON.stringify(realpathSync(packageDirectory))}, resolvedEntry); assert.equal(isAbsolute(packageRelative), false); assert.equal( packageRelative === '..' || packageRelative.startsWith('..' + sep), @@ -329,7 +332,7 @@ const editor = require('${packageJson.name}'); void (async () => { const resolvedEntry = require.resolve('${packageJson.name}'); - const resolvedRelative = relative(${JSON.stringify(packageDirectory)}, resolvedEntry); + const resolvedRelative = relative(${JSON.stringify(realpathSync(packageDirectory))}, resolvedEntry); assert.equal(isAbsolute(resolvedRelative), false); assert.equal( resolvedRelative === '..' || resolvedRelative.startsWith('..' + sep), diff --git a/src/performanceBenchmarkDocumentation.test.ts b/src/performanceBenchmarkDocumentation.test.ts new file mode 100644 index 00000000..16a15e60 --- /dev/null +++ b/src/performanceBenchmarkDocumentation.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +function repositoryFile(path: string): string { + return readFileSync(resolve(process.cwd(), path), 'utf8'); +} + +describe('large-document performance baseline documentation', () => { + it('keeps the proposed baseline distinct from a support claim', () => { + const envelope = repositoryFile('docs/performance-envelope.md'); + const adr = repositoryFile( + 'docs/adr/0028-large-document-performance-baseline.md', + ); + + expect(envelope).toContain('Status: Proposed active-PR baseline'); + expect(envelope).toContain('inkspan-large-document-v1'); + expect(envelope).toContain('does not publish a maximum document size'); + expect(adr).toContain('- Status: Proposed'); + expect(adr).toContain('No benchmark code adds transport'); + }); + + it('keeps the browser gate and smoke spec discoverable', () => { + const config = repositoryFile('tests/browser/playwright.config.ts'); + const spec = repositoryFile( + 'tests/browser/specs/performance.browser.spec.ts', + ); + const corpus = repositoryFile('tests/browser/performanceCorpus.ts'); + const harness = repositoryFile('tests/browser/harness.ts'); + + expect(config).toContain('performance'); + expect(spec).toContain('PERFORMANCE_CORPUS_VERSION'); + expect(spec).toContain('without body telemetry'); + expect(corpus).toContain("'inkspan-large-document-v1'"); + expect(harness).toContain('runInkspanDocumentPerformanceProbe'); + }); +}); diff --git a/tests/browser/harness.ts b/tests/browser/harness.ts index c00b47a7..2cff6868 100644 --- a/tests/browser/harness.ts +++ b/tests/browser/harness.ts @@ -1,4 +1,6 @@ import { Editor } from '@tiptap/core'; +import { createElement, useRef } from 'react'; +import { createRoot } from 'react-dom/client'; import { ClipboardSanitizationError, buildExtensions, @@ -6,6 +8,10 @@ import { type ClipboardConfig, type ClipboardSanitizationErrorCode, } from 'inkspan-browser-under-test'; +import { + CwlEditor, + type CwlEditorHandle, +} from 'inkspan-browser-under-test'; interface BrowserClipboardProbeRequest { readonly sourceHtml: string; @@ -23,6 +29,16 @@ interface BrowserHostileDocumentProbeResult { readonly message: string; } +interface BrowserPerformanceProbeResult { + readonly mountMillis: number; + readonly snapshotMillis: number; + readonly envelopeMillis: number; + readonly revisionMillis: number; + readonly sourceCodeUnits: number; + readonly snapshotCodeUnits: number; + readonly revisionAvailable: boolean; +} + declare global { interface Window { runInkspanClipboardProbe( @@ -31,6 +47,9 @@ declare global { runInkspanHostileDocumentProbe( sourceHtml: string, ): BrowserHostileDocumentProbeResult; + runInkspanDocumentPerformanceProbe( + sourceValue: string, + ): Promise; } } @@ -86,4 +105,65 @@ window.runInkspanHostileDocumentProbe = ( } }; +window.runInkspanDocumentPerformanceProbe = async ( + sourceValue: string, +): Promise => { + const harness = document.querySelector('#harness'); + if (!harness) throw new Error('Performance harness root is missing.'); + harness.innerHTML = ''; + const mount = document.createElement('div'); + mount.id = 'performance-probe'; + harness.append(mount); + + return new Promise((resolve, reject) => { + const root = createRoot(mount); + const finish = (callback: () => void): void => { + callback(); + root.unmount(); + mount.remove(); + }; + const started = performance.now(); + const Probe = () => { + const ref = useRef(null); + return createElement(CwlEditor, { + ref, + defaultValue: sourceValue, + hideToolbar: true, + onReady: async () => { + const editorHandle = ref.current; + if (!editorHandle) { + finish(() => reject(new Error('Performance editor handle is unavailable.'))); + return; + } + const mountMillis = performance.now() - started; + const snapshotStarted = performance.now(); + const snapshot = editorHandle.getSnapshot(); + const snapshotMillis = performance.now() - snapshotStarted; + const envelopeStarted = performance.now(); + editorHandle.getDocumentEnvelopeJson(); + const envelopeMillis = performance.now() - envelopeStarted; + const revisionStarted = performance.now(); + const revision = await editorHandle.getDocumentEnvelopeRevision(); + const revisionMillis = performance.now() - revisionStarted; + finish(() => + resolve( + Object.freeze({ + mountMillis, + snapshotMillis, + envelopeMillis, + revisionMillis, + sourceCodeUnits: sourceValue.length, + snapshotCodeUnits: snapshot.plainText.length, + revisionAvailable: revision !== null, + }), + ), + ); + }, + }); + }; + + root.render(createElement(Probe)); + }); +}; + export {}; diff --git a/tests/browser/performanceCorpus.ts b/tests/browser/performanceCorpus.ts new file mode 100644 index 00000000..76271c9e --- /dev/null +++ b/tests/browser/performanceCorpus.ts @@ -0,0 +1,42 @@ +/** Versioned synthetic corpus used by the initial large-document browser smoke. */ +export const PERFORMANCE_CORPUS_VERSION = 'inkspan-large-document-v1' as const; + +export interface PerformanceDocumentProfile { + readonly id: 'small' | 'medium' | 'large'; + readonly value: string; + readonly paragraphCount: number; +} + +const PARAGRAPH_SEEDS = [ + 'English heading and paragraph with a deterministic link.', + '한국어 문장과 한글 조합 입력을 위한 합성 문단입니다.', + '日本語の見出しと文章を含む決定的なテスト段落です。', + '简体中文和繁體中文的混合段落用于编辑性能测试。', + 'Đoạn văn tiếng Việt có dấu và mixed Latin content.', +] as const; + +function paragraph(index: number): string { + const seed = PARAGRAPH_SEEDS[index % PARAGRAPH_SEEDS.length]; + return `### Section ${index + 1}\n\n${seed} Item ${index + 1}.`; +} + +function profile( + id: PerformanceDocumentProfile['id'], + paragraphCount: number, +): PerformanceDocumentProfile { + return Object.freeze({ + id, + paragraphCount, + value: Array.from({ length: paragraphCount }, (_, index) => paragraph(index)).join( + '\n\n', + ), + }); +} + +/** Small-to-large profiles keep the first gate deterministic and content-safe. */ +export const PERFORMANCE_DOCUMENT_PROFILES: readonly PerformanceDocumentProfile[] = + Object.freeze([ + profile('small', 20), + profile('medium', 100), + profile('large', 300), + ]); diff --git a/tests/browser/playwright.config.ts b/tests/browser/playwright.config.ts index 375056da..ed2ff8e1 100644 --- a/tests/browser/playwright.config.ts +++ b/tests/browser/playwright.config.ts @@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; const HARNESS_ORIGIN = 'http://127.0.0.1:4173'; const HARNESS_URL = `${HARNESS_ORIGIN}/tests/browser/harness.html`; -const ENGINE_BROWSER_SPECS = /(?:clipboard|print)\.browser\.spec\.ts/u; +const ENGINE_BROWSER_SPECS = /(?:clipboard|performance|print)\.browser\.spec\.ts/u; export default defineConfig({ testDir: './specs', @@ -44,4 +44,4 @@ export default defineConfig({ dependencies: ['chromium', 'firefox', 'webkit'], }, ], -}); \ No newline at end of file +}); diff --git a/tests/browser/specs/performance.browser.spec.ts b/tests/browser/specs/performance.browser.spec.ts new file mode 100644 index 00000000..ee012706 --- /dev/null +++ b/tests/browser/specs/performance.browser.spec.ts @@ -0,0 +1,88 @@ +import { expect, test } from '@playwright/test'; +import { + PERFORMANCE_CORPUS_VERSION, + PERFORMANCE_DOCUMENT_PROFILES, + type PerformanceDocumentProfile, +} from '../performanceCorpus.js'; + +interface BrowserPerformanceProbeResult { + readonly mountMillis: number; + readonly snapshotMillis: number; + readonly envelopeMillis: number; + readonly revisionMillis: number; + readonly sourceCodeUnits: number; + readonly snapshotCodeUnits: number; + readonly revisionAvailable: boolean; +} + +declare global { + interface Window { + runInkspanDocumentPerformanceProbe( + sourceValue: string, + ): Promise; + } +} + +const HARNESS_URL = 'http://127.0.0.1:4173/tests/browser/harness.html'; + +const allowHarnessRequest = (requestUrl: string): boolean => { + const url = new URL(requestUrl); + return url.hostname === '127.0.0.1' && url.port === '4173'; +}; + +function assertMeasurement( + profile: PerformanceDocumentProfile, + result: BrowserPerformanceProbeResult, +): void { + for (const value of [ + result.mountMillis, + result.snapshotMillis, + result.envelopeMillis, + result.revisionMillis, + ]) { + expect(Number.isFinite(value)).toBe(true); + expect(value).toBeGreaterThanOrEqual(0); + } + expect(result.sourceCodeUnits).toBe(profile.value.length); + expect(result.snapshotCodeUnits).toBeGreaterThan(0); + expect(result.revisionAvailable).toBe(true); +} + +test.describe.configure({ mode: 'serial' }); + +test.beforeEach(async ({ page }) => { + const rejectedExternalRequests: string[] = []; + await page.route('**/*', async (route) => { + if (allowHarnessRequest(route.request().url())) { + await route.continue(); + return; + } + rejectedExternalRequests.push(new URL(route.request().url()).origin); + await route.abort('blockedbyclient'); + }); + await page.goto(HARNESS_URL); + expect(rejectedExternalRequests).toEqual([]); +}); + +for (const profile of PERFORMANCE_DOCUMENT_PROFILES) { + test(`measures ${profile.id} synthetic document without body telemetry`, async ({ + page, + }) => { + test.setTimeout(30_000); + const result = await page.evaluate( + async (sourceValue) => + window.runInkspanDocumentPerformanceProbe(sourceValue), + profile.value, + ); + assertMeasurement(profile, result); + await test.info().attach(`${profile.id}-${PERFORMANCE_CORPUS_VERSION}.json`, { + body: JSON.stringify({ + corpusVersion: PERFORMANCE_CORPUS_VERSION, + profile: profile.id, + paragraphCount: profile.paragraphCount, + ...result, + }), + contentType: 'application/json', + }); + }); +} diff --git a/tests/package/verify-framework-free-autosave-package.mjs b/tests/package/verify-framework-free-autosave-package.mjs index 807dcc05..422ae78d 100644 --- a/tests/package/verify-framework-free-autosave-package.mjs +++ b/tests/package/verify-framework-free-autosave-package.mjs @@ -45,7 +45,10 @@ function run(command, argumentsList, cwd = repositoryRoot) { /** Assert that a resolved path remains inside the isolated consumer tree. */ function assertInsideConsumer(resolvedPath, description) { - const relativePath = relative(consumerDirectory, realpathSync(resolvedPath)); + const relativePath = relative( + realpathSync(consumerDirectory), + realpathSync(resolvedPath), + ); assert.equal(isAbsolute(relativePath), false, description); assert.equal( relativePath === '..' || relativePath.startsWith(`..${sep}`),