mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(collab): synchronize owner devices with overwrite consent * docs(collab): add owner device synchronization QA handoff * chore(collab): move delivery reports and screenshots to Plane * fix(collab): preserve owner pull fallback and ghost heal Generated-By: looper 0.16.1 (runner=fixer, agent=codex) * fix(collab): probe null owner heads for retractions Generated-By: looper 0.16.1 (runner=fixer, agent=codex) * fix(collab): preserve manual checks across coalescing Generated-By: looper 0.16.1 (runner=fixer, agent=codex) * feat(collab): hide update check behind a dismissed-version badge Show the header entry only when the owner skipped a cloud version, as a "Cloud update available" badge that re-opens the overwrite dialog. Rewrite the dialog description in plain product language. * fix(collab): clarify device sync copy and place entry before present * fix(collab): silence update reminders without automatic overwrite --------- Co-authored-by: Looper <looper@noreply.github.com> Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { createCollabPublishWatcher } from '../src/collab/collab-publish-watcher.js';
|
|
import type { ResourceHubPrincipal } from '../src/collab/resource-principal.js';
|
|
|
|
describe('collab publish watcher', () => {
|
|
it('reattaches after directory promotion without publishing received content', async () => {
|
|
const notifyChanged = vi.fn();
|
|
const unsubscribe = vi.fn();
|
|
const handlers: Array<() => void> = [];
|
|
const watcher = createCollabPublishWatcher({
|
|
notifyChanged, publishInitialContent: false, listProjectIds: () => ['p1'],
|
|
shouldPublish: async () => true,
|
|
subscribeFiles: (_id, handler) => { handlers.push(handler); return { unsubscribe }; },
|
|
});
|
|
await watcher.reconcile();
|
|
await watcher.refresh('p1');
|
|
expect(unsubscribe).toHaveBeenCalledOnce();
|
|
expect(handlers).toHaveLength(2);
|
|
expect(notifyChanged).not.toHaveBeenCalled();
|
|
handlers[1]!();
|
|
expect(notifyChanged).toHaveBeenCalledWith('p1');
|
|
watcher.dispose();
|
|
});
|
|
|
|
it('publishes current content once when it first watches an owned+shared project', async () => {
|
|
const notifyChanged = vi.fn();
|
|
const onChangeHandlers = new Map<string, () => void>();
|
|
const watcher = createCollabPublishWatcher({
|
|
notifyChanged,
|
|
listProjectIds: () => ['owned-shared', 'someone-elses'],
|
|
shouldPublish: async (projectId) => projectId === 'owned-shared',
|
|
subscribeFiles: (projectId, onChange) => {
|
|
onChangeHandlers.set(projectId, onChange);
|
|
return { unsubscribe: () => {} };
|
|
},
|
|
});
|
|
|
|
await watcher.reconcile();
|
|
|
|
// Owned+shared → subscribed AND an initial publish so existing files (which
|
|
// the file watcher's ignoreInitial would skip) still reach members.
|
|
expect(onChangeHandlers.has('owned-shared')).toBe(true);
|
|
expect(notifyChanged).toHaveBeenCalledTimes(1);
|
|
expect(notifyChanged).toHaveBeenCalledWith('owned-shared');
|
|
// A project owned by someone else is never watched or published from here.
|
|
expect(onChangeHandlers.has('someone-elses')).toBe(false);
|
|
});
|
|
|
|
it('does not re-publish an already-watched project on subsequent reconciles', async () => {
|
|
const notifyChanged = vi.fn();
|
|
const watcher = createCollabPublishWatcher({
|
|
notifyChanged,
|
|
listProjectIds: () => ['p1'],
|
|
shouldPublish: async () => true,
|
|
subscribeFiles: () => ({ unsubscribe: () => {} }),
|
|
});
|
|
|
|
await watcher.reconcile();
|
|
await watcher.reconcile();
|
|
await watcher.reconcile();
|
|
|
|
// Initial publish fires once per watch session, not on every reconcile tick.
|
|
expect(notifyChanged).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('publishes on a later file change through the subscribed handler', async () => {
|
|
const notifyChanged = vi.fn();
|
|
const handler: { onChange: (() => void) | null } = { onChange: null };
|
|
const watcher = createCollabPublishWatcher({
|
|
notifyChanged,
|
|
listProjectIds: () => ['p1'],
|
|
shouldPublish: async () => true,
|
|
subscribeFiles: (_projectId, onChange) => {
|
|
handler.onChange = onChange;
|
|
return { unsubscribe: () => {} };
|
|
},
|
|
});
|
|
|
|
await watcher.reconcile();
|
|
notifyChanged.mockClear();
|
|
handler.onChange?.();
|
|
|
|
expect(notifyChanged).toHaveBeenCalledTimes(1);
|
|
expect(notifyChanged).toHaveBeenCalledWith('p1');
|
|
});
|
|
|
|
it('keeps the verified workspace principal captured by the watch', async () => {
|
|
const principal: ResourceHubPrincipal = {
|
|
teamId: 'workspace-a',
|
|
memberId: 'member-a',
|
|
role: 'owner',
|
|
lifecycleState: 'active',
|
|
workspaceType: 'team',
|
|
};
|
|
const notifyChanged = vi.fn();
|
|
const handler: { onChange: (() => void) | null } = { onChange: null };
|
|
const watcher = createCollabPublishWatcher({
|
|
notifyChanged,
|
|
listProjectIds: () => ['p1'],
|
|
shouldPublish: async () => principal,
|
|
subscribeFiles: (_projectId, onChange) => {
|
|
handler.onChange = onChange;
|
|
return { unsubscribe: () => {} };
|
|
},
|
|
});
|
|
|
|
await watcher.reconcile();
|
|
expect(notifyChanged).toHaveBeenLastCalledWith('p1', principal);
|
|
|
|
notifyChanged.mockClear();
|
|
handler.onChange?.();
|
|
expect(notifyChanged).toHaveBeenCalledOnce();
|
|
expect(notifyChanged).toHaveBeenCalledWith('p1', principal);
|
|
});
|
|
});
|