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
259 lines
7.7 KiB
TypeScript
259 lines
7.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { describe, it } from 'node:test';
|
|
import {
|
|
collectPluginSyncSidecars,
|
|
excludeSecretPluginSettingsFromSidecars,
|
|
isCloudSyncablePluginSetting,
|
|
mergePluginSyncSidecars,
|
|
mergePluginSyncSidecarsThreeWay,
|
|
parseSettingsSidecarKey,
|
|
type PluginSettingFieldDescriptor,
|
|
type PluginSyncSidecarEntry,
|
|
} from './pluginSyncSidecar';
|
|
|
|
const pluginId = 'com.example.sidecar';
|
|
|
|
const declared = new Map<string, PluginSettingFieldDescriptor[]>([
|
|
[pluginId, [
|
|
{ id: `${pluginId}.theme`, sync: true, secret: false },
|
|
{ id: `${pluginId}.token`, sync: true, secret: true },
|
|
{ id: `${pluginId}.localOnly`, sync: false, secret: false },
|
|
]],
|
|
]);
|
|
|
|
describe('pluginSyncSidecar', () => {
|
|
it('rejects secret fields from cloud sync even when sync is true', () => {
|
|
assert.equal(isCloudSyncablePluginSetting({ sync: true, secret: true }), false);
|
|
assert.equal(isCloudSyncablePluginSetting({ sync: true, secret: false }), true);
|
|
assert.equal(isCloudSyncablePluginSetting({ sync: false, secret: false }), false);
|
|
});
|
|
|
|
it('collects only non-secret sync:true settings into the sidecar bundle', () => {
|
|
const bundle = collectPluginSyncSidecars({
|
|
declaredSettingsByPlugin: declared,
|
|
storedSettings: [
|
|
{
|
|
pluginId,
|
|
settingId: `${pluginId}.theme`,
|
|
scope: 'application',
|
|
scopeId: 'application',
|
|
value: 'dark',
|
|
updatedAt: 10,
|
|
},
|
|
{
|
|
pluginId,
|
|
settingId: `${pluginId}.token`,
|
|
scope: 'application',
|
|
scopeId: 'application',
|
|
value: 'should-not-sync',
|
|
updatedAt: 10,
|
|
},
|
|
{
|
|
pluginId,
|
|
settingId: `${pluginId}.localOnly`,
|
|
scope: 'application',
|
|
scopeId: 'application',
|
|
value: true,
|
|
updatedAt: 10,
|
|
},
|
|
],
|
|
now: 20,
|
|
});
|
|
|
|
assert.equal(bundle.version, 1);
|
|
assert.equal(bundle.entries.length, 1);
|
|
assert.equal(bundle.entries[0].kind, 'settings');
|
|
assert.equal(bundle.entries[0].value, 'dark');
|
|
assert.deepEqual(parseSettingsSidecarKey(bundle.entries[0].key), {
|
|
settingId: `${pluginId}.theme`,
|
|
scope: 'application',
|
|
scopeId: 'application',
|
|
});
|
|
});
|
|
|
|
it('preserves account and CRDT baselines when the plugin is missing', () => {
|
|
const existing: PluginSyncSidecarEntry[] = [
|
|
{
|
|
pluginId: 'com.missing.plugin',
|
|
kind: 'account_baseline',
|
|
key: 'account',
|
|
value: { accountId: 'acct-1' },
|
|
updatedAt: 5,
|
|
},
|
|
{
|
|
pluginId: 'com.missing.plugin',
|
|
kind: 'crdt_baseline',
|
|
key: 'replica',
|
|
value: { clock: 3 },
|
|
updatedAt: 5,
|
|
},
|
|
{
|
|
pluginId: 'com.missing.plugin',
|
|
kind: 'settings',
|
|
key: 'com.missing.plugin.theme\0application\0application',
|
|
value: 'light',
|
|
updatedAt: 5,
|
|
},
|
|
];
|
|
|
|
const bundle = collectPluginSyncSidecars({
|
|
declaredSettingsByPlugin: new Map(), // plugin not installed
|
|
storedSettings: [],
|
|
existingSidecars: existing,
|
|
now: 100,
|
|
});
|
|
|
|
assert.equal(bundle.entries.length, 3);
|
|
assert.ok(bundle.entries.some((entry) => entry.kind === 'account_baseline'));
|
|
assert.ok(bundle.entries.some((entry) => entry.kind === 'crdt_baseline'));
|
|
assert.ok(bundle.entries.some((entry) => entry.kind === 'settings' && entry.value === 'light'));
|
|
});
|
|
|
|
it('merges remote sidecars without dropping local baselines for missing remote plugins', () => {
|
|
const local: PluginSyncSidecarEntry[] = [
|
|
{
|
|
pluginId: 'com.local.only',
|
|
kind: 'account_baseline',
|
|
key: 'account',
|
|
value: { id: 'local' },
|
|
updatedAt: 1,
|
|
},
|
|
];
|
|
const remote = {
|
|
version: 1 as const,
|
|
entries: [
|
|
{
|
|
pluginId,
|
|
kind: 'settings' as const,
|
|
key: `${pluginId}.theme\0application\0application`,
|
|
value: 'dark',
|
|
updatedAt: 2,
|
|
},
|
|
],
|
|
};
|
|
|
|
const merged = mergePluginSyncSidecars({ local, remote, declaredSettingsByPlugin: declared });
|
|
assert.equal(merged.length, 2);
|
|
assert.ok(merged.some((entry) => entry.pluginId === 'com.local.only'));
|
|
assert.ok(merged.some((entry) => entry.value === 'dark'));
|
|
});
|
|
|
|
it('excludes secret settings when stripping sidecars with known declarations', () => {
|
|
const entries: PluginSyncSidecarEntry[] = [
|
|
{
|
|
pluginId,
|
|
kind: 'settings',
|
|
key: `${pluginId}.token\0application\0application`,
|
|
value: 'secret',
|
|
updatedAt: 1,
|
|
},
|
|
{
|
|
pluginId,
|
|
kind: 'settings',
|
|
key: `${pluginId}.theme\0application\0application`,
|
|
value: 'dark',
|
|
updatedAt: 1,
|
|
},
|
|
];
|
|
const filtered = excludeSecretPluginSettingsFromSidecars(entries, declared);
|
|
assert.equal(filtered.length, 1);
|
|
assert.equal(filtered[0].value, 'dark');
|
|
});
|
|
|
|
it('collect prefers newer retained sidecars over older stored settings', () => {
|
|
const bundle = collectPluginSyncSidecars({
|
|
declaredSettingsByPlugin: declared,
|
|
storedSettings: [{
|
|
pluginId,
|
|
settingId: `${pluginId}.theme`,
|
|
scope: 'application',
|
|
scopeId: 'application',
|
|
value: 'local-old',
|
|
updatedAt: 100,
|
|
}],
|
|
existingSidecars: [{
|
|
pluginId,
|
|
kind: 'settings',
|
|
key: `${pluginId}.theme\0application\0application`,
|
|
value: 'remote-new',
|
|
updatedAt: 200,
|
|
}],
|
|
now: 300,
|
|
});
|
|
assert.equal(bundle.entries.length, 1);
|
|
assert.equal(bundle.entries[0].value, 'remote-new');
|
|
assert.equal(bundle.entries[0].updatedAt, 200);
|
|
});
|
|
|
|
it('three-way merge propagates local settings deletions against an unchanged remote', () => {
|
|
const entry: PluginSyncSidecarEntry = {
|
|
pluginId,
|
|
kind: 'settings',
|
|
key: `${pluginId}.theme\0application\0application`,
|
|
value: 'dark',
|
|
updatedAt: 10,
|
|
};
|
|
const merged = mergePluginSyncSidecarsThreeWay({
|
|
base: [entry],
|
|
local: [],
|
|
remote: [entry],
|
|
});
|
|
assert.equal(merged.some((item) => item.key === entry.key), false);
|
|
});
|
|
|
|
it('three-way merge keeps a newer remote edit over a local deletion', () => {
|
|
const base: PluginSyncSidecarEntry = {
|
|
pluginId,
|
|
kind: 'settings',
|
|
key: `${pluginId}.theme\0application\0application`,
|
|
value: 'dark',
|
|
updatedAt: 10,
|
|
};
|
|
const remote: PluginSyncSidecarEntry = {
|
|
...base,
|
|
value: 'light',
|
|
updatedAt: 20,
|
|
};
|
|
const merged = mergePluginSyncSidecarsThreeWay({
|
|
base: [base],
|
|
local: [],
|
|
remote: [remote],
|
|
});
|
|
assert.equal(merged.length, 1);
|
|
assert.equal(merged[0].value, 'light');
|
|
});
|
|
|
|
it('preferCloud keeps local-only settings and baseline orphans', () => {
|
|
const localOnly: PluginSyncSidecarEntry = {
|
|
pluginId,
|
|
kind: 'settings',
|
|
key: `${pluginId}.theme\0application\0application`,
|
|
value: 'local-new',
|
|
updatedAt: 50,
|
|
};
|
|
const baseline: PluginSyncSidecarEntry = {
|
|
pluginId,
|
|
kind: 'account_baseline',
|
|
key: 'account',
|
|
value: { id: 'local-acct' },
|
|
updatedAt: 40,
|
|
};
|
|
const remoteBaseline: PluginSyncSidecarEntry = {
|
|
pluginId,
|
|
kind: 'crdt_baseline',
|
|
key: 'crdt',
|
|
value: { clock: 1 },
|
|
updatedAt: 30,
|
|
};
|
|
const merged = mergePluginSyncSidecarsThreeWay({
|
|
base: [],
|
|
local: [localOnly, baseline],
|
|
remote: [remoteBaseline],
|
|
strategy: 'preferCloud',
|
|
});
|
|
assert.equal(merged.some((item) => item.key === localOnly.key), true);
|
|
assert.equal(merged.some((item) => item.kind === 'account_baseline'), true);
|
|
assert.equal(merged.some((item) => item.kind === 'crdt_baseline'), true);
|
|
});
|
|
});
|