mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-30 15:27:06 +00:00
Migrate LiveSync flows to provider-owned resources
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user