- chat: left-aligned discord-style messages with grouped senders, hover actions, an auto-growing composer and file cards - send: render messages and files locally first and replace the placeholder when the worker publishes the real item; failures keep the text with a retry action - navigation: space rail plus a flatter channel list with the space name header and collapsible groups - members: right pane tabs (files / members) grouped by online, offline and pending users, with a prominent invite action and a top bar shortcut - notifications: toast host for invite results; invitees get a toast with an accept action, and the invitation payload now carries the space name and inviter inside the encrypted grant - users over devices: drop device names, the device list and device rename/release; senders show the account (guests show as guest) and the share panel only keeps links - follow contract 7.0.0 (max_members, members_used, members_full, /join) - docs: ui-design and frontend updated for the new layout and concepts
29 lines
1.9 KiB
TypeScript
29 lines
1.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { exportResource, mergeVault, type VaultProfile } from './accountVault';
|
|
import type { SavedResource } from '../database';
|
|
const resource = (modifiedAt = 10): SavedResource => ({ id: 'AAAAAAAAAAAAAAAAAAAAAA', kind: 'channel', mode: 'enc', role: 'owner',
|
|
link: 'https://drop.test/c/a#k=secret', endpointSecret: new Uint8Array(32).fill(7), localScope: 'account:alice',
|
|
createdAt: 1, modifiedAt, expiresAt: 100, maxMembers: 3, name: `version-${modifiedAt}` });
|
|
const profile = (): VaultProfile => ({ v: 3, keys: { publicKey: {}, privateKey: {}, signing: { publicKey: {}, privateKey: {} } }, resources: [exportResource(resource(20))], deletes: [] });
|
|
describe('account vault portability and merge', () => {
|
|
it('never synchronizes device identity or local scope', () => {
|
|
const exported = exportResource(resource());
|
|
expect(exported).not.toHaveProperty('endpointSecret');
|
|
expect(exported).not.toHaveProperty('localScope'); expect(exported.link).toContain('secret');
|
|
});
|
|
it('a stale device does not overwrite a newer remote resource', () => {
|
|
expect(mergeVault(profile(), [resource(10)], []).resources[0]?.name).toBe('version-20');
|
|
});
|
|
it('resource removal survives a stale device syncing its old copy', () => {
|
|
const remote = { ...profile(), resources: [], deletes: [{ id: resource().id, modifiedAt: 30 }] };
|
|
expect(mergeVault(remote, [resource(20)], []).resources).toEqual([]);
|
|
expect(mergeVault(profile(), [], [{ id: resource().id, modifiedAt: 30 }]).resources).toEqual([]);
|
|
});
|
|
it('explicitly rejoining after removal restores the resource', () => {
|
|
const remote = { ...profile(), resources: [], deletes: [{ id: resource().id, modifiedAt: 30 }] };
|
|
const merged = mergeVault(remote, [resource(40)], []);
|
|
expect(merged.resources).toHaveLength(1); expect(merged.deletes).toEqual([]);
|
|
});
|
|
|
|
});
|