mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
The settings show the stored status of the last sync. A sync that finds nothing to do still replaced it, so a change the server refused and the phone put back, or a conflict, was shown for a second or two and then gone. The previous fix covered only the upload sync Android starts right after a manual one. When changes arrive during a sync, the adapter runs a full sync right after it, which finds nothing new on the server and replaced the status 1-2 s after the real one; a manual sync right after another does the same. Now what counts is what a run did, not how it started. A run that wrote nothing either way and reported no conflict, item error, note or failure keeps the details of the run before it and moves only the time on. After a failed run it shows the sync as working again, without the failure's message. Any other run replaces the status, so a problem stays only until the next run that does something.
76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
// The headless task entry: every run ends with finishRun and nothing
|
|
// escapes it (docs/device-sync.md, "The Android shell").
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../../native', () => ({
|
|
createProviderPort: vi.fn(),
|
|
finishRun: vi.fn(async () => true),
|
|
getSyncSettings: vi.fn(async () => ({
|
|
masterAutomatic: true,
|
|
authorities: { 'com.android.contacts': { automatic: false, syncable: 1, periodicSeconds: 0, active: false, pending: false } },
|
|
})),
|
|
isRunCancelled: vi.fn(async () => false),
|
|
showSyncProblem: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
import * as native from '../../native';
|
|
import { emptyStats } from '../../engine/report';
|
|
import { createTaskDeps, runDeviceSyncTask, teardownAuthority } from '../../task';
|
|
import { useDeviceSyncStore } from '../../../stores/device-sync-store';
|
|
import type { RunPayload } from '../../types';
|
|
|
|
const payload: RunPayload = {
|
|
runId: 'r1',
|
|
accountName: 'alice@example.com',
|
|
registryId: 'alice@mail.example.com',
|
|
authority: 'com.android.contacts',
|
|
extras: {},
|
|
deadline: Date.now() + 60_000,
|
|
};
|
|
|
|
describe('device sync task', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(native.finishRun).mockClear();
|
|
});
|
|
|
|
it('hands the report to the adapter before settling, and records the status', async () => {
|
|
await runDeviceSyncTask(payload);
|
|
|
|
expect(native.finishRun).toHaveBeenCalledWith('r1', expect.objectContaining({ v: 1, runId: 'r1', outcome: 'disabled' }));
|
|
expect(useDeviceSyncStore.getState().accounts['alice@mail.example.com']?.lastRun?.['com.android.contacts']).toMatchObject({
|
|
outcome: 'disabled',
|
|
});
|
|
});
|
|
|
|
it('reads back the status it records, for a run that did nothing to keep', async () => {
|
|
const deps = createTaskDeps();
|
|
const status = { at: 5, outcome: 'ok' as const, durationMs: 3, conflicts: 1, itemErrors: 2, stats: emptyStats() };
|
|
|
|
await deps.recordStatus('bob@mail.example.com', 'com.android.calendar', status);
|
|
|
|
await expect(deps.lastStatus('bob@mail.example.com', 'com.android.calendar')).resolves.toEqual(status);
|
|
await expect(deps.lastStatus('bob@mail.example.com', 'com.android.contacts')).resolves.toBeUndefined();
|
|
await expect(deps.lastStatus('nobody@mail.example.com', 'com.android.calendar')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('reports a failure instead of throwing, and survives a refused finishRun', async () => {
|
|
vi.mocked(native.getSyncSettings).mockRejectedValueOnce(new Error('module gone'));
|
|
vi.mocked(native.finishRun).mockRejectedValueOnce(new Error('bridge down'));
|
|
|
|
await expect(runDeviceSyncTask({ ...payload, runId: 'r2' })).resolves.toBeUndefined();
|
|
|
|
expect(native.finishRun).toHaveBeenCalledWith('r2', expect.objectContaining({ outcome: 'internal', message: 'module gone' }));
|
|
});
|
|
|
|
it('ignores a payload without a run', async () => {
|
|
await runDeviceSyncTask({} as RunPayload);
|
|
expect(native.finishRun).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('exports the teardown the app calls', () => {
|
|
expect(teardownAuthority).toBeTypeOf('function');
|
|
expect(teardownAuthority.length).toBe(4);
|
|
});
|
|
});
|