Migrate LiveSync flows to provider-owned resources

This commit is contained in:
vorotamoroz
2026-08-28 17:54:04 +00:00
parent f7206b1a6e
commit db70b4c2b6
34 changed files with 1830 additions and 365 deletions
+160 -20
View File
@@ -2,10 +2,22 @@ import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node";
import * as processSetting from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types";
import {
DEFAULT_SETTINGS,
REMOTE_COUCHDB,
REMOTE_MINIO,
REMOTE_P2P,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { runCommand } from "./runCommand";
import type { CLIOptions } from "./types";
import {
REMOTE_ADMINISTRATION_ACTIONS,
REMOTE_ADMINISTRATION_FAILURE_REASONS,
REMOTE_ADMINISTRATION_OBSERVATION_KINDS,
REMOTE_ADMINISTRATION_RESULT_STATUSES,
REMOTE_RESOURCE_KINDS,
} from "@vrtmrz/livesync-commonlib/replication";
function createStandardIoMock() {
return {
@@ -46,6 +58,23 @@ function createCoreMock() {
markLocked: vi.fn(async () => {}),
},
replicator: {
runRemoteAdministration: vi.fn(async ({ action }) => ({
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFIED,
observation: {
kind: REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
locked: action === REMOTE_ADMINISTRATION_ACTIONS.LOCK,
accepted: true,
nodeId: "test-node-id",
},
})),
createRemoteResource: vi.fn(async () => ({
check: vi.fn(async () => ({ ok: true as const })),
getStatus: vi.fn(async () => ({
db_name: "test-db",
doc_count: 42,
})),
dispose: vi.fn(async () => undefined),
})),
getActiveReplicator: vi.fn(() => ({
nodeid: "test-node-id",
initializeDatabaseForReplication: vi.fn(async () => {}),
@@ -93,6 +122,7 @@ function makeOptions(command: CLIOptions["command"], commandArgs: string[]): CLI
databasePath: "/tmp/vault",
verbose: false,
force: false,
compatRemoteAdminExitZero: false,
};
}
@@ -706,28 +736,110 @@ describe("runCommand abnormal cases", () => {
});
describe("mark-resolved and unlock-remote commands", () => {
it("fails by default when remote administration cannot verify its postcondition", async () => {
const core = createCoreMock();
core.services.replicator.runRemoteAdministration.mockResolvedValueOnce({
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFICATION_FAILED,
reason: REMOTE_ADMINISTRATION_FAILURE_REASONS.NO_ACTIVE_REPLICATOR,
});
const result = await runCommand(makeOptions("mark-resolved", []), {
...context,
core,
});
expect(result).toBe(false);
});
it("preserves the historical zero exit for returned verification failures only when requested", async () => {
const core = createCoreMock();
core.services.replicator.runRemoteAdministration.mockResolvedValueOnce({
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFICATION_FAILED,
reason: REMOTE_ADMINISTRATION_FAILURE_REASONS.NO_ACTIVE_REPLICATOR,
});
const result = await runCommand(
{ ...makeOptions("mark-resolved", []), compatRemoteAdminExitZero: true },
{
...context,
core,
}
);
expect(result).toBe(true);
});
it("does not hide a thrown remote mutation failure behind the compatibility option", async () => {
const core = createCoreMock();
const failure = new Error("mutation failed");
core.services.replicator.runRemoteAdministration.mockRejectedValueOnce(failure);
await expect(
runCommand(
{ ...makeOptions("mark-resolved", []), compatRemoteAdminExitZero: true },
{
...context,
core,
}
)
).rejects.toBe(failure);
});
it("does not hide an unknown remote ID behind the compatibility option", async () => {
const core = createCoreMock();
const result = await runCommand(
{ ...makeOptions("mark-resolved", ["missing-remote"]), compatRemoteAdminExitZero: true },
{
...context,
core,
}
);
expect(result).toBe(false);
expect(core.services.replicator.runRemoteAdministration).not.toHaveBeenCalled();
});
it("fails a lock command when the observed milestone remains unlocked", async () => {
const core = createCoreMock();
core.services.replicator.runRemoteAdministration.mockResolvedValueOnce({
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFICATION_FAILED,
reason: REMOTE_ADMINISTRATION_FAILURE_REASONS.POSTCONDITION_MISMATCH,
observation: {
kind: REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
locked: false,
accepted: true,
nodeId: "test-node-id",
},
});
const result = await runCommand(makeOptions("lock-remote", []), {
...context,
core,
});
expect(result).toBe(false);
const verificationOutput = core.services.context.standardIo.writeStderr.mock.calls
.map(([chunk]: [string | Uint8Array]) =>
typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk)
)
.join("");
expect(verificationOutput).toContain("[Verification] Remote Database: UNLOCKED\n");
expect(verificationOutput).toContain("[Verification] Current Device Node ID (test-node-id): ACCEPTED\n");
});
it("mark-resolved without args runs on active database", async () => {
const core = createCoreMock();
const remoteDatabase = {
close: vi.fn(async () => undefined),
get: vi.fn(async () => ({
locked: false,
accepted_nodes: ["test-node-id"],
})),
};
core.services.replicator.getActiveReplicator.mockReturnValueOnce({
nodeid: "test-node-id",
initializeDatabaseForReplication: vi.fn(async () => undefined),
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase })),
});
const result = await runCommand(makeOptions("mark-resolved", []), {
...context,
core,
});
expect(result).toBe(true);
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
expect(core.services.replicator.runRemoteAdministration).toHaveBeenCalledWith({
action: REMOTE_ADMINISTRATION_ACTIONS.MARK_RESOLVED,
});
expect(core.services.control.applySettings).not.toHaveBeenCalled();
expect(remoteDatabase.close).toHaveBeenCalledOnce();
expect(core.services.replication.markResolved).not.toHaveBeenCalled();
});
it("mark-resolved with remote-id temporarily activates it and runs markResolved", async () => {
@@ -745,7 +857,9 @@ describe("runCommand abnormal cases", () => {
core,
});
expect(result).toBe(true);
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
expect(core.services.replicator.runRemoteAdministration).toHaveBeenCalledWith({
action: REMOTE_ADMINISTRATION_ACTIONS.MARK_RESOLVED,
});
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
expect(settings.activeConfigurationId).toBe("r1");
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
@@ -758,7 +872,9 @@ describe("runCommand abnormal cases", () => {
core,
});
expect(result).toBe(true);
expect(core.services.replication.markUnlocked).toHaveBeenCalledTimes(1);
expect(core.services.replicator.runRemoteAdministration).toHaveBeenCalledWith({
action: REMOTE_ADMINISTRATION_ACTIONS.UNLOCK,
});
expect(core.services.control.applySettings).not.toHaveBeenCalled();
});
@@ -777,7 +893,9 @@ describe("runCommand abnormal cases", () => {
core,
});
expect(result).toBe(true);
expect(core.services.replication.markUnlocked).toHaveBeenCalledTimes(1);
expect(core.services.replicator.runRemoteAdministration).toHaveBeenCalledWith({
action: REMOTE_ADMINISTRATION_ACTIONS.UNLOCK,
});
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
expect(settings.activeConfigurationId).toBe("r1");
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
@@ -790,7 +908,9 @@ describe("runCommand abnormal cases", () => {
core,
});
expect(result).toBe(true);
expect(core.services.replication.markLocked).toHaveBeenCalledTimes(1);
expect(core.services.replicator.runRemoteAdministration).toHaveBeenCalledWith({
action: REMOTE_ADMINISTRATION_ACTIONS.LOCK,
});
expect(core.services.control.applySettings).not.toHaveBeenCalled();
});
@@ -809,7 +929,9 @@ describe("runCommand abnormal cases", () => {
core,
});
expect(result).toBe(true);
expect(core.services.replication.markLocked).toHaveBeenCalledTimes(1);
expect(core.services.replicator.runRemoteAdministration).toHaveBeenCalledWith({
action: REMOTE_ADMINISTRATION_ACTIONS.LOCK,
});
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
expect(settings.activeConfigurationId).toBe("r1");
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
@@ -817,6 +939,17 @@ describe("runCommand abnormal cases", () => {
it("remote-status without args outputs status of active remote configuration", async () => {
const core = createCoreMock();
const getStatus = vi.fn(async () => ({
db_name: "test-db",
doc_count: 42,
}));
const dispose = vi.fn(async () => undefined);
const createRemoteResource = vi.fn(async () => ({
check: vi.fn(),
getStatus,
dispose,
}));
core.services.replicator.createRemoteResource = createRemoteResource;
const stdout = captureStdout(core);
const result = await runCommand(makeOptions("remote-status", []), {
...context,
@@ -827,6 +960,13 @@ describe("runCommand abnormal cases", () => {
const parsedStatus = JSON.parse(fullOutput);
expect(parsedStatus.db_name).toBe("test-db");
expect(parsedStatus.doc_count).toBe(42);
expect(createRemoteResource).toHaveBeenCalledWith(
REMOTE_RESOURCE_KINDS.CONNECTION,
core.services.setting.currentSettings()
);
expect(getStatus).toHaveBeenCalledOnce();
expect(dispose).toHaveBeenCalledOnce();
expect(core.services.replicator.getActiveReplicator).not.toHaveBeenCalled();
});
it("remote-status with remote-id temporarily activates it and outputs status", async () => {