Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
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');
|
|
});
|