Adopt active Replicator ownership contracts

This commit is contained in:
vorotamoroz
2026-08-30 09:45:00 +00:00
parent db70b4c2b6
commit 7300db08d6
18 changed files with 984 additions and 410 deletions
+117 -6
View File
@@ -3,13 +3,17 @@ import { REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
import {
CAPABILITY_SUPPORT_KINDS,
NO_INTERACTION,
REPLICATION_COMPLETED,
REMOTE_RESOURCE_KINDS,
REPLACE_SAME_KIND_REPLICATOR,
USER_INITIATED_REPLICATION_AUTHORITY,
} from "@vrtmrz/livesync-commonlib/replication";
const constructorMocks = vi.hoisted(() => ({
couchDB: vi.fn(),
couchDBOneShot: vi.fn(async (..._args: unknown[]) => REPLICATION_COMPLETED),
objectStorage: vi.fn(),
objectStorageOneShot: vi.fn(async (..._args: unknown[]) => REPLICATION_COMPLETED),
}));
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
@@ -17,6 +21,9 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicato
constructor(host: unknown) {
constructorMocks.couchDB(host);
}
openOneShotReplicationWithOutcome(...args: unknown[]) {
return constructorMocks.couchDBOneShot(...args);
}
},
}));
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator", () => ({
@@ -24,12 +31,21 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalRe
constructor(host: unknown) {
constructorMocks.objectStorage(host);
}
openOneShotReplicationWithOutcome(...args: unknown[]) {
return constructorMocks.objectStorageOneShot(...args);
}
},
}));
import { createCentralReplicatorProviderDefinitions } from "./replicatorProviders";
describe("central Replicator provider definitions", () => {
it("keeps the retained remote-resource catalogue bounded", () => {
expect
.soft(Object.values(REMOTE_RESOURCE_KINDS).sort())
.toEqual(["connection", "preferred-tweak", "security-seed", "synchronisation-information"].sort());
});
it("composes CouchDB and Object Storage policies outside LiveSyncBaseCore", async () => {
const host = {} as Parameters<typeof createCentralReplicatorProviderDefinitions>[0];
const definitions = createCentralReplicatorProviderDefinitions(host);
@@ -37,8 +53,8 @@ describe("central Replicator provider definitions", () => {
const objectStorage = definitions.get(REMOTE_MINIO)!;
expect([...definitions.keys()]).toEqual([REMOTE_COUCHDB, REMOTE_MINIO]);
expect(couchDB.sameKindReconciliation).toBe(REPLACE_SAME_KIND_REPLICATOR);
expect(objectStorage.sameKindReconciliation).toBe(REPLACE_SAME_KIND_REPLICATOR);
expect("sameKindReconciliation" in couchDB).toBe(false);
expect("sameKindReconciliation" in objectStorage).toBe(false);
expect(
couchDB.isConfigured(
@@ -76,10 +92,12 @@ describe("central Replicator provider definitions", () => {
).toBe(false);
});
it("declares an exhaustive resource and administration catalogue for both central providers", () => {
it("declares the retained owned resources and cohesive optional administration", () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchResources = definitions.get(REMOTE_COUCHDB)?.remoteResources;
const objectResources = definitions.get(REMOTE_MINIO)?.remoteResources;
const couchAdministration = definitions.get(REMOTE_COUCHDB)?.centralRemoteAdministration;
const objectAdministration = definitions.get(REMOTE_MINIO)?.centralRemoteAdministration;
expect(Object.keys(couchResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
expect(Object.keys(objectResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
@@ -93,7 +111,100 @@ describe("central Replicator provider definitions", () => {
expect(objectResources?.[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION].kind).toBe(
CAPABILITY_SUPPORT_KINDS.NOT_APPLICABLE
);
expect(definitions.get(REMOTE_COUCHDB)?.remoteAdministration.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(definitions.get(REMOTE_MINIO)?.remoteAdministration.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(couchAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect(objectAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
expect("activeRemoteReads" in definitions.get(REMOTE_COUCHDB)!).toBe(false);
expect("fullTransfers" in definitions.get(REMOTE_COUCHDB)!).toBe(false);
});
it("dispatches central finite work through provider-local attempt results", async () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const objectStorage = definitions.get(REMOTE_MINIO)!;
const setting = createNewVaultSettings();
const couchInstance = await couchDB.create(setting);
const objectInstance = await objectStorage.create(setting);
if (!couchInstance || !objectInstance) throw new Error("Provider construction failed");
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("CouchDB OneShot is unavailable");
}
if (objectStorage.unattendedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("Object Storage OneShot is unavailable");
}
await expect(
couchDB.userInitiatedOneShot.run(couchInstance, setting, {
trigger: "manual",
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
})
).resolves.toBe(REPLICATION_COMPLETED);
await expect(
objectStorage.unattendedOneShot.run(objectInstance, setting, {
trigger: "resume",
interaction: NO_INTERACTION,
})
).resolves.toBe(REPLICATION_COMPLETED);
expect(constructorMocks.couchDBOneShot).toHaveBeenCalledWith(setting, true);
expect(constructorMocks.objectStorageOneShot).toHaveBeenCalledWith(setting, false);
});
it("dispatches central finite work through the declared operation rather than constructor identity", async () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const objectStorage = definitions.get(REMOTE_MINIO)!;
const setting = createNewVaultSettings();
const createStructuralOneShotReplicator = () => ({
initializeDatabaseForReplication: vi.fn(async () => true),
openReplication: vi.fn(async () => true),
terminateSync: vi.fn(),
closeReplication: vi.fn(),
openOneShotReplicationWithOutcome: vi.fn(async () => REPLICATION_COMPLETED),
});
const couchInstance = createStructuralOneShotReplicator();
const objectStorageInstance = createStructuralOneShotReplicator();
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("CouchDB OneShot is unavailable");
}
if (objectStorage.unattendedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("Object Storage OneShot is unavailable");
}
const couchOutcome = await couchDB.userInitiatedOneShot.run(couchInstance, setting, {
trigger: "manual",
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
});
const objectStorageOutcome = await objectStorage.unattendedOneShot.run(objectStorageInstance, setting, {
trigger: "resume",
interaction: NO_INTERACTION,
});
expect.soft(couchOutcome).toBe(REPLICATION_COMPLETED);
expect.soft(objectStorageOutcome).toBe(REPLICATION_COMPLETED);
expect(couchInstance.openOneShotReplicationWithOutcome).toHaveBeenCalledWith(setting, true);
expect(objectStorageInstance.openOneShotReplicationWithOutcome).toHaveBeenCalledWith(setting, false);
});
it("rejects a one-shot adapter whose Replicator does not declare the required operation", async () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
const couchDB = definitions.get(REMOTE_COUCHDB)!;
const setting = createNewVaultSettings();
const incompleteInstance = {
initializeDatabaseForReplication: vi.fn(async () => true),
openReplication: vi.fn(async () => true),
terminateSync: vi.fn(),
closeReplication: vi.fn(),
};
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
throw new Error("CouchDB OneShot is unavailable");
}
const outcome = await couchDB.userInitiatedOneShot.run(incompleteInstance, setting, {
trigger: "manual",
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
});
expect(outcome.status).toBe("failed");
expect(incompleteInstance.openReplication).not.toHaveBeenCalled();
});
});