210 lines
5.8 KiB
TypeScript
210 lines
5.8 KiB
TypeScript
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
|
||
|
|
type LocalStorageMock = {
|
||
|
|
clear(): void;
|
||
|
|
getItem(key: string): string | null;
|
||
|
|
setItem(key: string, value: string): void;
|
||
|
|
removeItem(key: string): void;
|
||
|
|
};
|
||
|
|
|
||
|
|
function installLocalStorage(): LocalStorageMock {
|
||
|
|
const store = new Map<string, string>();
|
||
|
|
const localStorage: LocalStorageMock = {
|
||
|
|
clear() { store.clear(); },
|
||
|
|
getItem(key) { return store.has(key) ? store.get(key)! : null; },
|
||
|
|
setItem(key, value) { store.set(key, String(value)); },
|
||
|
|
removeItem(key) { store.delete(key); },
|
||
|
|
};
|
||
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
||
|
|
value: localStorage,
|
||
|
|
configurable: true,
|
||
|
|
});
|
||
|
|
return localStorage;
|
||
|
|
}
|
||
|
|
|
||
|
|
const localStorage = installLocalStorage();
|
||
|
|
|
||
|
|
const { SYNC_STORAGE_KEYS } = await import('../domain/sync.ts');
|
||
|
|
const { localStorageAdapter } = await import('../infrastructure/persistence/localStorageAdapter.ts');
|
||
|
|
const {
|
||
|
|
collectPluginSyncSidecarsFromHost,
|
||
|
|
commitPluginSidecarsLastKnown,
|
||
|
|
isPluginSidecarHostReady,
|
||
|
|
} = await import('./pluginSyncSidecarBridge.ts');
|
||
|
|
const { hasMeaningfulCloudSyncData } = await import('./syncPayload.ts');
|
||
|
|
|
||
|
|
test.beforeEach(() => {
|
||
|
|
localStorage.clear();
|
||
|
|
delete (globalThis as { window?: unknown }).window;
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isPluginSidecarHostReady follows pluginHostReady probe when present', () => {
|
||
|
|
(globalThis as { window: unknown }).window = {
|
||
|
|
netcatty: {
|
||
|
|
pluginHostReady: () => true,
|
||
|
|
async collectPluginSyncSidecars() {
|
||
|
|
return { version: 1, entries: [] };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
assert.equal(isPluginSidecarHostReady(), true);
|
||
|
|
|
||
|
|
(globalThis as { window: unknown }).window = {
|
||
|
|
netcatty: {
|
||
|
|
pluginHostReady: () => false,
|
||
|
|
async collectPluginSyncSidecars() {
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
assert.equal(isPluginSidecarHostReady(), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('collect defers empty last-known until commit (empty-vault guard ignores last-known alone)', async () => {
|
||
|
|
localStorageAdapter.write(SYNC_STORAGE_KEYS.PLUGIN_SIDECARS_LAST_KNOWN, {
|
||
|
|
version: 1,
|
||
|
|
entries: [{
|
||
|
|
pluginId: 'com.example.p',
|
||
|
|
kind: 'settings',
|
||
|
|
key: 'k',
|
||
|
|
value: 1,
|
||
|
|
updatedAt: 1,
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
(globalThis as { window: unknown }).window = {
|
||
|
|
netcatty: {
|
||
|
|
async collectPluginSyncSidecars() {
|
||
|
|
return { version: 1, entries: [] };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const collected = await collectPluginSyncSidecarsFromHost();
|
||
|
|
assert.deepEqual(collected, { version: 1, entries: [] });
|
||
|
|
|
||
|
|
// Last-known still holds prior entries until commit, but that alone must not
|
||
|
|
// bypass the empty-vault upload guard.
|
||
|
|
const lastKnown = localStorageAdapter.read<{ entries: unknown[] }>(
|
||
|
|
SYNC_STORAGE_KEYS.PLUGIN_SIDECARS_LAST_KNOWN,
|
||
|
|
);
|
||
|
|
assert.equal(lastKnown?.entries?.length, 1);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
hasMeaningfulCloudSyncData({
|
||
|
|
hosts: [],
|
||
|
|
keys: [],
|
||
|
|
identities: [],
|
||
|
|
snippets: [],
|
||
|
|
customGroups: [],
|
||
|
|
syncedAt: 1,
|
||
|
|
pluginSidecars: { version: 1, entries: [] },
|
||
|
|
}),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
|
||
|
|
commitPluginSidecarsLastKnown({ version: 1, entries: [] });
|
||
|
|
const committed = localStorageAdapter.read<{ entries: unknown[] }>(
|
||
|
|
SYNC_STORAGE_KEYS.PLUGIN_SIDECARS_LAST_KNOWN,
|
||
|
|
);
|
||
|
|
assert.deepEqual(committed?.entries, []);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('liveOnly collect returns null instead of last-known when host is gated off', async () => {
|
||
|
|
localStorageAdapter.write(SYNC_STORAGE_KEYS.PLUGIN_SIDECARS_LAST_KNOWN, {
|
||
|
|
version: 1,
|
||
|
|
entries: [{
|
||
|
|
pluginId: 'com.example.p',
|
||
|
|
kind: 'settings',
|
||
|
|
key: 'k',
|
||
|
|
value: 1,
|
||
|
|
updatedAt: 1,
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
(globalThis as { window: unknown }).window = {
|
||
|
|
netcatty: {
|
||
|
|
async collectPluginSyncSidecars() {
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
assert.equal(await collectPluginSyncSidecarsFromHost({ liveOnly: true }), null);
|
||
|
|
const fallback = await collectPluginSyncSidecarsFromHost();
|
||
|
|
assert.equal(fallback?.entries?.length, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('liveOnly collect omits pending remote when replay cannot apply', async () => {
|
||
|
|
localStorageAdapter.write(SYNC_STORAGE_KEYS.PLUGIN_SIDECARS_PENDING_REMOTE, {
|
||
|
|
version: 1,
|
||
|
|
entries: [{
|
||
|
|
pluginId: 'com.example.remote',
|
||
|
|
kind: 'settings',
|
||
|
|
key: 'k',
|
||
|
|
value: 'remote',
|
||
|
|
updatedAt: 2,
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
(globalThis as { window: unknown }).window = {
|
||
|
|
netcatty: {
|
||
|
|
async collectPluginSyncSidecars() {
|
||
|
|
return { version: 1, entries: [] };
|
||
|
|
},
|
||
|
|
// no applyPluginSyncSidecars → pending cannot replay
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
assert.equal(await collectPluginSyncSidecarsFromHost({ liveOnly: true }), null);
|
||
|
|
const uploadPath = await collectPluginSyncSidecarsFromHost();
|
||
|
|
assert.equal(uploadPath?.entries?.[0]?.value, 'remote');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('commitPluginSidecarsAfterSuccessfulSync prefers merged payload sidecars', async () => {
|
||
|
|
const { commitPluginSidecarsAfterSuccessfulSync } = await import('./pluginSyncSidecarBridge.ts');
|
||
|
|
localStorageAdapter.write(SYNC_STORAGE_KEYS.PLUGIN_SIDECARS_LAST_KNOWN, {
|
||
|
|
version: 1,
|
||
|
|
entries: [{
|
||
|
|
pluginId: 'com.example.old',
|
||
|
|
kind: 'settings',
|
||
|
|
key: 'k',
|
||
|
|
value: 'old',
|
||
|
|
updatedAt: 1,
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
|
||
|
|
commitPluginSidecarsAfterSuccessfulSync(
|
||
|
|
{
|
||
|
|
pluginSidecars: {
|
||
|
|
version: 1,
|
||
|
|
entries: [{
|
||
|
|
pluginId: 'com.example.local',
|
||
|
|
kind: 'settings',
|
||
|
|
key: 'k',
|
||
|
|
value: 'local',
|
||
|
|
updatedAt: 2,
|
||
|
|
}],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
[{
|
||
|
|
success: true,
|
||
|
|
mergedPayload: {
|
||
|
|
pluginSidecars: {
|
||
|
|
version: 1,
|
||
|
|
entries: [{
|
||
|
|
pluginId: 'com.example.merged',
|
||
|
|
kind: 'settings',
|
||
|
|
key: 'k',
|
||
|
|
value: 'merged',
|
||
|
|
updatedAt: 3,
|
||
|
|
}],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}],
|
||
|
|
);
|
||
|
|
|
||
|
|
const committed = localStorageAdapter.read<{ entries: Array<{ value: string }> }>(
|
||
|
|
SYNC_STORAGE_KEYS.PLUGIN_SIDECARS_LAST_KNOWN,
|
||
|
|
);
|
||
|
|
assert.equal(committed?.entries?.[0]?.value, 'merged');
|
||
|
|
});
|