Merge remote-tracking branch 'origin/main' into fix_941

This commit is contained in:
vorotamoroz
2026-06-08 10:55:47 +01:00
8 changed files with 533 additions and 7 deletions
+11 -1
View File
@@ -83,6 +83,10 @@ livesync-cli [database-path] [command] [args...]
- `remote-export <remote-id>`: Export the stored connection string by remote ID.
- `remote-set <remote-id> <connstr>`: Replace the stored connection string by remote ID.
- `remote-activate <remote-id>`: Activate a remote configuration by ID.
- `mark-resolved [remote-id]`: Resolve remote synchronisation status.
- `unlock-remote [remote-id]`: Unlock the remote database.
- `lock-remote [remote-id]`: Lock the remote database.
- `remote-status [remote-id]`: Show remote database status.
- `init-settings [file]`: Create a default settings file.
### Examples
@@ -262,13 +266,19 @@ livesync-cli /path/to/your-local-database --settings /path/to/settings.json rm /
# Resolve conflict by keeping a specific revision
livesync-cli /path/to/your-local-database --settings /path/to/settings.json resolve /vault/path/file.md 3-abcdef
# Add/list/activate/remove remote configurations
# Add, list, activate, and remove remote configurations
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-add main "sls+https://user:pass@example.com/db"
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-ls
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-export remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-set remote-abc123 "sls+p2p://room-abc?passphrase=secret"
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-activate remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-rm remote-abc123
# Lock, unlock, resolve, and view status of remote database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-status remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json lock-remote remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json mark-resolved remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json unlock-remote remote-abc123
```
### Configuration
+174 -2
View File
@@ -2,7 +2,14 @@ import * as fs from "fs/promises";
import * as path from "path";
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
import { configURIBase } from "@lib/common/models/shared.const";
import { DEFAULT_SETTINGS, type FilePathWithPrefix, type ObsidianLiveSyncSettings } from "@lib/common/types";
import {
DEFAULT_SETTINGS,
MILESTONE_DOCID,
type FilePathWithPrefix,
type ObsidianLiveSyncSettings,
REMOTE_COUCHDB,
REMOTE_MINIO,
} from "@lib/common/types";
import { ConnectionStringParser } from "@lib/common/ConnectionString";
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
@@ -16,6 +23,51 @@ function redactConnectionString(uri: string): string {
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
}
async function verifyRemoteState(
core: CLICommandContext["core"],
settings: ObsidianLiveSyncSettings
): Promise<boolean> {
const replicator = core.services.replicator.getActiveReplicator();
if (!replicator) {
process.stderr.write("[Verification] No active replicator found\n");
return false;
}
if (!replicator.nodeid) {
await replicator.initializeDatabaseForReplication();
}
try {
let milestone: any;
if (settings.remoteType === REMOTE_COUCHDB) {
const dbRet = await (replicator as any).connectRemoteCouchDBWithSetting(settings, false, true);
if (typeof dbRet === "string") {
process.stderr.write(`[Verification] Failed to connect to remote CouchDB: ${dbRet}\n`);
return false;
}
milestone = await dbRet.db.get(MILESTONE_DOCID);
} else if (settings.remoteType === REMOTE_MINIO) {
milestone = await (replicator as any).client.downloadJson("_00000000-milestone.json");
}
if (milestone) {
const isLocked = !!milestone.locked;
const isAccepted = !!milestone.accepted_nodes?.includes(replicator.nodeid);
process.stderr.write(`[Verification] Remote Database: ${isLocked ? "LOCKED" : "UNLOCKED"}\n`);
process.stderr.write(
`[Verification] Current Device Node ID (${replicator.nodeid}): ${isAccepted ? "ACCEPTED" : "NOT ACCEPTED"}\n`
);
return true;
} else {
process.stderr.write("[Verification] Milestone document not found on remote.\n");
return false;
}
} catch (e: any) {
process.stderr.write(`[Verification] Failed to fetch milestone document: ${e?.message || e}\n`);
return false;
}
}
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
const { databasePath, core, settingsPath } = context;
@@ -646,7 +698,6 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
console.error(`[Command] remote-set ${id}`);
return true;
}
if (options.command === "remote-activate") {
if (options.commandArgs.length < 1) {
throw new Error("remote-activate requires one argument: <remote-id>");
@@ -676,5 +727,126 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
return true;
}
if (options.command === "mark-resolved") {
const id = options.commandArgs[0]?.trim();
if (id) {
let switched = false;
await core.services.setting.updateSettings((currentSettings) => {
const activated = activateRemoteConfiguration(currentSettings, id);
if (activated) {
switched = true;
return activated;
}
return currentSettings;
}, false);
if (!switched) {
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
return false;
}
await core.services.control.applySettings();
}
console.error(`[Command] mark-resolved${id ? ` ${id}` : ""}`);
await core.services.replication.markResolved();
const settings = core.services.setting.currentSettings();
await verifyRemoteState(core, settings);
return true;
}
if (options.command === "unlock-remote") {
const id = options.commandArgs[0]?.trim();
if (id) {
let switched = false;
await core.services.setting.updateSettings((currentSettings) => {
const activated = activateRemoteConfiguration(currentSettings, id);
if (activated) {
switched = true;
return activated;
}
return currentSettings;
}, false);
if (!switched) {
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
return false;
}
await core.services.control.applySettings();
}
console.error(`[Command] unlock-remote${id ? ` ${id}` : ""}`);
await core.services.replication.markUnlocked();
const settings = core.services.setting.currentSettings();
await verifyRemoteState(core, settings);
return true;
}
if (options.command === "lock-remote") {
const id = options.commandArgs[0]?.trim();
if (id) {
let switched = false;
await core.services.setting.updateSettings((currentSettings) => {
const activated = activateRemoteConfiguration(currentSettings, id);
if (activated) {
switched = true;
return activated;
}
return currentSettings;
}, false);
if (!switched) {
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
return false;
}
await core.services.control.applySettings();
}
console.error(`[Command] lock-remote${id ? ` ${id}` : ""}`);
await core.services.replication.markLocked();
const settings = core.services.setting.currentSettings();
await verifyRemoteState(core, settings);
return true;
}
if (options.command === "remote-status") {
const id = options.commandArgs[0]?.trim();
if (id) {
let switched = false;
await core.services.setting.updateSettings((currentSettings) => {
const activated = activateRemoteConfiguration(currentSettings, id);
if (activated) {
switched = true;
return activated;
}
return currentSettings;
}, false);
if (!switched) {
process.stderr.write(`[Info] Failed to temporarily activate remote configuration: ${id}\n`);
return false;
}
await core.services.control.applySettings();
}
console.error(`[Command] remote-status${id ? ` ${id}` : ""}`);
const replicator = core.services.replicator.getActiveReplicator();
if (!replicator) {
process.stderr.write("[Error] No active replicator found\n");
return false;
}
const settings = core.services.setting.currentSettings();
const status = await replicator.getRemoteStatus(settings);
if (status === false) {
process.stderr.write("[Error] Failed to fetch remote status\n");
return false;
}
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
return true;
}
throw new Error(`Unsupported command: ${options.command}`);
}
@@ -28,6 +28,34 @@ function createCoreMock() {
updater(liveSettings);
}),
},
replication: {
markResolved: vi.fn(async () => {}),
markUnlocked: vi.fn(async () => {}),
markLocked: vi.fn(async () => {}),
},
replicator: {
getActiveReplicator: vi.fn(() => ({
nodeid: "test-node-id",
initializeDatabaseForReplication: vi.fn(async () => {}),
connectRemoteCouchDBWithSetting: vi.fn(async () => ({
db: {
get: vi.fn(async (id) => {
if (id.includes("milestone")) {
return {
locked: false,
accepted_nodes: ["test-node-id"],
};
}
throw new Error("not found");
}),
},
})),
getRemoteStatus: vi.fn(async () => ({
db_name: "test-db",
doc_count: 42,
})),
})),
},
},
serviceModules: {
fileHandler: {
@@ -572,4 +600,139 @@ describe("runCommand abnormal cases", () => {
const exported2 = export2Lines.length > 0 ? export2Lines[export2Lines.length - 1] : "";
expect(exported2).toBe(roundTripInput);
});
describe("mark-resolved and unlock-remote commands", () => {
it("mark-resolved without args runs on active database", async () => {
const core = createCoreMock();
const result = await runCommand(makeOptions("mark-resolved", []), {
...context,
core,
});
expect(result).toBe(true);
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
expect(core.services.control.applySettings).not.toHaveBeenCalled();
});
it("mark-resolved with remote-id temporarily activates it and runs markResolved", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://example.com/db1",
isEncrypted: false,
};
const result = await runCommand(makeOptions("mark-resolved", ["r1"]), {
...context,
core,
});
expect(result).toBe(true);
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
expect(settings.activeConfigurationId).toBe("r1");
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
});
it("unlock-remote without args runs on active database", async () => {
const core = createCoreMock();
const result = await runCommand(makeOptions("unlock-remote", []), {
...context,
core,
});
expect(result).toBe(true);
expect(core.services.replication.markUnlocked).toHaveBeenCalledTimes(1);
expect(core.services.control.applySettings).not.toHaveBeenCalled();
});
it("unlock-remote with remote-id temporarily activates it and runs markUnlocked", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://example.com/db1",
isEncrypted: false,
};
const result = await runCommand(makeOptions("unlock-remote", ["r1"]), {
...context,
core,
});
expect(result).toBe(true);
expect(core.services.replication.markUnlocked).toHaveBeenCalledTimes(1);
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
expect(settings.activeConfigurationId).toBe("r1");
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
});
it("lock-remote without args runs on active database", async () => {
const core = createCoreMock();
const result = await runCommand(makeOptions("lock-remote", []), {
...context,
core,
});
expect(result).toBe(true);
expect(core.services.replication.markLocked).toHaveBeenCalledTimes(1);
expect(core.services.control.applySettings).not.toHaveBeenCalled();
});
it("lock-remote with remote-id temporarily activates it and runs markLocked", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://example.com/db1",
isEncrypted: false,
};
const result = await runCommand(makeOptions("lock-remote", ["r1"]), {
...context,
core,
});
expect(result).toBe(true);
expect(core.services.replication.markLocked).toHaveBeenCalledTimes(1);
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
expect(settings.activeConfigurationId).toBe("r1");
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
});
it("remote-status without args outputs status of active remote configuration", async () => {
const core = createCoreMock();
const stdout = captureStdout();
const result = await runCommand(makeOptions("remote-status", []), {
...context,
core,
});
expect(result).toBe(true);
const fullOutput = stdout.spy.mock.calls.map((c) => c[0]).join("");
const parsedStatus = JSON.parse(fullOutput);
expect(parsedStatus.db_name).toBe("test-db");
expect(parsedStatus.doc_count).toBe(42);
});
it("remote-status with remote-id temporarily activates it and outputs status", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteConfigurations.r1 = {
id: "r1",
name: "R1",
uri: "sls+https://example.com/db1",
isEncrypted: false,
};
const stdout = captureStdout();
const result = await runCommand(makeOptions("remote-status", ["r1"]), {
...context,
core,
});
expect(result).toBe(true);
const fullOutput = stdout.spy.mock.calls.map((c) => c[0]).join("");
const parsedStatus = JSON.parse(fullOutput);
expect(parsedStatus.db_name).toBe("test-db");
expect(parsedStatus.doc_count).toBe(42);
expect(settings.activeConfigurationId).toBe("r1");
expect(core.services.setting.updateSettings).toHaveBeenCalledWith(expect.any(Function), false);
});
});
});
+8
View File
@@ -26,6 +26,10 @@ export type CLICommand =
| "remote-export"
| "remote-set"
| "remote-activate"
| "mark-resolved"
| "unlock-remote"
| "lock-remote"
| "remote-status"
| "init-settings";
export interface CLIOptions {
@@ -80,5 +84,9 @@ export const VALID_COMMANDS = new Set([
"remote-export",
"remote-set",
"remote-activate",
"mark-resolved",
"unlock-remote",
"lock-remote",
"remote-status",
"init-settings",
] as const);
+17 -1
View File
@@ -72,6 +72,14 @@ Commands:
Replace a stored remote connection string by ID
remote-activate <remote-id>
Activate a stored remote configuration by ID
mark-resolved [remote-id]
Resolve remote synchronisation status
unlock-remote [remote-id]
Unlock remote database
lock-remote [remote-id]
Lock remote database
remote-status [remote-id]
Show remote database status
Options:
--vault <path>, -V <path> (daemon/mirror) Path to the vault directory containing .md files
@@ -102,6 +110,10 @@ Examples:
livesync-cli ./my-database remote-set remote-abc123 "sls+s3://ak:sk@example.com/?endpoint=https%3A%2F%2Fs3.example.com&bucket=mybucket"
livesync-cli ./my-database remote-activate remote-abc123
livesync-cli ./my-database remote-rm remote-abc123
livesync-cli ./my-database mark-resolved remote-abc123
livesync-cli ./my-database unlock-remote remote-abc123
livesync-cli ./my-database lock-remote remote-abc123
livesync-cli ./my-database remote-status remote-abc123
livesync-cli init-settings ./data.json
livesync-cli ./my-database --verbose
`);
@@ -265,7 +277,11 @@ export async function main() {
options.command === "p2p-peers" ||
options.command === "info" ||
options.command === "rm" ||
options.command === "resolve";
options.command === "resolve" ||
options.command === "mark-resolved" ||
options.command === "unlock-remote" ||
options.command === "lock-remote" ||
options.command === "remote-status";
const infoLog = avoidStdoutNoise ? console.error : console.log;
if (options.debug) {
setGlobalLogFunction((msg, level) => {
+4 -2
View File
@@ -25,16 +25,18 @@
"test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh",
"test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh",
"test:e2e:mirror": "bash test/test-mirror-linux.sh",
"test:e2e:remote-commands": "bash test/test-remote-commands-linux.sh",
"pretest:e2e:all": "npm run build",
"test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:p2p",
"test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:remote-commands",
"pretest:e2e:docker:all": "npm run build:docker",
"test:e2e:docker:push-pull": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-push-pull-linux.sh",
"test:e2e:docker:setup-put-cat": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-setup-put-cat-linux.sh",
"test:e2e:docker:mirror": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-mirror-linux.sh",
"test:e2e:docker:remote-commands": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-remote-commands-linux.sh",
"test:e2e:docker:sync-two-local": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-sync-two-local-databases-linux.sh",
"test:e2e:docker:p2p": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-three-nodes-conflict-linux.sh",
"test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh",
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror"
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror && npm run test:e2e:docker:remote-commands"
},
"dependencies": {},
"devDependencies": {}
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# Test: CLI remote management commands: remote-status, unlock-remote, and mark-resolved.
#
# Scenario:
# 1. Start CouchDB, create a test database, and perform an initial sync.
# 2. Run remote-status and assert that the output contains the database name in JSON format.
# 3. Lock the remote database milestone manually using curl, verify status, and run unlock-remote.
# Assert that the output of unlock-remote contains the unlocked verification status.
# 4. Run mark-resolved and verify it succeeds.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
cd "$CLI_DIR"
source "$SCRIPT_DIR/test-helpers.sh"
display_test_info
RUN_BUILD="${RUN_BUILD:-1}"
TEST_ENV_FILE="${TEST_ENV_FILE:-$CLI_DIR/.test.env}"
cli_test_init_cli_cmd
if [[ ! -f "$TEST_ENV_FILE" ]]; then
echo "[ERROR] test env file not found: $TEST_ENV_FILE" >&2
exit 1
fi
set -a
source "$TEST_ENV_FILE"
set +a
DB_SUFFIX="$(date +%s)-$RANDOM"
COUCHDB_URI="${hostname%/}"
COUCHDB_DBNAME="${dbname}-remotes-${DB_SUFFIX}"
COUCHDB_USER="${username:-}"
COUCHDB_PASSWORD="${password:-}"
if [[ -z "$COUCHDB_URI" || -z "$COUCHDB_USER" || -z "$COUCHDB_PASSWORD" ]]; then
echo "[ERROR] COUCHDB_URI, COUCHDB_USER, and COUCHDB_PASSWORD are required" >&2
exit 1
fi
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-remote-cmds.XXXXXX")"
VAULT_DIR="$WORK_DIR/vault"
SETTINGS_FILE="$WORK_DIR/settings.json"
mkdir -p "$VAULT_DIR"
cleanup() {
local exit_code=$?
cli_test_stop_couchdb
rm -rf "$WORK_DIR"
exit "$exit_code"
}
trap cleanup EXIT
if [[ "$RUN_BUILD" == "1" ]]; then
echo "[INFO] building CLI"
npm run build
fi
echo "[INFO] starting CouchDB and creating test database: $COUCHDB_DBNAME"
cli_test_start_couchdb "$COUCHDB_URI" "$COUCHDB_USER" "$COUCHDB_PASSWORD" "$COUCHDB_DBNAME"
echo "[INFO] preparing settings"
cli_test_init_settings_file "$SETTINGS_FILE"
echo ".."
cli_test_apply_couchdb_settings "$SETTINGS_FILE" "$COUCHDB_URI" "$COUCHDB_USER" "$COUCHDB_PASSWORD" "$COUCHDB_DBNAME" 1
echo "..."
echo "[INFO] initial sync to create milestone document"
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" sync >/dev/null
MILESTONE_ID="_local/obsydian_livesync_milestone"
MILESTONE_URL="${COUCHDB_URI}/${COUCHDB_DBNAME}/${MILESTONE_ID}"
update_milestone() {
local locked="$1"
local accepted_nodes="$2"
local current
current="$(cli_test_curl_json --user "${COUCHDB_USER}:${COUCHDB_PASSWORD}" "$MILESTONE_URL")"
local updated
updated="$(node -e '
const doc = JSON.parse(process.argv[1]);
doc.locked = process.argv[2] === "true";
doc.accepted_nodes = JSON.parse(process.argv[3]);
process.stdout.write(JSON.stringify(doc));
' "$current" "$locked" "$accepted_nodes")"
cli_test_curl_json -X PUT \
--user "${COUCHDB_USER}:${COUCHDB_PASSWORD}" \
-H "Content-Type: application/json" \
-d "$updated" \
"$MILESTONE_URL" >/dev/null
}
CMD_LOG="$WORK_DIR/cmd.log"
echo "[CASE] remote-status outputs valid JSON with CouchDB details"
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" remote-status >"$CMD_LOG" 2>&1
cli_test_assert_contains "$(cat "$CMD_LOG")" \
"\"db_name\": \"$COUCHDB_DBNAME\"" \
"remote-status should return JSON containing db_name"
echo "[PASS] remote-status verified"
echo "[CASE] lock-remote locks and verifies state"
# Run lock-remote and verify output contains verification message
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" lock-remote >"$CMD_LOG" 2>&1
cli_test_assert_contains "$(cat "$CMD_LOG")" \
"[Verification] Remote Database: LOCKED" \
"lock-remote output should show that the remote database is locked"
echo "[PASS] lock-remote verified"
echo "[CASE] unlock-remote unlocks and verifies state"
# Manually lock milestone
update_milestone "true" "[]"
# Run unlock-remote and verify output contains verification message
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" unlock-remote >"$CMD_LOG" 2>&1
cli_test_assert_contains "$(cat "$CMD_LOG")" \
"[Verification] Remote Database: UNLOCKED" \
"unlock-remote output should contain verification status"
echo "[PASS] unlock-remote verified"
echo "[CASE] mark-resolved resolves and verifies state"
# Manually lock milestone
update_milestone "true" "[]"
# Run mark-resolved and verify output contains verification message
run_cli "$VAULT_DIR" --settings "$SETTINGS_FILE" mark-resolved >"$CMD_LOG" 2>&1
cli_test_assert_contains "$(cat "$CMD_LOG")" \
"[Verification] Remote Database: LOCKED" \
"mark-resolved output should show that the remote database remains locked"
cli_test_assert_contains "$(cat "$CMD_LOG")" \
"ACCEPTED" \
"mark-resolved output should show that the current device node is accepted"
echo "[PASS] mark-resolved verified"
echo "[ALL PASS] All remote CLI commands verified successfully"
+10 -1
View File
@@ -9,7 +9,16 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
### Fixed
- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan.
- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan (#941).
- Prevented the automatic merging of conflicted revisions when one of the revisions has been deleted, which was causing deleted files to reappear (#911).
## Only CLI
8th June, 2026
I should also consider the version numbering for the CLI...
- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`.
## 0.25.73