mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-28 05:23:08 +00:00
Recover missing file provenance with Commonlib 0.1.28
This commit is contained in:
+1
-1
@@ -122,4 +122,4 @@ Each simulated device owns a separate real PouchDB database, `LiveSyncLocalDB` m
|
||||
|
||||
This is a service integration test, not a CLI subprocess or Obsidian runtime test. The file and provenance stores are in-memory fixtures; automatic conflict callbacks are observed without running interactive or automatic merge policies. PouchDB revision creation, chunk storage and retrieval, local compaction, CouchDB replication, the CLI command dispatcher, and its resolution operations are real.
|
||||
|
||||
Use the CLI and real-Obsidian procedures linked above for argument parsing, persistent host stores, file watchers, and dialogues. In particular, the real-Obsidian `stale-file-restart` scenario exercises persisted pending events and restart, and `folder-batch` exercises bulk Vault rename and deletion. These scenarios do not simulate a mobile operating system suspending the application.
|
||||
Use the CLI and real-Obsidian procedures linked above for argument parsing, persistent host stores, file watchers, and dialogues. In particular, the real-Obsidian `stale-file-restart` scenario exercises persisted pending events and restart. It checks that start-up records an unchanged file whose provenance is missing, then accepts a newer database revision without a conflict, while an unknown historical file remains protected. The `folder-batch` scenario exercises bulk Vault rename and deletion. These scenarios do not simulate a mobile operating system suspending the application.
|
||||
|
||||
@@ -84,7 +84,7 @@ The wrapper accepts only maintained real-Obsidian scenario names; run it with `-
|
||||
|
||||
`folder-batch` needs no remote service. It creates 24 notes in nested folders, renames and deletes the parent through the Obsidian Vault API, and checks descendant events, content, Chunks, deletion markers, and provenance. A note outside the parent must remain writable.
|
||||
|
||||
`stale-file-restart` needs no remote service. It advances the local database while old Vault bytes remain, persists pending storage events, and restarts the same isolated Vault and profile. It checks that an unchanged file with exact provenance receives the newer database content without creating a revision, that unknown-origin content is preserved on a fresh independent branch, and that losing provenance and processing the file again does not duplicate or automatically merge that branch. The database advance and pending snapshot are controlled fixtures; startup processing, persistence, file reflection, and conflict checking run in real Obsidian. The scenario does not simulate a mobile operating system suspending the application.
|
||||
`stale-file-restart` needs no remote service. It advances the local database while old Vault bytes remain, persists pending storage events, and restarts the same isolated Vault and profile. It checks that an unchanged file with exact provenance receives the newer database content without creating a revision, that unknown-origin content is preserved on a fresh independent branch, and that losing provenance and processing the file again does not duplicate or automatically merge that branch. A third file begins with no provenance while its bytes still match the current database revision; start-up must record that revision without creating a new one, and a later incoming revision must reflect without a conflict. The database advances and pending snapshot are controlled fixtures; start-up processing, persistence, file reflection, and conflict checking run in real Obsidian. The scenario does not simulate a mobile operating system suspending the application.
|
||||
|
||||
The principal entry points are:
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../r
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
const paths = ["stale-known.md", "stale-unknown.md"];
|
||||
const recoveredPath = "stale-recovered.md";
|
||||
const oldContent = "# Note\nKeep\n\nTail\n\nFooter\n";
|
||||
const newContent = oldContent.replace(
|
||||
"Footer\n",
|
||||
@@ -26,7 +27,7 @@ async function readState(cliBinary: string, env: NodeJS.ProcessEnv): Promise<Fil
|
||||
const core=app.plugins.plugins['obsidian-livesync'].core;
|
||||
const store=core.services.keyValueDB.openSimpleStore('file-reflection-provenance-v1');
|
||||
const states=[];
|
||||
for(const path of ${JSON.stringify(paths)}){
|
||||
for(const path of ${JSON.stringify([...paths, recoveredPath])}){
|
||||
const meta=await core.localDatabase.getDBEntryMeta(path,{conflicts:true},true);
|
||||
const branches=[];
|
||||
for(const rev of [meta._rev,...(meta._conflicts??[])]){
|
||||
@@ -82,16 +83,16 @@ async function main(): Promise<void> {
|
||||
await evalObsidianJson(
|
||||
cliBinary,
|
||||
`(async()=>{
|
||||
for(const path of ${JSON.stringify(paths)}) await app.vault.create(path,${JSON.stringify(oldContent)});
|
||||
for(const path of ${JSON.stringify([...paths, recoveredPath])}) await app.vault.create(path,${JSON.stringify(oldContent)});
|
||||
return JSON.stringify(true);
|
||||
})()`,
|
||||
session.cliEnv
|
||||
);
|
||||
for (const path of paths) await waitForLocalDatabaseEntry(cliBinary, session.cliEnv, path);
|
||||
for (const path of [...paths, recoveredPath]) await waitForLocalDatabaseEntry(cliBinary, session.cliEnv, path);
|
||||
|
||||
// Drain real Vault events before creating a persisted pending-event fixture.
|
||||
// The DB advances without reflecting it in the Vault, as on an offline device.
|
||||
const fixture = await evalObsidianJson<{ current: string[]; original: string[] }>(
|
||||
const fixture = await evalObsidianJson<{ current: string[]; original: string[]; recovered: string }>(
|
||||
cliBinary,
|
||||
`(async()=>{
|
||||
const core=app.plugins.plugins['obsidian-livesync'].core;
|
||||
@@ -110,8 +111,11 @@ async function main(): Promise<void> {
|
||||
else await store.delete(path);
|
||||
snapshot.push({type:'CHANGED',key:'CHANGED-'+path,args:{file}});
|
||||
}
|
||||
const recovered=${JSON.stringify(recoveredPath)};
|
||||
const recoveredMeta=await core.localDatabase.getDBEntryMeta(recovered,{},true);
|
||||
await store.delete(recovered);
|
||||
await core.kvDB.set('storage-event-manager-snapshot',snapshot);
|
||||
return JSON.stringify({current,original});
|
||||
return JSON.stringify({current,original,recovered:recoveredMeta._rev});
|
||||
})()`,
|
||||
session.cliEnv
|
||||
);
|
||||
@@ -120,7 +124,7 @@ async function main(): Promise<void> {
|
||||
|
||||
session = await startObsidianLiveSyncSession({ binary, cliBinary, vault });
|
||||
await waitForLiveSyncCoreReady(cliBinary, session.cliEnv);
|
||||
const [known, unknown] = await readState(cliBinary, session.cliEnv);
|
||||
const [known, unknown, recovered] = await readState(cliBinary, session.cliEnv);
|
||||
assertEqual(known.rev, fixture.current[0], "An unchanged stale file created a revision during restart.");
|
||||
assertEqual(known.branches.length, 1, "An unchanged stale file created a conflict.");
|
||||
assertEqual(known.content, newContent, "The newer DB content was not reflected after suppressing the save.");
|
||||
@@ -134,6 +138,32 @@ async function main(): Promise<void> {
|
||||
if (!unknown.branches.some((branch) => branch.content === newContent)) {
|
||||
throw new Error("The remote additions were lost.");
|
||||
}
|
||||
assertEqual(recovered.rev, fixture.recovered, "Unchanged recovery added a database revision.");
|
||||
assertEqual(recovered.branches.length, 1, "Unchanged recovery created a conflict.");
|
||||
assertEqual(recovered.content, oldContent, "Unchanged recovery modified the Vault file.");
|
||||
assertEqual(recovered.provenance, fixture.recovered, "Start-up did not recover missing file provenance.");
|
||||
|
||||
const receivedRevision = await evalObsidianJson<string>(
|
||||
cliBinary,
|
||||
`(async()=>{
|
||||
const core=app.plugins.plugins['obsidian-livesync'].core;
|
||||
const path=${JSON.stringify(recoveredPath)};
|
||||
const meta=await core.localDatabase.getDBEntryMeta(path,{},true);
|
||||
const file=await core.storageAccess.getFileStub(path);
|
||||
const data=new Blob([${JSON.stringify(newContent)}],{type:'text/plain'});
|
||||
const result=await core.localDatabase.putDBEntry({...meta,data,mtime:file.stat.mtime+60000,
|
||||
size:data.size,children:[]},false,meta._rev);
|
||||
if(!result?.ok) throw new Error('Could not advance '+path);
|
||||
if(!await core.fileHandler.dbToStorage(path,path,false)) throw new Error('Could not reflect '+path);
|
||||
return JSON.stringify(result.rev);
|
||||
})()`,
|
||||
session.cliEnv
|
||||
);
|
||||
const [, , received] = await readState(cliBinary, session.cliEnv);
|
||||
assertEqual(received.rev, receivedRevision, "Incoming revision was not retained.");
|
||||
assertEqual(received.branches.length, 1, "Incoming revision created a conflict after recovery.");
|
||||
assertEqual(received.content, newContent, "Incoming revision was not reflected after recovery.");
|
||||
assertEqual(received.provenance, receivedRevision, "Incoming reflection did not record its revision.");
|
||||
|
||||
await evalObsidianJson(
|
||||
cliBinary,
|
||||
@@ -174,7 +204,7 @@ async function main(): Promise<void> {
|
||||
assertEqual(resolved.branches.length, 1, "The explicit newer-file option did not resolve the conflict.");
|
||||
assertEqual(resolved.content, newContent, "The newer-file option did not reflect the newer DB version.");
|
||||
console.log(
|
||||
"Stale-file restart: known content reflected; unknown content preserved without duplicate branches; explicit newer-file resolution retained."
|
||||
"Stale-file restart: missing unchanged provenance recovered; later update reflected; unknown content preserved; explicit newer-file resolution retained."
|
||||
);
|
||||
} finally {
|
||||
if (session) await session.app.stop();
|
||||
|
||||
Reference in New Issue
Block a user