Verify diagnostic follow-up cancellation in Obsidian

This commit is contained in:
vorotamoroz committed 2026-10-20 09:47:29 +00:00
1 parent 579a2ff373
commit 7b5dc1243c
2 files changed
+86 -1

No files matched your search

+1 -1
View File
@@ -89,7 +89,7 @@ The wrapper accepts only maintained real-Obsidian scenario names; run it with `-
`database-diagnostics` needs no remote service. It checks the Usage command and Hatch integrity action with an isolated Vault, including visible radio choices for original names and pseudonyms, their default and keyboard navigation, the default-on integrity-only remote option, retained local findings when the remote is unavailable, cancellation during inspection with earlier rows retained, and unchanged settings, local revisions, and Vault files. It captures desktop results and the choice and result dialogues in mobile emulation, checking layout at 800×600 and 375×667, radio choices for available formats, their touch targets, and export controls. Copy and download payloads are captured inside the isolated renderer, so the scenario leaves the host clipboard and download directory unchanged.
The same scenario checks the five actions in Reporting Tools, the separate Checks and Recovery panel, Metadata progress with its document total and gauge, and collapsible Markdown. It also observes Usage revision progress with a count and discovery note during a gated read. Local lookup checks cover multiple document IDs for one path, selection of an exact ID, and a follow-up report using the original salt. The compact Markdown must retain every JSON observation reference and pseudonym, and its copy and save payloads must match the full output. Results and export controls are checked at 800×600 on desktop and 375×667 in mobile emulation, including after copy and save feedback appears.
The same scenario checks the five actions in Reporting Tools, the separate Checks and Recovery panel, Metadata progress with its document total and gauge, and collapsible Markdown. It also observes Usage revision progress with a count and discovery note during a gated read. Local lookup checks cover multiple document IDs for one path, selection of an exact ID, and a follow-up report using the original salt. Closing the follow-up dialogue during a pending Metadata read must abort the report after that read settles, without querying Chunk availability. The compact Markdown must retain every JSON observation reference and pseudonym, and its copy and save payloads must match the full output. Results and export controls are checked at 800×600 on desktop and 375×667 in mobile emulation, including after copy and save feedback appears.
`database-diagnostics-couchdb` needs the CouchDB fixture with CORS enabled. It checks that clearing the remote option completes a local database and Vault inspection with remote coverage marked not checked and makes no HTTP requests, then confirms that each new run includes the remote database by default. It also checks unencrypted and encrypted databases, both browser fetch and Obsidian's native Request API, and both name policies through the real integrity dialogue. A remote-only document with a mismatched ID must produce a finding with readable Chunks and equal content. JSON and Markdown must omit file contents and credentials. Included inspections observe real requests and require GET-only access, unchanged local settings, revisions, and Vault contents, and unchanged remote documents and Security Seed. Commonlib's separate CouchDB integration tests additionally cover a missing Security Seed without creating one.
@@ -331,6 +331,89 @@ async function assertDiagnosticFileLookup(
await dialogue.waitFor({ state: "hidden", timeout });
}
async function assertFollowUpStopsAfterClose(page: Page, hatch: Locator, token: string): Promise<void> {
await page.evaluate(`(() => {
const core = app.plugins.plugins['obsidian-livesync'].core;
const db = core.localDatabase.localDatabase;
const manager = core.services.UI.dialogManager;
const gate = {
reached: false, metadataReads: 0, chunkQueries: 0, outcome: null,
originalGet: db.get, originalAllDocsRaw: core.localDatabase.allDocsRaw,
originalOpen: manager.open,
};
gate.releasePromise = new Promise(resolve => { gate.release = resolve; });
gate.settledPromise = new Promise(resolve => { gate.settle = resolve; });
db.get = async function(id, options) {
if (id === ${JSON.stringify(shadowId)} && options?.conflicts) {
gate.metadataReads++;
if (!gate.reached) {
gate.reached = true;
await gate.releasePromise;
}
}
return await gate.originalGet.call(this, id, options);
};
core.localDatabase.allDocsRaw = async function(...args) {
if (gate.reached) gate.chunkQueries++;
return await gate.originalAllDocsRaw.apply(this, args);
};
manager.open = function(component, options, ...args) {
if (typeof options?.prepareReport === 'function') {
const prepare = options.prepareReport;
options.prepareReport = async (...reportArgs) => {
try {
const result = await prepare(...reportArgs);
gate.outcome = 'completed';
return result;
} catch (error) {
gate.outcome = error.name;
throw error;
} finally {
gate.settle();
}
};
}
return gate.originalOpen.call(this, component, options, ...args);
};
globalThis.diagnosticFollowUpCloseGate = gate;
})()`);
try {
const setting = hatch.locator(".setting-item").filter({
has: page.getByText("Find a file from a report pseudonym", { exact: true }),
});
await setting.getByRole("button", { name: "Find file", exact: true }).click();
const dialogue = page.locator(".diagnostic-file-lookup");
await dialogue.getByLabel("Report path pseudonym", { exact: true }).fill(token);
await dialogue.getByRole("button", { name: "Find file", exact: true }).click();
const candidate = dialogue.locator("[data-diagnostic-candidate]").filter({ hasText: shadowId });
await candidate.getByRole("button", { name: "Prepare follow-up report", exact: true }).click();
await page.waitForFunction("globalThis.diagnosticFollowUpCloseGate.reached", undefined, { timeout });
await dialogue.getByRole("button", { name: "Close", exact: true }).click();
await dialogue.waitFor({ state: "hidden", timeout });
const result = await page.evaluate(`(async () => {
const gate = globalThis.diagnosticFollowUpCloseGate;
gate.release();
await gate.settledPromise;
return { outcome: gate.outcome, metadataReads: gate.metadataReads, chunkQueries: gate.chunkQueries };
})()`);
assert.deepEqual(
result,
{ outcome: "AbortError", metadataReads: 1, chunkQueries: 0 },
"Closing a pending follow-up must stop inspection after the active Metadata read settles"
);
} finally {
await page.evaluate(`(() => {
const gate = globalThis.diagnosticFollowUpCloseGate;
const core = app.plugins.plugins['obsidian-livesync'].core;
gate.release();
core.localDatabase.localDatabase.get = gate.originalGet;
core.localDatabase.allDocsRaw = gate.originalAllDocsRaw;
core.services.UI.dialogManager.open = gate.originalOpen;
delete globalThis.diagnosticFollowUpCloseGate;
})()`);
}
}
async function main(): Promise<void> {
const binary = requireObsidianBinary();
const cli = requireObsidianCli();
@@ -475,6 +558,7 @@ async function main(): Promise<void> {
activeSession
);
hatch = await (await openLiveSyncSettings(page, timeout)).openPage("Hatch");
await assertFollowUpStopsAfterClose(page, hatch, usageFixtureRow.path);
await captureObsidianElement(activeSession.remoteDebuggingPort, "database-diagnostics-hatch.png", (page) =>
page.locator(".modal.mod-settings")
);
@@ -805,6 +889,7 @@ async function main(): Promise<void> {
mobileScreenshot,
mobileResultScreenshot,
readOnly: true,
followUpCloseCancellation: true,
})
);
} catch (error) {