All checks were successful
CD / Build and push images (push) Successful in 2m54s
CI / Lint, typecheck, test (push) Successful in 2m3s
CI / Auth e2e pack (push) Successful in 2m41s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 12s
Users can see who changed what and restore old states (ADR 0013). - shared: dependency-free word-level Markdown diff (diffMarkdown) with a unit test; PageVersionContentView; PAGE_RESTORE_CHANNEL + PageRestoreRequest. - api: GET /pages/:id/versions (list), GET .../:versionId (read-only HTML + Markdown for diffing), POST .../:versionId/restore. Every route requires write access — viewing history is gated like editing (permissions.md). Restore checks permission, then emits the page_restore NOTIFY; history is append-only (the api never deletes a version). - collab: a page_restore listener applies the restore on the live document via openDirectConnection — it snapshots the current state as a PRE_RESTORE version, then replaces the content in one transaction, so every connected client converges and the change persists like a normal edit. - web: HistoryPanel (version list with time/trigger/label/contributors, a read-only render of a selected version, a Markdown diff against the current page, and a restore action), toggled from the page menu. de+en strings. Tests: shared diff (added/removed/round-trip/edges); collab restore DB test (a connected client converges on the restored content; a pre-restore snapshot is appended alongside the original — append-only); api list/get/restore (newest-first, rendered content, write-permission gate, restore returns the target without mutating history). This completes M3 (real-time collaboration & history, #33–#42). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { diffMarkdown } from './text-diff';
|
|
|
|
/** Concatenate the segments of one type back into a string. */
|
|
function only(
|
|
segments: ReturnType<typeof diffMarkdown>,
|
|
type: 'added' | 'removed' | 'equal',
|
|
): string {
|
|
return segments
|
|
.filter((s) => s.type === type)
|
|
.map((s) => s.value)
|
|
.join('');
|
|
}
|
|
|
|
describe('diffMarkdown (issue #42)', () => {
|
|
it('marks added and removed words against a fixture pair', () => {
|
|
const before = 'The quick brown fox';
|
|
const after = 'The slow brown fox jumps';
|
|
const segments = diffMarkdown(before, after);
|
|
|
|
expect(only(segments, 'removed')).toContain('quick');
|
|
expect(only(segments, 'added')).toContain('slow');
|
|
expect(only(segments, 'added')).toContain('jumps');
|
|
// Unchanged words stay in the equal channel.
|
|
expect(only(segments, 'equal')).toContain('brown');
|
|
expect(only(segments, 'equal')).toContain('fox');
|
|
});
|
|
|
|
it('round-trips: equal+removed reconstructs before, equal+added reconstructs after', () => {
|
|
const before = '# Title\n\nHello world, this is old.';
|
|
const after = '# Title\n\nHello brave world, this is new.';
|
|
const segments = diffMarkdown(before, after);
|
|
|
|
const reconstructedBefore = segments
|
|
.filter((s) => s.type !== 'added')
|
|
.map((s) => s.value)
|
|
.join('');
|
|
const reconstructedAfter = segments
|
|
.filter((s) => s.type !== 'removed')
|
|
.map((s) => s.value)
|
|
.join('');
|
|
expect(reconstructedBefore).toBe(before);
|
|
expect(reconstructedAfter).toBe(after);
|
|
});
|
|
|
|
it('returns only equal segments for identical text', () => {
|
|
const text = 'nothing changed here';
|
|
const segments = diffMarkdown(text, text);
|
|
expect(segments.every((s) => s.type === 'equal')).toBe(true);
|
|
expect(only(segments, 'equal')).toBe(text);
|
|
});
|
|
|
|
it('handles empty before (pure insert) and empty after (pure delete)', () => {
|
|
expect(diffMarkdown('', 'brand new')).toEqual([{ type: 'added', value: 'brand new' }]);
|
|
expect(diffMarkdown('all gone', '')).toEqual([{ type: 'removed', value: 'all gone' }]);
|
|
});
|
|
});
|