Files
ng-eventually/packages/polyfill/e2e/repro-fresh-wallet.ts
T
Sylvain Duchesne 1271d48e9f refactor(e2e): la mécanique de test devient un paquet à part, ng-e2e-helpers
Créer un portefeuille, en obtenir le .ngw, traverser le broker : ce n'est pas du
ressort du polyfill. C'est un besoin commun au polyfill et à toute application
NextGraph — et surtout, ça SURVIT à la migration, alors que le polyfill est fait
pour disparaître. L'y laisser, c'était le faire mourir avec lui ou rendre le
polyfill indéracinable.

Le paquet n'importe rien du polyfill — vérifié mécaniquement — et déclare
Playwright et @ng-org/web en pairs, le consommateur devant maîtriser les
versions. Sa surface : attentes bornées, mesure, navigateur, profils,
portefeuille, traversée du broker, rapport d'exécution, et reconnaissance des
modes de panne connus.

La preuve qu'il est utilisable de l'extérieur : le polyfill le CONSOMME, sans
garder de copie. Restent chez lui les parcours, la barrière et les identités
virtuelles, qui lui sont propres.

Le verrou entre exécutions disparaît, remplacé par un profil par exécution. Il
ne traitait qu'un symptôme — un répertoire partagé que la création de
portefeuille effaçait. Avec un profil par exécution il n'y a plus rien à
sérialiser, les exécutions concurrentes deviennent indépendantes, et la
collision entre deux dépôts s'évanouit au lieu d'être exportée. Six exécutions :
aucun répertoire ni Chromium orphelin.

Et la connaissance descriptive est séparée du pilotage : URL, sélecteurs et
inventaire ordonné des écrans sont des données, passées DANS la page pour la
reconnaissance — donc un échec nomme le même écran que celui sur lequel on
dispatchait.

Un échec de navigateur est désormais nommé comme tel — « the actors browser
STOPPED ANSWERING » — au lieu de sortir sous le nom de l'opération innocente qui
se trouvait en vol.
2026-08-16 14:16:11 +02:00

129 lines
5.7 KiB
TypeScript

/**
* COLD-START repro — a genuinely FRESH wallet (never ran the app) driving the
* shim's first account resolution/provision.
*
* Unlike `run.ts`, which reuses ONE dedicated wallet (always "hot" — its
* private-store repo is already in the verifier's `self.repos`), this mints a
* BRAND-NEW wallet + fresh profile per run and opens the SDK page over it as the
* very first session. It then, in order:
* 1) probes the RAW anchored shim SELECT on `did:ng:${private_store_id}` — the
* cold-start bug surfaces here as `RepoNotFound` (the private-store repo not
* yet open) rather than a silent 0 rows;
* 2) runs `ensureAccount` (resetRegistryCache first) — the app's first-login
* bootstrap — and asserts it provisions 3 scope docs WITHOUT throwing;
* 3) re-resolves the SAME id from a fresh anchored read and asserts it returns
* the SAME docs (real persistence in the shim).
*
* Expected BEFORE the fix: step 1 throws RepoNotFound; step 2/3 fail to persist.
* Expected AFTER the fix: step 1 may still throw (raw, no open), but step 2/3
* succeed because ensureAccount opens the anchor repo before read/write.
*
* Run: `bun run e2e/repro-fresh-wallet.ts`.
*/
import type { Frame, Page, BrowserContext } from "playwright";
import { mintWalletProfileKeepingContext, setupBrokerPage, type RunProfile } from "ng-e2e-helpers";
import { WALLET, buildBundle, serveHarness } from "./harness-page";
type Check = { name: string; ok: boolean; detail?: string };
const results: Check[] = [];
function check(name: string, ok: boolean, detail?: string): void {
results.push({ name, ok, detail });
console.log(` [${ok ? "PASS" : "FAIL"}] ${name}${detail ? " — " + detail : ""}`);
}
function sdk<T>(frame: Frame, method: string, ...args: unknown[]): Promise<T> {
return frame.evaluate(
([m, a]) => (window as any).__sdk[m as string](...(a as unknown[])),
[method, args] as const,
) as Promise<T>;
}
async function main(): Promise<void> {
console.log("[repro] building SDK page bundle...");
buildBundle();
const { url, close: closeServer } = await serveHarness();
console.log(`[repro] harness served at ${url}`);
console.log("[repro] creating a BRAND-NEW wallet (fresh profile)...");
let ctx: BrowserContext | null = null;
let profile: RunProfile | null = null;
let page: Page | null = null;
try {
// A name of its own, not the batch wallet's: what this reproduction needs is a wallet
// whose private-store repo has never been opened by an application, and reusing a name
// would not give one.
const credentials = {
name: `ng-fresh-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
password: WALLET.password,
};
const fresh = await mintWalletProfileKeepingContext("the cold-start reproduction", credentials);
ctx = fresh.ctx;
profile = fresh.profile;
console.log(`[repro] fresh wallet: ${credentials.name}`);
page = await ctx.newPage();
page.on("pageerror", (e) => console.error("[iframe error]", e.message));
page.on("console", (m) => {
if (m.type() === "error") console.error("[iframe console]", m.text());
});
console.log("[repro] opening SDK page over the FRESH wallet (first-ever app session)...");
const frame = await setupBrokerPage(page, url, WALLET.password);
await frame.waitForFunction(() => (window as any).__sdk !== undefined, { timeout: 30000 });
await frame.waitForFunction(() => (window as any).__sdk.status() === "connected", {
timeout: 60000,
});
console.log("[repro] connected. Driving the cold-start shim resolution...");
// 1) RAW anchored shim probe — the diagnostic. Reports RepoNotFound if the
// private-store repo is not yet open in this fresh session.
const probe = await sdk<any>(frame, "shimAnchorProbe");
console.log(
` [DIAG] raw anchored shim read: threw=${probe.threw} error=${probe.error} rows=${probe.rows}`,
);
// 2) ensureAccount — the real bootstrap. This MUST provision cleanly on a fresh
// wallet (all 3 docs truthy, no throw). This is the load-bearing assertion.
const ensured = await sdk<any>(frame, "coldEnsureAccount", "@cold-user-1");
check(
"fresh wallet: ensureAccount provisions the account without throwing",
!ensured.threw && !!ensured.docPublic && !!ensured.docProtected && !!ensured.docPrivate,
ensured.threw
? `THREW: ${ensured.error}`
: `pub=${String(ensured.docPublic).slice(0, 22)}… prot=${String(ensured.docProtected).slice(0, 22)}…`,
);
// 3) Re-resolve from a FRESH anchored read — proves the shim actually persisted.
const verified = await sdk<any>(frame, "verifyShimPersisted", "@cold-user-1");
check(
"fresh wallet: the provisioned account persists (re-resolves the SAME docs, no RepoNotFound)",
!verified.threw &&
verified.docPublic === ensured.docPublic &&
verified.docProtected === ensured.docProtected &&
verified.docPrivate === ensured.docPrivate,
verified.threw
? `THREW: ${verified.error}`
: `same=${verified.docPublic === ensured.docPublic && verified.docProtected === ensured.docProtected}`,
);
} finally {
try { if (page) await page.close(); } catch { /* ignore */ }
try { if (ctx) await ctx.close(); } catch { /* ignore */ }
profile?.discard();
closeServer();
}
const passed = results.filter((r) => r.ok).length;
const failed = results.length - passed;
console.log(`\n══ cold-start repro: ${passed} passed, ${failed} failed ══`);
if (failed > 0) {
for (const r of results.filter((x) => !x.ok)) console.log(` - ${r.name}: ${r.detail ?? ""}`);
process.exit(1);
}
}
main().catch((e) => {
console.error("[repro] fatal:", e);
process.exit(1);
});