Keep Commonlib CLI diagnostics behind debug mode

This commit is contained in:
vorotamoroz committed 2026-11-05 08:36:36 +00:00
1 parent a0f1483891
commit e7124eda98
4 files changed
+25 -21

No files matched your search

+5
View File
@@ -116,6 +116,11 @@ saved. After correcting the cause, run `sync` again with the same database and
settings to retry it. Existing partial writes are retained. `fetch --force` settings to retry it. Existing partial writes are retained. `fetch --force`
replaces the local database instead and follows the reconstruction rules below. replaces the local database instead and follows the reconstruction rules below.
A failed `sync` prints `[Error] Command 'sync' failed` and exits with code `1`.
To inspect the Journal key, failure stage, and diagnostic details, retry with
`--debug`, for example `livesync-cli ./my-db --debug sync`, retaining the same
database and settings. Debug diagnostics are written to standard error.
If the Journal cannot be recovered and missing content or history is If the Journal cannot be recovered and missing content or history is
acceptable, run `journal-skip <journal-key>`. The CLI first performs normal acceptable, run `journal-skip <journal-key>`. The CLI first performs normal
replication with the retained local database. It skips only when that attempt replication with the retained local database. It skips only when that attempt
+2 -9
View File
@@ -79,15 +79,6 @@ describe("CLI log handler", () => {
it.each([ it.each([
["NOTICE", LOG_LEVEL_NOTICE], ["NOTICE", LOG_LEVEL_NOTICE],
["URGENT", LOG_LEVEL_URGENT], ["URGENT", LOG_LEVEL_URGENT],
] as const)("writes Commonlib %s diagnostics to stderr by default", async (name, level) => {
const message = `Commonlib ${name} diagnostic`;
const standardIo = await invokeGlobalLogger(databasePath, message, level);
expect(standardIo.writeStderr).toHaveBeenCalledWith(expect.stringContaining(`[${level}] ${message}\n`));
expect(standardIo.writeStdout).not.toHaveBeenCalledWith(expect.stringContaining(message));
});
it.each([
["DEBUG", LOG_LEVEL_DEBUG], ["DEBUG", LOG_LEVEL_DEBUG],
["INFO", LOG_LEVEL_INFO], ["INFO", LOG_LEVEL_INFO],
["VERBOSE", LOG_LEVEL_VERBOSE], ["VERBOSE", LOG_LEVEL_VERBOSE],
@@ -101,6 +92,8 @@ describe("CLI log handler", () => {
it("writes detailed Commonlib diagnostics to stderr in debug mode", async () => { it("writes detailed Commonlib diagnostics to stderr in debug mode", async () => {
const messages = [ const messages = [
["NOTICE", LOG_LEVEL_NOTICE],
["URGENT", LOG_LEVEL_URGENT],
["DEBUG", LOG_LEVEL_DEBUG], ["DEBUG", LOG_LEVEL_DEBUG],
["INFO", LOG_LEVEL_INFO], ["INFO", LOG_LEVEL_INFO],
["VERBOSE", LOG_LEVEL_VERBOSE], ["VERBOSE", LOG_LEVEL_VERBOSE],
+6 -1
View File
@@ -378,10 +378,15 @@ export async function main(
const writeLine = avoidStdoutNoise ? writeStderrLine : writeStdoutLine; const writeLine = avoidStdoutNoise ? writeStderrLine : writeStdoutLine;
writeLine(standardIo, ...values); writeLine(standardIo, ...values);
}; };
if (options.debug) {
setGlobalLogFunction((msg, level) => { setGlobalLogFunction((msg, level) => {
if (!options.debug && level !== LOG_LEVEL_NOTICE && level !== LOG_LEVEL_URGENT) return;
writeStderrLine(standardIo, `[${level}]`, msg); writeStderrLine(standardIo, `[${level}]`, msg);
}); });
} else {
setGlobalLogFunction((msg, level) => {
// NO OP, leave it to logFunction
});
}
if (options.command === "init-settings") { if (options.command === "init-settings") {
await createDefaultSettingsFile(options, standardIo); await createDefaultSettingsFile(options, standardIo);
return; return;
+12 -11
View File
@@ -247,9 +247,12 @@ async function runScenario(encrypt: boolean): Promise<void> {
assert.notEqual(finalRevision, firstRevision); assert.notEqual(finalRevision, firstRevision);
await withCorruptedJournals(client, minioBucket, [brokenJournalKey, secondBrokenJournalKey], async () => { await withCorruptedJournals(client, minioBucket, [brokenJournalKey, secondBrokenJournalKey], async () => {
const failedSync = await tryDefault("--debug", "sync"); const failedSync = await tryDefault("sync");
assert.notEqual(failedSync.code, 0, "Default receive must report failure for a corrupt Journal"); assert.equal(failedSync.code, 1, "Default receive must report failure for a corrupt Journal");
assert.match(failedSync.combined, /receive journal error|journal.*failed|replication.*failed/i); assert.match(failedSync.stderr, /\[Error\] Command 'sync' failed/);
const debugRetry = await tryDefault("--debug", "sync");
assert.equal(debugRetry.code, 1, "Debug retry must still report the corrupt Journal");
assert.ok(debugRetry.stderr.includes(`Receive Journal ${brokenJournalKey} failed at `));
const checkpoint = await readReceivedJournalKeys(defaultDatabase); const checkpoint = await readReceivedJournalKeys(defaultDatabase);
assert.ok(checkpoint.has(firstJournalKey), "Completed earlier Journal must remain checkpointed"); assert.ok(checkpoint.has(firstJournalKey), "Completed earlier Journal must remain checkpointed");
@@ -307,14 +310,12 @@ async function runScenario(encrypt: boolean): Promise<void> {
`Skipping the first Journal should stop at the next broken Journal: ${firstSkip.combined}` `Skipping the first Journal should stop at the next broken Journal: ${firstSkip.combined}`
); );
assert.ok(firstSkip.combined.includes(brokenJournalKey), "The first skip must identify its Journal key"); assert.ok(firstSkip.combined.includes(brokenJournalKey), "The first skip must identify its Journal key");
assert.match(firstSkip.stderr, /\[Error\] Command 'journal-skip' failed/);
const nextFailure = await trySkip("--debug", "sync");
assert.equal(nextFailure.code, 1, "Debug retry must stop at the next broken Journal");
assert.ok( assert.ok(
firstSkip.combined.includes(secondBrokenJournalKey), nextFailure.stderr.includes(`Receive Journal ${secondBrokenJournalKey} failed at `),
"Resumed replication must stop and identify the next broken Journal" "Debug retry must identify the next broken Journal and its failure stage"
);
assert.match(
firstSkip.combined,
/decrypt|inflate|decompress|decode|parse|persist|apply/i,
"The first skip attempt must report the failed Journal processing stage"
); );
const firstSkipCheckpoint = await readReceivedJournalKeys(skipDatabase); const firstSkipCheckpoint = await readReceivedJournalKeys(skipDatabase);
@@ -331,7 +332,7 @@ async function runScenario(encrypt: boolean): Promise<void> {
assert.notEqual((await trySkip("cat", secondBrokenPath)).code, 0); assert.notEqual((await trySkip("cat", secondBrokenPath)).code, 0);
assert.notEqual((await trySkip("cat", laterPath)).code, 0); assert.notEqual((await trySkip("cat", laterPath)).code, 0);
const secondSkip = await trySkip("journal-skip", secondBrokenJournalKey); const secondSkip = await trySkip("--debug", "journal-skip", secondBrokenJournalKey);
assert.equal(secondSkip.code, 0, `Explicit second skip should resume replication: ${secondSkip.combined}`); assert.equal(secondSkip.code, 0, `Explicit second skip should resume replication: ${secondSkip.combined}`);
assert.ok(secondSkip.combined.includes(secondBrokenJournalKey), "The second skip must identify its key"); assert.ok(secondSkip.combined.includes(secondBrokenJournalKey), "The second skip must identify its key");
assert.match( assert.match(