mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-05 15:58:19 +00:00
Verify NFC P2P with real Obsidian peers in Compose
This commit is contained in:
1 parent
4982d0188a
commit
8b1f03b1aa
7 files changed
+48
-17
No files matched your search
@@ -334,12 +334,20 @@ covered:
|
||||
- real Obsidian and CLI Journal round trips with plain IDs and independent
|
||||
encrypted IDs: Standard Fetch, identical Metadata IDs and Chunk content,
|
||||
retention of a pre-existing NFD physical name, edits after CLI process
|
||||
restart, and deletion reflected back into Obsidian; and
|
||||
restart, and deletion reflected back into Obsidian;
|
||||
- real CLI P2P peers in legacy mode and NFC mode, with and without encryption
|
||||
in NFC mode: canonical path reads, incompatible-peer refusal, reverse edits
|
||||
after restart, retained original revision content, logical rename, and
|
||||
deletion. Existing automatic-host, adapter-replacement, and explicit
|
||||
disconnect scenarios also passed.
|
||||
disconnect scenarios also passed; and
|
||||
- two real Obsidian `1.12.7` processes in an isolated Compose project, with NFC
|
||||
identity in plain and E2EE/Path Obfuscation modes using an independent ID key:
|
||||
initial Fetch, identical Metadata IDs and Chunk-backed content, reverse edits,
|
||||
retained original revision bodies, a full application restart and subsequent
|
||||
transfer, and explicit path-version refusal after ordinary peer permission.
|
||||
The refused peer retained its sentinel ID, revision, and content and imported
|
||||
no source Metadata or file. These cases passed on 2026-11-02 with the updated
|
||||
locally packed candidate after integrating the current main branches.
|
||||
|
||||
The Journal migration scenarios also start with existing legacy CLI databases.
|
||||
The source Rebuild changes its Metadata ID and declares compatibility range
|
||||
@@ -348,8 +356,10 @@ Metadata ID and revision and the remote milestone. After selecting NFC, its
|
||||
Fetch converges to the source ID and content while preserving the NFD physical
|
||||
path. Both plain and independently encrypted ID scenarios passed.
|
||||
|
||||
P2P runtime evidence is limited to CLI peers; a real Obsidian P2P peer remains
|
||||
unverified. The settings and reconstruction paths are covered by unit tests.
|
||||
The settings, first-join identity inspection, legacy Setup URI defaults, and
|
||||
initial CLI Fetch persistence are also covered by unit tests. Logical rename
|
||||
and deletion in the P2P extension have CLI runtime coverage; the real Obsidian
|
||||
P2P cases above do not separately exercise those operations.
|
||||
These checks do not extend NFC support to other hosts or establish native iOS
|
||||
or macOS filesystem behaviour.
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -23,7 +23,7 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "file:_testdata/nfc/vrtmrz-livesync-commonlib-0.2.3-nfc-dd26db91a1f8.tgz",
|
||||
"@vrtmrz/livesync-commonlib": "file:_testdata/nfc/vrtmrz-livesync-commonlib-0.2.3-nfc-a86e217a7ae4.tgz",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.7",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"@vrtmrz/value-store": "0.1.0",
|
||||
@@ -4066,8 +4066,8 @@
|
||||
},
|
||||
"node_modules/@vrtmrz/livesync-commonlib": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "file:_testdata/nfc/vrtmrz-livesync-commonlib-0.2.3-nfc-dd26db91a1f8.tgz",
|
||||
"integrity": "sha512-AZCxGj8RULPLe7wAJCYtfzYb6brKw69qhDhZPVUIldnkbEZlR2kzlSHHjrw1JnXRAONU3ax2r9yYsFGNz3drEg==",
|
||||
"resolved": "file:_testdata/nfc/vrtmrz-livesync-commonlib-0.2.3-nfc-a86e217a7ae4.tgz",
|
||||
"integrity": "sha512-2gN8IHd3PEumBZbuD6zPwt8zO6KnhzHQUWVMVCFNRbGDBxKLmGZjMeNDEYugnfs40sTmB1jxdbrTH5zhB/G5DA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
|
||||
+1
-1
@@ -210,7 +210,7 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "file:_testdata/nfc/vrtmrz-livesync-commonlib-0.2.3-nfc-dd26db91a1f8.tgz",
|
||||
"@vrtmrz/livesync-commonlib": "file:_testdata/nfc/vrtmrz-livesync-commonlib-0.2.3-nfc-a86e217a7ae4.tgz",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.7",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"@vrtmrz/value-store": "0.1.0",
|
||||
|
||||
@@ -350,7 +350,7 @@ describe("CLI database preparation", () => {
|
||||
const inspect = vi.spyOn(ReplicatorService.prototype, "createRemoteResource").mockResolvedValue({
|
||||
check: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
inspectPathIdentity: vi.fn(async () => ({ status: "configured", pathNormalizationVersion: 0 })),
|
||||
inspectPathIdentity: vi.fn(async () => ({ status: "configured", pathNormalizationVersion: 0 }) as const),
|
||||
dispose,
|
||||
});
|
||||
await start(
|
||||
|
||||
@@ -244,7 +244,7 @@ The P2P Setup URI workflow also schedules another Fetch and injects an exception
|
||||
|
||||
`test:e2e:obsidian:p2p-connection-check` owns the browser-to-Obsidian preflight path. It serves the WebPeer production build from loopback, asks the page to generate a disposable Setup URI using the local relay, starts its browser reference peer, and applies that exact URI through visible onboarding in an isolated empty real Obsidian Vault. After the first successful WebRTC diagnostic appears, it selects the action for another device in the same room, proves that the Setup URI was not regenerated, applies it to a second isolated empty real Obsidian Vault, and requires both the successful total and the baseline number of simultaneous active connections to advance. It captures the result card without Setup URI credentials and does not claim to verify note synchronisation. Run `test:e2e:obsidian:p2p-connection-check:services` to build both production artefacts and let the scenario start and stop the Compose relay.
|
||||
|
||||
`test:e2e:obsidian:p2p-nfc` runs the plain case and the E2EE case using Path Obfuscation and an independent ID key, serially, in an isolated Compose project with a local signalling relay and two real Obsidian processes at a time. Each case seeds an NFD-named source file, fetches it by its NFC spelling, and compares the real local Metadata ID, exact body, and referenced Chunks on both peers. It edits the note from the receiving peer, verifies that the Metadata ID remains stable and that the original revision body remains readable, then fully stops and relaunches the same Vault to check that the ID and updated body remain. A fresh path version 0 peer must show the visible P2P compatibility refusal; the test verifies that it receives no source Metadata and retains its local sentinel. Run `npm run test:e2e:obsidian:p2p-nfc -- --case plain` or `-- --case encrypted` to select one case for debugging. The host wrapper builds the plug-in, uses the existing extracted Obsidian directory and its `obsidian-cli`, mounts the repository read-only, and tears down its unique Compose project after the run. It does not download Obsidian or npm dependencies.
|
||||
`test:e2e:obsidian:p2p-nfc` runs the plain case and the E2EE case using Path Obfuscation and an independent ID key, serially, in an isolated Compose project with a local signalling relay and two real Obsidian processes at a time. Each case seeds an NFD-named source file, fetches it by its NFC spelling, and compares the real local Metadata ID, exact body, and referenced Chunks on both peers. It edits the note from the receiving peer, verifies that the Metadata ID remains stable and that the original revision body remains readable, then fully stops and relaunches the same Vault to verify the ID, updated body, original revision, and a further transfer after restart. A fresh path version 0 peer must show the visible P2P compatibility refusal; the test verifies that it receives no source Metadata and retains its local sentinel. Run `npm run test:e2e:obsidian:p2p-nfc -- --case plain` or `-- --case encrypted` to select one case for debugging. The host wrapper builds the plug-in, uses the existing extracted Obsidian directory and its `obsidian-cli`, mounts the repository read-only, and tears down its unique Compose project after the run. It does not download Obsidian or npm dependencies.
|
||||
|
||||
`test:e2e:obsidian:startup-scan` starts from a CouchDB fixture using current settings with its device-local compatibility marker already acknowledged, stops Obsidian, writes a note directly into the Vault, restarts the same isolated Vault and profile without rewriting its plug-in data, and verifies from CouchDB that the start-up scan picked up the offline file. Onboarding remains covered by `onboarding-invitation`; this scenario owns the ordinary configured restart and start-up scan.
|
||||
|
||||
@@ -408,6 +408,7 @@ When these checks pass for the release artefacts, BRAT acceptance can be limited
|
||||
- `E2E_OBSIDIAN_P2P_WORKFLOW_TIMEOUT_MS`: timeout for each visible P2P Setup URI, peer-discovery, approval, and replication control; default is 60 seconds.
|
||||
- `E2E_OBSIDIAN_P2P_NFC_TIMEOUT_MS`: timeout for P2P NFC transfers and the incompatible-peer refusal; default is 120 seconds.
|
||||
- `E2E_OBSIDIAN_P2P_NFC_UI_TIMEOUT_MS`: timeout for P2P NFC status and setup controls; default is 60 seconds.
|
||||
- `E2E_P2P_NFC_KEEP_ARTIFACTS=1`: retain successful Compose screenshots and diagnostics; failures are always retained.
|
||||
- `E2E_P2P_NFC_CASE`: case selected by the Compose wrapper (`all`, `plain`, or `encrypted`); default is `all`. The `--case` option overrides this value.
|
||||
- `E2E_P2P_NFC_OBSIDIAN_DIR`: extracted Linux Obsidian directory mounted into the Compose runner. The wrapper normally infers it from the discovered `OBSIDIAN_BINARY` and requires the corresponding `obsidian-cli` in the same directory.
|
||||
- `E2E_P2P_CHECK_CONNECTION_TIMEOUT_MS`: timeout for the browser-to-Obsidian successful WebRTC diagnostic; default is 60 seconds.
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
type LocalDatabaseEntry,
|
||||
} from "../runner/liveSyncWorkflow.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import { obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts";
|
||||
import { captureObsidianPage, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts";
|
||||
import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000";
|
||||
@@ -445,6 +445,10 @@ async function waitForDetectedPeer(port: number): Promise<void> {
|
||||
"xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]"
|
||||
);
|
||||
await pane.getByRole("button", { name: "Refresh", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
const peer = pane.locator(".peer-item").first();
|
||||
await peer.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const allow = peer.getByRole("button", { name: "Allow in session", exact: true });
|
||||
if (await allow.isVisible()) await allow.click({ timeout: uiTimeoutMs });
|
||||
await pane.getByRole("button", { name: "Replicate now" }).first().waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
@@ -522,7 +526,7 @@ async function replicateAndWait(
|
||||
let finished = false;
|
||||
const acceptor = acceptConnectionRequests([sourcePort, receiverPort], () => finished);
|
||||
try {
|
||||
await replicateFromStatusPane(sourcePort);
|
||||
await replicateFromStatusPane(receiverPort);
|
||||
await waitForVaultContent(receiverVaultPath, expectedPath, expectedContent);
|
||||
} finally {
|
||||
finished = true;
|
||||
@@ -593,7 +597,7 @@ async function runCase(definition: CaseDefinition): Promise<void> {
|
||||
const incompatibleArtifact = await createSetupArtifact(shared, 0);
|
||||
const captures: SetupCaptureNames = {
|
||||
scenario: "p2p-nfc-" + definition.name,
|
||||
guide: "p2p-nfc-" + definition.name,
|
||||
guide: "p2p-setup",
|
||||
};
|
||||
const vaults: TemporaryVault[] = [];
|
||||
let sessionA: ObsidianLiveSyncSession | undefined;
|
||||
@@ -650,9 +654,10 @@ async function runCase(definition: CaseDefinition): Promise<void> {
|
||||
);
|
||||
assertEqual(receiverEntry.content, sourceContent, "The NFC peer did not receive the exact source body.");
|
||||
|
||||
console.log("P2P NFC initial Fetch: matching IDs and exact content verified.");
|
||||
await reconnectP2PStatus(portA);
|
||||
await reconnectP2PStatus(portB);
|
||||
await waitForDetectedPeer(portB);
|
||||
await waitForDetectedPeer(portA);
|
||||
await modifyVaultNote(context.cliBinary, sessionB.cliEnv, pathNfc, editedContent);
|
||||
const locallyEditedEntry = await writeEntryStateWhenReady(
|
||||
context.cliBinary,
|
||||
@@ -676,6 +681,7 @@ async function runCase(definition: CaseDefinition): Promise<void> {
|
||||
assertEqual(originalRevision.content, sourceContent, "P2P discarded the original revision body.");
|
||||
await assertSourceNameIsNfd(vaultA.path, editedContent);
|
||||
|
||||
console.log("P2P NFC reverse transfer: stable ID and original revision verified.");
|
||||
await stopSession(context, sessionB);
|
||||
sessionB = await startSession(context, vaultB, portB);
|
||||
await waitForLiveSyncCoreReady(context.cliBinary, sessionB.cliEnv);
|
||||
@@ -692,7 +698,7 @@ async function runCase(definition: CaseDefinition): Promise<void> {
|
||||
|
||||
await reconnectP2PStatus(portA);
|
||||
await openP2PStatus(portB);
|
||||
await waitForDetectedPeer(portB);
|
||||
await waitForDetectedPeer(portA);
|
||||
await modifyVaultNote(context.cliBinary, sessionB.cliEnv, pathNfc, restartedContent);
|
||||
await writeEntryStateWhenReady(context.cliBinary, sessionB.cliEnv, pathNfc, restartedContent);
|
||||
await replicateAndWait(portB, portA, vaultA.path, pathNfc, restartedContent);
|
||||
@@ -705,6 +711,7 @@ async function runCase(definition: CaseDefinition): Promise<void> {
|
||||
assertEqual(afterRestartTransfer.id, sourceEntry.id, "A post-restart transfer changed the Metadata ID.");
|
||||
await assertSourceNameIsNfd(vaultA.path, restartedContent);
|
||||
|
||||
console.log("P2P NFC full restart: persisted data and another transfer verified.");
|
||||
await stopSession(context, sessionB);
|
||||
sessionB = undefined;
|
||||
const vaultMode0 = await createTemporaryVault("obsidian-livesync-p2p-nfc-mode0-");
|
||||
@@ -768,6 +775,19 @@ async function runCase(definition: CaseDefinition): Promise<void> {
|
||||
console.log(
|
||||
"P2P NFC case passed: NFD source identity, NFC lookup, exact Chunk-backed transfer, reverse edit, revision retention, full restart, mode 0 refusal, and sentinel preservation."
|
||||
);
|
||||
} catch (error) {
|
||||
for (const session of context.activeSessions) {
|
||||
const port = session.remoteDebuggingPort;
|
||||
await captureObsidianPage(port, `p2p-nfc-${definition.name}-${port}-failure.png`, (page) =>
|
||||
page.locator("body").waitFor({ state: "visible" })
|
||||
).catch(() => undefined);
|
||||
const state = await withObsidianPage(
|
||||
port,
|
||||
async (page) => await page.locator(".workspace-leaf-content").allTextContents()
|
||||
).catch(() => []);
|
||||
console.error(`P2P NFC failure UI on ${port}: ${JSON.stringify(state).slice(-6000)}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await stopSessions(context).catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : error);
|
||||
|
||||
@@ -85,10 +85,10 @@ async function main(): Promise<void> {
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
if (exitCode === 0) {
|
||||
if (exitCode === 0 && process.env.E2E_P2P_NFC_KEEP_ARTIFACTS !== "1") {
|
||||
await rm(artifactsDirectory, { recursive: true, force: true });
|
||||
} else {
|
||||
console.error("P2P NFC diagnostics were retained at " + artifactsDirectory);
|
||||
console.log("P2P NFC diagnostics were retained at " + artifactsDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user