diff --git a/src/apps/cli/README.md b/src/apps/cli/README.md index 99614627..a74107e8 100644 --- a/src/apps/cli/README.md +++ b/src/apps/cli/README.md @@ -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` 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 acceptable, run `journal-skip `. The CLI first performs normal replication with the retained local database. It skips only when that attempt diff --git a/src/apps/cli/main.log-handler.unit.spec.ts b/src/apps/cli/main.log-handler.unit.spec.ts index 5c344a10..20b8037c 100644 --- a/src/apps/cli/main.log-handler.unit.spec.ts +++ b/src/apps/cli/main.log-handler.unit.spec.ts @@ -79,15 +79,6 @@ describe("CLI log handler", () => { it.each([ ["NOTICE", LOG_LEVEL_NOTICE], ["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], ["INFO", LOG_LEVEL_INFO], ["VERBOSE", LOG_LEVEL_VERBOSE], @@ -101,6 +92,8 @@ describe("CLI log handler", () => { it("writes detailed Commonlib diagnostics to stderr in debug mode", async () => { const messages = [ + ["NOTICE", LOG_LEVEL_NOTICE], + ["URGENT", LOG_LEVEL_URGENT], ["DEBUG", LOG_LEVEL_DEBUG], ["INFO", LOG_LEVEL_INFO], ["VERBOSE", LOG_LEVEL_VERBOSE], diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index 511e015d..6c93dd93 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -378,10 +378,15 @@ export async function main( const writeLine = avoidStdoutNoise ? writeStderrLine : writeStdoutLine; writeLine(standardIo, ...values); }; - setGlobalLogFunction((msg, level) => { - if (!options.debug && level !== LOG_LEVEL_NOTICE && level !== LOG_LEVEL_URGENT) return; - writeStderrLine(standardIo, `[${level}]`, msg); - }); + if (options.debug) { + setGlobalLogFunction((msg, level) => { + writeStderrLine(standardIo, `[${level}]`, msg); + }); + } else { + setGlobalLogFunction((msg, level) => { + // NO OP, leave it to logFunction + }); + } if (options.command === "init-settings") { await createDefaultSettingsFile(options, standardIo); return; diff --git a/test/e2e-cli/test-journal-receive-failure.ts b/test/e2e-cli/test-journal-receive-failure.ts index c659aab9..cb5f43e2 100644 --- a/test/e2e-cli/test-journal-receive-failure.ts +++ b/test/e2e-cli/test-journal-receive-failure.ts @@ -247,9 +247,12 @@ async function runScenario(encrypt: boolean): Promise { assert.notEqual(finalRevision, firstRevision); await withCorruptedJournals(client, minioBucket, [brokenJournalKey, secondBrokenJournalKey], async () => { - const failedSync = await tryDefault("--debug", "sync"); - assert.notEqual(failedSync.code, 0, "Default receive must report failure for a corrupt Journal"); - assert.match(failedSync.combined, /receive journal error|journal.*failed|replication.*failed/i); + const failedSync = await tryDefault("sync"); + assert.equal(failedSync.code, 1, "Default receive must report failure for a corrupt Journal"); + 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); assert.ok(checkpoint.has(firstJournalKey), "Completed earlier Journal must remain checkpointed"); @@ -307,14 +310,12 @@ async function runScenario(encrypt: boolean): Promise { `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.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( - firstSkip.combined.includes(secondBrokenJournalKey), - "Resumed replication must stop and identify the next broken Journal" - ); - assert.match( - firstSkip.combined, - /decrypt|inflate|decompress|decode|parse|persist|apply/i, - "The first skip attempt must report the failed Journal processing stage" + nextFailure.stderr.includes(`Receive Journal ${secondBrokenJournalKey} failed at `), + "Debug retry must identify the next broken Journal and its failure stage" ); const firstSkipCheckpoint = await readReceivedJournalKeys(skipDatabase); @@ -331,7 +332,7 @@ async function runScenario(encrypt: boolean): Promise { assert.notEqual((await trySkip("cat", secondBrokenPath)).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.ok(secondSkip.combined.includes(secondBrokenJournalKey), "The second skip must identify its key"); assert.match(