Trim prototype-era artifacts that no longer fit a local-first Deno template: - remove the M-series ADRs (docs/decisions/) and the m2 abstraction-leaks note - delete one-off golden-path/env/migration/secret check tools and the jazz2 spike + reference runners under tools/ - drop stale output/ evidence and the retired environment tooling test - relocate spikes/ to docs/spikes/ and add docs/seed.md
- commit
- 362a631bd93d975d342d0d347785c8915ffbaaed
- parent
- 0559efb261e75b743f2fee4d9b866e979d81fc75
- author
- Dami <dami.etoile@gmail.com>
@@ -1,63 +0,0 @@
1-# 0001: Graduate the Jazz 2.0.0-alpha.53 data path with lofi-owned edges2-3-Status: accepted for M1 integration\4-Date: 2026-07-15\5-Issue: #7\6-Evidence:7-[`spikes/jazz2-baseline/evidence/2026-07-15.md`](../../spikes/jazz2-baseline/evidence/2026-07-15.md)8-9-## Decision10-11-Use the relational Jazz 2 API from the exact `jazz-tools@2.0.0-alpha.53` npm pin for the M112-prototype:13-14-- declare the schema with `schema.table` and `schema.defineApp`;15-- create one browser `Db` with the persistent driver and a namespaced `BrowserAuthSecretStore`16- identity;17-- render reactive state from `Db.subscribeAll`;18-- treat a write as locally retained only after `wait({ tier: "local" })`;19-- treat a cloud write as confirmed only after `wait({ tier: "global" })`; and20-- keep raw Jazz setup, worker configuration, and transport details behind lofi's runtime module.21-22-Keep `JAZZ_APP_ID` and `JAZZ_SERVER_URL` as lofi's public configuration names. The Deno/Vite edge23-maps the complete validated pair to Jazz's internal `VITE_JAZZ_*` names. `JAZZ_ADMIN_SECRET` and24-`BACKEND_SECRET` remain server-only and unprefixed.25-26-## Contract deltas27-28-- Revise `DX-OBS-01`: configuration proves only that sync is configured. This pin has no public live29- connection-state or pending-operation-count API. lofi may report per-write local/global30- confirmation and explicitly unavailable detail, but not fabricate “connected”.31-- Retain the persistent driver as the default for `DX-DUR-01`; unsupported browser capabilities fail32- boot rather than silently selecting memory.33-- Validate the desktop/browser portion of `DX-LOCAL-01` and the vendor portion of `DX-SYNC-01`.34- Integrated Astro/Preact ownership remains for #5/#6.35-- Confirm the lofi-owned environment names for `DX-ENV-01`; the final Astro build still has to pass36- server-secret scanning.37-38-## Why39-40-The exact package passed local writes, subscription updates, reload retention, offline writes,41-multi-tab propagation, managed-cloud global durability, and a clean second-client download. Its42-relational API matches lofi's intended author model without inventing a second schema language.43-44-The package is alpha and its framework integration assumes a conventional npm layout. Keeping the45-edge lofi-owned lets the product absorb the required Deno/Vite filesystem allowance, first-run env46-pre-seeding, singleton lifecycle, and secret-safe command output without leaking those mechanics47-into product UI.48-49-## Rejected alternatives50-51-- **Use Jazz Classic 0.20 APIs from `init.md`:** those CoValue APIs do not describe this package.52-- **Wrap Jazz in a provider-neutral data abstraction now:** M1 has no second provider and would53- merely create an untested shadow API.54-- **Report connection state from configuration:** a configured URL is not transport evidence.55-- **Fall back to memory automatically:** this violates retained-write and failure-honesty promises.56-- **Track the generated Vite-prefixed `.env`:** it makes vendor implementation names part of the57- public contract and does not solve secret-safe cloud configuration.58-59-## Sources60-61-- [Exact npm package](https://www.npmjs.com/package/jazz-tools/v/2.0.0-alpha.53)62-- [Jazz source snapshot inspected for context](https://github.com/garden-co/jazz/tree/3384730e1c4509524b38c52fc9102fc6753411aa)63-- [Official TypeScript local-first starter at that snapshot](https://github.com/garden-co/jazz/tree/3384730e1c4509524b38c52fc9102fc6753411aa/starters/ts-localfirst)@@ -1,33 +0,0 @@
1-# ADR 0002: Graduate the thin Preact adapter2-3-Status: accepted\4-Date: 2026-07-15\5-Issues: #5, #66-7-## Decision8-9-Use Preact directly over a small lofi-owned external store backed by Jazz's vanilla `Db` API. Keep10-one browser-global database promise, share one vendor query subscription across equivalent island11-consumers, and replace adapter instances during HMR without shutting down the database.12-13-Product components may use Preact, lofi hooks, and application types. They may not import Jazz,14-construct a client/provider, choose a transport, or branch on storage implementation.15-16-## Why17-18-Jazz 2.0.0-alpha.53's React bindings require React 19's `use`. Preact 10.29.7 compat does not export19-that API, so the alias-only control fails its production build. A custom semantic polyfill would be20-a larger and riskier compatibility layer than the adapter it was meant to avoid.21-22-The thin path passed two-root reactivity, retained local writes, explicit and concurrent client23-recreation, teardown, error visibility, deferred-initialization disposal, and five HMR cycles. Its24-reachable JavaScript is 6,983 gzip bytes larger than the single-view vanilla API control; that is an25-indicative floor rather than an equivalent-UI bundle claim.26-27-## Consequences28-29-- Jazz alpha churn is isolated to `apps/reference/src/_lofi` and schema/config files.30-- Component authors get context-free hooks that work across independent Astro islands.31-- The adapter owns query snapshot identity, cleanup, mutation error mapping, and HMR lifecycle.32-- Deferred asynchronous subscription failures remain limited by Jazz's public API and must not be33- represented as observable UI errors until the vendor exposes a channel.@@ -1,29 +0,0 @@
1-# ADR 0003: Use static Astro through the Deno command surface2-3-Status: accepted\4-Date: 2026-07-15\5-Issue: #66-7-## Decision8-9-Use Astro 7 static output with independently hydrated Preact islands. Run development, checking,10-testing, building, and production preview through repository-owned Deno tasks. Serve the production11-directory with the small Deno preview command.12-13-Do not add `@deno/astro-adapter` to the graduated application until an on-demand route exists.14-15-## Why16-17-The local-first PWA needs a prerendered offline-capable shell and currently has no server-rendered18-route. Static Astro built and hydrated the exact Jazz stack under Deno, starts within the contract19-budget, and avoids an unnecessary runtime/deployment layer. A clean-room Deno adapter control also20-passed, so the decision is about product simplicity rather than a blocked fallback.21-22-## Consequences23-24-- `deno task preview` serves retained build output, not a fresh implicit build.25-- The wrapper owns stable user-facing status, safe environment projection, signal forwarding, and26- the post-build secret scan.27-- Astro/Vite/Jazz npm compatibility details remain internal to config and generated runtime files.28-- A future SSR requirement must open a new contract decision and can adopt the already-proven Deno29- adapter path.@@ -1,37 +0,0 @@
1-# ADR 0004: Reject Jazz alpha.53 passkey backup2-3-Status: accepted\4-Date: 2026-07-15\5-Issue: #86-7-## Decision8-9-Do not ship or invoke `BrowserPasskeyBackup` from `jazz-tools@2.0.0-alpha.53`. Present the identity10-as a device-local auth key. State explicitly that passkey backup is blocked by security review and11-that clearing site data can make the identity unrecoverable.12-13-Do not expose 24-word phrase export/import in M1. Retain its exact semantics for a later deliberate14-UX: it is a reversible encoding of the full-control auth seed and recovers identity authority, not15-unsynced local data.16-17-## Why18-19-The exact package stores the raw 32-byte Jazz auth secret in the WebAuthn user handle and reads it20-back from `AuthenticatorAssertionResponse.userHandle`. The WebAuthn specification defines that field21-as an account identifier and warns that authenticators may reveal it without prior user22-verification. Ceremony flags and credProtect do not establish a general confidentiality contract for23-the field.24-25-The API also cannot prove that a created credential is synced or manage/revoke its credentials.26-Physical-device success would therefore not establish the security or recovery promise lofi needs.27-28-## Replacement acceptance29-30-A replacement must:31-32-- keep the auth seed out of WebAuthn identifiers and non-confidential metadata;33-- wrap/encrypt the seed using an authenticator-derived or otherwise reviewed secret;34-- bind a stable RP ID and verify the complete ceremony/server record as applicable;35-- distinguish local credential creation from ecosystem backup state;36-- support duplicate prevention, inventory, replacement, and recovery failure UX;37-- retain real iOS and Android browser/installed-app evidence before graduation.@@ -1,38 +0,0 @@
1-# ADR 0005: Publish one `@nzip/lofi` package2-3-Status: accepted\4-Date: 2026-07-155-6-Owner confirmation: FelineStateMachine explicitly confirmed `@nzip/lofi` as the canonical package7-after #21 and superseded the earlier `@lofi-cat/*` direction. The `@nzip` JSR scope is the owned8-nzip publishing surface and already lists the reserved `@nzip/lofi` package alongside the published9-nzip packages. Publication credentials remain a release-time secret, not repository configuration.10-11-## Decision12-13-Use `@nzip/lofi` as lofi's only JSR package. Public framework boundaries remain explicit subpath14-exports such as `@nzip/lofi/core`, `@nzip/lofi/sync`, `@nzip/lofi/auth`, `@nzip/lofi/ui`, and15-`@nzip/lofi/pwa`. Public tooling graduates under the same package, including the M2 creation command16-at `jsr:@nzip/lofi/create` and testing helpers at `@nzip/lofi/testing`.17-18-Do not publish lockstep `@lofi-cat/*` packages. Internal module boundaries and dependency direction19-remain enforceable even though those modules share one package version.20-21-## Why22-23-The prototype's modules evolve together around a fast-moving pinned Jazz alpha. One JSR package24-provides one version, one publish operation, and direct subpath imports without creating independent25-release promises before the seams have been proven by a second application.26-27-## Consequences28-29-- The M2 public create command is `deno run -A jsr:@nzip/lofi/create <name>`.30-- Consumers import framework APIs through named subpaths, not the package root.31-- Package extraction validates export boundaries, but does not create independently versioned32- artifacts.33-- A future split requires evidence that a module needs an independent compatibility or release34- lifecycle.35-36-## Registry evidence37-38-- [JSR `@nzip` scope](https://jsr.io/@nzip), including the reserved `@nzip/lofi` package@@ -1,80 +0,0 @@
1-# 0006: Bound Layer 3 inspection and testing to truthful observable seams2-3-Status: accepted for M2 implementation\4-Date: 2026-07-15\5-Issues: #10, #126-7-## Decision8-9-Layer 3 graduates two developer surfaces:10-11-- a visible development-only inspector backed by a provisional internal snapshot/action adapter;12-- readiness-first Playwright helpers exported from the single package at `@nzip/lofi/testing`.13-14-The inspector is generated runtime machinery, not a public framework facade. `./testing` is the only15-new public package export in this layer. Broader `core`, `ui`, `sync`, `auth`, and `pwa` extraction16-still requires the M4 evidence described by ADR 0005.17-18-The inspector may report only signals lofi can observe:19-20-- device-local identity and the blocked alpha passkey-backup state;21-- requested/open/failed persistent storage plus browser persistence permission;22-- local-only or managed-configured mode;23-- lofi-wrapped per-write local/global durability and mutation failures; and24-- active client, consumer, vendor-subscription, and mutation-listener counts.25-26-The Jazz alpha pin exposes no supported live connection state, vendor queue depth, or multi-tab27-leader/follower role. Those fields remain visibly unavailable. Configuration is never presented as28-connectivity, and checklist writes are not mislabeled as the vendor's complete pending queue.29-30-Inspector transport pause/resume calls Jazz `Db.disconnect()`/`reconnect()` only when managed sync31-is configured and labels the action as a cloud-transport test seam. Real browser offline control is32-owned by `@nzip/lofi/testing` through Playwright `BrowserContext.setOffline()`. Client restart uses33-a full document reload: Jazz alpha.53 does not reliably reopen the same browser-broker namespace34-after an in-document `Db.shutdown()`/`createDb()` cycle, while a document lifecycle restart retains35-the locally durable replica. Replica clear uses `Db.deleteClientStorage()` behind a destructive36-confirmation, then reloads the document; it clears the local OPFS namespace while preserving the37-localStorage identity and warns that unsynced data may be lost.38-39-## Two-client identity rule40-41-The current permissions are creator-only. Two distinct user identities therefore cannot both read42-and edit one row, so claiming distinct-identity convergence would be false. The convergence fixture43-uses two isolated browser contexts and separate OPFS replicas with the same restored identity. The44-first context's Playwright storage state is passed directly to the second context in memory. It is45-never written to reports, traces, snapshots, logs, or screenshots because it contains the bearer46-identity secret.47-48-A truly distinct-user fixture belongs with sharing/invitation and permission-model work, not this49-testing layer.50-51-## Verification boundary52-53-Local acceptance proves readiness predicates, offline restoration in `finally`, restart/reload,54-artifact redaction, inspector actions, and production exclusion. Closing the convergence contract55-also requires a cloud-configured generated-project journey:56-57-1. seed a row and wait for global durability;58-2. boot the second isolated replica with the in-memory restored identity;59-3. take both browser contexts offline;60-4. make orthogonal edits, then reload/recreate while work remains local;61-5. restore network control in `finally` and wait for both visible snapshots to converge; and62-6. remove fixture rows and wait for global durability.63-64-Only the public `JAZZ_APP_ID`/`JAZZ_SERVER_URL` pair reaches the child application. Server secrets65-remain blank, and no configured value is retained in evidence.66-67-Production builds must contain no lofi inspector marker, global bridge, route, or chunk. Jazz's raw68-vendor inspector remains disabled.69-70-## Deferred physical-device evidence71-72-Playwright does not graduate installed-PWA lifecycle, storage eviction, airplane-mode cold launch,73-real passkey custody, or mobile background/foreground recovery. Those remain explicit M3 physical-74-device gates.75-76-## Why77-78-This boundary gives developers useful controls and deterministic tests without inventing vendor79-precision, leaking identity material, weakening creator-only permissions, or prematurely turning the80-M1 runtime into a general framework API.@@ -1,20 +0,0 @@
1-# Decision records2-3-Every M1 spike produces a decision record with this shape:4-5-1. **Question** — the falsifiable choice or platform claim.6-2. **Contract IDs** — promises exercised by the experiment.7-3. **Exact inputs** — package, runtime, browser, OS, and device versions.8-4. **Control** — the smallest vendor-supported baseline.9-5. **Procedure** — commands and manual steps someone else can repeat.10-6. **Evidence** — repository paths, raw-output checksums when useful, tested commit SHA, timestamp,11- measurements, tests, and limitations.12-7. **Decision** — adopt, revise, defer, or reject.13-8. **Contract delta** — exact promises changed by the result.14-9. **Follow-up** — later issue, if the decision intentionally leaves work.15-16-Vendor and platform claims cite primary documentation, changelog/source locations, or retained17-device evidence. A search summary without a stable source is not evidence.18-19-A spike is complete when it supports a decision, including a negative decision. It is not complete20-merely because a demo rendered once.@@ -1,24 +0,0 @@
1-# M2 abstraction-leak ledger2-3-Status: active during M2\4-Last reviewed: 2026-07-155-6-This ledger keeps the Layer 1 reference application honest. A listed leak is not a supported lofi7-API; it is a concrete input to the layer that owns removing or productizing it.8-9-| Leak retained in Layer 1 | Why it remains | Owning issue | Graduation evidence |10-| ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- |11-| Repository-root wrappers still own the checkout journey; generated commands now live in `@nzip/lofi`. | The generated path is implemented without repository-relative paths, but this leak remains listed until exact-head generated golden evidence is retained. | #13, #14 | A generated directory runs its own documented tasks without repository-relative paths. |12-| `schema.ts` and `permissions.ts` import the pinned Jazz schema DSL directly. | M1 rejected inventing a second schema language without a proven ergonomic gain. | Later API extraction after M2 | A second real application demonstrates a smaller stable facade, or the direct DSL is accepted explicitly. |13-| `app.ts` uses a provisional plain configuration object and the Jazz schema value. | Layer 1 needs configuration-as-data but does not yet have two consumers that justify a public factory or exported type. | #13 | The generator emits the approved configuration without raw client, transport, or environment plumbing. |14-| Live socket state, vendor queue depth, and multi-tab role remain unavailable. | Jazz alpha.53 exposes mutation durability but no supported signal for these details; ADR 0006 accepts explicit unavailability instead of fabricated precision. | Jazz baseline review | A later Jazz pin exposes supported signals, or the unavailable boundary remains an intentional product contract. |15-16-## Graduated in Layer 317-18-| Former leak | Graduated surface | Evidence |19-| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |20-| Runtime cardinality and lifecycle evidence used a development-only probe. | The generated dev-only inspector owns redacted diagnostics and safe actions; production builds contain no inspector marker, bridge, route, or chunk. | [ADR 0006](decisions/0006-bound-layer3-inspection-and-testing.md), inspector unit coverage, and generated golden restart/clear/production-exclusion assertions in `tools/golden_path.ts`. |21-| Browser orchestration existed only in the repository checkout journey. | `@nzip/lofi/testing` exports readiness, offline restoration, two-client identity isolation, convergence coordination, and sanitized failure artifacts. | [ADR 0006](decisions/0006-bound-layer3-inspection-and-testing.md), generated subpath-resolution coverage, real Chromium integration, and the cloud two-replica convergence gate. |22-23-Deleting an entry requires retained evidence in the owning issue or a decision record accepting the24-behavior as an intentional public boundary.No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
No content changes.
Binary file.
Binary file.
@@ -1,22 +0,0 @@
1-import { childEnvironment, validateEnvironment } from "./environment.ts";2-3-function assert(condition: unknown, message: string): asserts condition {4- if (!condition) throw new Error(message);5-}6-7-Deno.test("local child environment clears Jazz configuration without inventing Vite inputs", () => {8- const validation = validateEnvironment({});9- assert(validation.ok, "empty configuration should select local-only mode");10- const environment = childEnvironment(validation, {11- PATH: "/test/bin",12- VITE_JAZZ_APP_ID: "ambient-value",13- UNRELATED: "ambient-value",14- });15- assert(environment.PATH === "/test/bin", "allowlisted process input was dropped");16- assert(environment.JAZZ_APP_ID === "", "local mode retained JAZZ_APP_ID");17- assert(environment.JAZZ_SERVER_URL === "", "local mode retained JAZZ_SERVER_URL");18- assert(environment.JAZZ_ADMIN_SECRET === "", "local mode retained JAZZ_ADMIN_SECRET");19- assert(environment.BACKEND_SECRET === "", "local mode retained BACKEND_SECRET");20- assert(!("VITE_JAZZ_APP_ID" in environment), "local mode invented a Vite-only Jazz input");21- assert(!("UNRELATED" in environment), "child environment retained unrelated ambient input");22-});@@ -1,21 +0,0 @@
1-import { validateEnvironment } from "./env_contract.ts";2-import { loadEnvironment } from "./load_env.ts";3-4-const validation = validateEnvironment(await loadEnvironment());5-6-console.log(`lofi environment mode: ${validation.mode}`);7-console.log(8- `client variables present: ${validation.presentClientNames.join(", ") || "none"}`,9-);10-console.log(11- `server-only variables present: ${validation.presentServerNames.join(", ") || "none"}`,12-);13-14-for (const warning of validation.warnings) console.warn(`warning: ${warning}`);15-16-if (!validation.ok) {17- for (const error of validation.errors) {18- console.error(`error: ${error}`);19- }20- Deno.exit(1);21-}@@ -1,161 +0,0 @@
1-import { schema as s } from "jazz-tools";2-import { referenceApp } from "../apps/reference/src/app.ts";3-import migration from "../apps/reference/src/migrations/20260715T194947-notes-to-tasks-6c62fec42c35-ff85ac1d97ee.ts";4-5-const SCHEMA_DIR = "apps/reference/src";6-const MIGRATIONS_DIR = `${SCHEMA_DIR}/migrations`;7-const SNAPSHOTS_DIR = `${MIGRATIONS_DIR}/snapshots`;8-const JAZZ_TOOLS = "npm:jazz-tools@2.0.0-alpha.53";9-10-function fail(message: string): never {11- throw new Error(`migration contract: ${message}`);12-}13-14-async function jazz(args: string[]): Promise<string> {15- const output = await new Deno.Command(Deno.execPath(), {16- args: ["run", "-A", JAZZ_TOOLS, ...args, "--schema-dir", SCHEMA_DIR],17- stdout: "piped",18- stderr: "piped",19- }).output();20- if (!output.success) {21- const error = new TextDecoder().decode(output.stderr).trim();22- fail(`Jazz CLI failed: ${error || `exit ${output.code}`}`);23- }24- return new TextDecoder().decode(output.stdout).trim();25-}26-27-async function jazzSnapshot(hash: string): Promise<unknown> {28- const isolatedEnvironment: Record<string, string> = {};29- for (30- const name of [31- "JAZZ_APP_ID",32- "JAZZ_SERVER_URL",33- "JAZZ_ADMIN_SECRET",34- "BACKEND_SECRET",35- "VITE_JAZZ_APP_ID",36- "VITE_JAZZ_SERVER_URL",37- "PUBLIC_JAZZ_APP_ID",38- "PUBLIC_JAZZ_SERVER_URL",39- "NEXT_PUBLIC_JAZZ_APP_ID",40- "NEXT_PUBLIC_JAZZ_SERVER_URL",41- "EXPO_PUBLIC_JAZZ_APP_ID",42- "EXPO_PUBLIC_JAZZ_SERVER_URL",43- ]44- ) isolatedEnvironment[name] = "";45- const output = await new Deno.Command(Deno.execPath(), {46- args: [47- "run",48- "-A",49- JAZZ_TOOLS,50- "schema",51- "export",52- "--schema-hash",53- hash,54- "--migrations-dir",55- MIGRATIONS_DIR,56- ],57- env: isolatedEnvironment,58- stdout: "piped",59- stderr: "piped",60- }).output();61- if (!output.success) {62- const error = new TextDecoder().decode(output.stderr).trim();63- fail(`snapshot ${hash} does not hash to its filename: ${error || `exit ${output.code}`}`);64- }65- return JSON.parse(new TextDecoder().decode(output.stdout));66-}67-68-function hashField(source: string, field: "fromHash" | "toHash"): string {69- const match = source.match(new RegExp(`${field}:\\s*["']([0-9a-f]{12})["']`));70- if (!match) fail(`reviewed migration is missing a 12-character ${field}`);71- return match[1];72-}73-74-const hashOutput = await jazz(["schema", "hash"]);75-const currentHash = hashOutput.match(/Current schema hash:\s*([0-9a-f]{12})/)?.[1];76-if (!currentHash) fail(`could not read the current hash from Jazz CLI output: ${hashOutput}`);77-78-const migrationNames: string[] = [];79-for await (const entry of Deno.readDir(MIGRATIONS_DIR)) {80- if (entry.isFile && entry.name.endsWith(".ts")) migrationNames.push(entry.name);81-}82-if (migrationNames.length === 0) fail(`no reviewed migrations found in ${MIGRATIONS_DIR}`);83-84-let currentEdgeFound = false;85-for (const name of migrationNames.sort()) {86- const source = await Deno.readTextFile(`${MIGRATIONS_DIR}/${name}`);87- const fromHash = hashField(source, "fromHash");88- const toHash = hashField(source, "toHash");89- if (!name.endsWith(`-${fromHash}-${toHash}.ts`)) {90- fail(`${name} does not encode its declared ${fromHash} -> ${toHash} edge`);91- }92- for (const hash of [fromHash, toHash]) {93- const snapshotNames = [];94- for await (const entry of Deno.readDir(SNAPSHOTS_DIR)) {95- if (entry.isFile && entry.name.endsWith(`-${hash}.json`)) snapshotNames.push(entry.name);96- }97- if (snapshotNames.length !== 1) {98- fail(`${name} requires exactly one ${hash} snapshot; found ${snapshotNames.length}`);99- }100- const snapshot = JSON.parse(101- await Deno.readTextFile(`${SNAPSHOTS_DIR}/${snapshotNames[0]}`),102- );103- if (JSON.stringify(await jazzSnapshot(hash)) !== JSON.stringify(snapshot)) {104- fail(`${snapshotNames[0]} does not round-trip through Jazz's hash loader`);105- }106- }107- if (toHash === currentHash) currentEdgeFound = true;108-}109-if (!currentEdgeFound) {110- fail(`current schema ${currentHash} has no reviewed incoming migration edge`);111-}112-113-const currentSnapshots: string[] = [];114-for await (const entry of Deno.readDir(SNAPSHOTS_DIR)) {115- if (entry.isFile && entry.name.endsWith(`-${currentHash}.json`)) {116- currentSnapshots.push(entry.name);117- }118-}119-if (currentSnapshots.length !== 1) {120- fail(`current schema ${currentHash} must have exactly one snapshot`);121-}122-123-const currentSnapshot = JSON.parse(124- await Deno.readTextFile(`${SNAPSHOTS_DIR}/${currentSnapshots[0]}`),125-);126-if (JSON.stringify(referenceApp.schema.wasmSchema) !== JSON.stringify(currentSnapshot)) {127- fail(`current snapshot ${currentSnapshots[0]} does not match schema.ts`);128-}129-130-const previousSnapshotName = [];131-for await (const entry of Deno.readDir(SNAPSHOTS_DIR)) {132- if (entry.isFile && entry.name.endsWith("-6c62fec42c35.json")) {133- previousSnapshotName.push(entry.name);134- }135-}136-if (previousSnapshotName.length !== 1) fail("the M1 source snapshot is missing or ambiguous");137-const previousSnapshot = JSON.parse(138- await Deno.readTextFile(`${SNAPSHOTS_DIR}/${previousSnapshotName[0]}`),139-);140-if (JSON.stringify(s.defineApp(migration.from).wasmSchema) !== JSON.stringify(previousSnapshot)) {141- fail("the reviewed lens source does not match the M1 snapshot");142-}143-if (JSON.stringify(s.defineApp(migration.to).wasmSchema) !== JSON.stringify(currentSnapshot)) {144- fail("the reviewed lens target does not match the current snapshot");145-}146-147-const expectedForward = [{148- table: "tasks",149- renamedFrom: "notes",150- operations: [151- { type: "rename", column: "body", value: "text" },152- { type: "introduce", column: "completed", sqlType: "BOOLEAN", value: false },153- ],154-}];155-if (JSON.stringify(migration.forward) !== JSON.stringify(expectedForward)) {156- fail("the reviewed lens must rename notes/body and add completed=false");157-}158-159-console.log(160- `migration contract: ${migrationNames.length} reviewed edge(s), current schema ${currentHash}`,161-);@@ -1,40 +0,0 @@
1-import { collectBuildArtifactFiles } from "./artifact_files.ts";2-import { serverEnvironmentNames } from "./env_contract.ts";3-import { loadNamedEnvironment } from "./load_env.ts";4-import { findSecretLeaks } from "./secret_scan.ts";5-6-async function sourceFiles(): Promise<string[]> {7- const output = await new Deno.Command("git", {8- args: ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],9- stdout: "piped",10- }).output();11- if (!output.success) throw new Error("Unable to enumerate source files.");12- return new TextDecoder().decode(output.stdout).split("\0").filter(Boolean);13-}14-15-const environment = await loadNamedEnvironment(serverEnvironmentNames);16-const secretValues = serverEnvironmentNames17- .map((name) => ({ name, value: environment[name]?.trim() ?? "" }))18- .filter(({ value }) => value.length > 0);19-const buildArtifacts = await collectBuildArtifactFiles();20-const candidates = [...new Set([...await sourceFiles(), ...buildArtifacts])];21-const files: Array<{ path: string; content: Uint8Array }> = [];22-23-for (const path of candidates) {24- try {25- files.push({ path, content: await Deno.readFile(path) });26- } catch (error) {27- if (error instanceof Deno.errors.NotFound) continue;28- throw error;29- }30-}31-32-const leaks = findSecretLeaks(files, secretValues);33-if (leaks.length > 0) {34- for (const leak of leaks) console.error(`error: ${leak.name} value found in ${leak.path}`);35- Deno.exit(1);36-}37-38-console.log(39- `secret leak check passed (${candidates.length} files including ${buildArtifacts.length} build artifacts, ${secretValues.length} available server secrets)`,40-);@@ -1,1220 +0,0 @@
1-import { join, resolve } from "node:path";2-import {3- type BrowserTestClient,4- createTwoClientFixture,5- runConcurrentOfflineConvergence,6-} from "@nzip/lofi/testing";7-import { type Browser, type BrowserContext, chromium, type Page } from "playwright";8-import {9- clientEnvironmentNames,10- environmentNames,11- serverEnvironmentNames,12- validateEnvironment,13-} from "./env_contract.ts";14-import {15- artifactPaths,16- type CapturedProcess,17- type CommandRecord,18- initializeFixtureGit,19- installNodeSentinel,20- type JourneyAssertion,21- type JourneyReport,22- type JourneySource,23- materializeCleanProject,24- probeHttpReady,25- reserveLocalPort,26- runCapturedCommand,27- safeChildEnvironment,28- scanAuthorBoundary,29- startReadyProcess,30- writeJourneyReport,31-} from "./golden_path_core.ts";32-import { loadEnvironment } from "./load_env.ts";33-import { checklistUi } from "../apps/reference/src/ui-contract.ts";34-import { redactDiagnosticText } from "../package/testing/safety.ts";35-import { readTraceArchive, sanitizeTraceArchive } from "../package/testing/trace.ts";36-import { assert } from "./assert.ts";37-38-export type GoldenPathCommands = {39- doctor?: string;40- dev: string;41- check: string;42- test: string;43- build: string;44- preview: string;45-};46-47-export type GoldenPathOptions = {48- source: JourneySource;49- /** Repository working tree for checkout mode, or the already-created project for create mode. */50- projectRoot?: string;51- artifactRoot?: string;52- authorPaths?: string[];53- commands?: Partial<GoldenPathCommands>;54- cacheMode?: "existing" | "cold";55- environmentMode?: "isolated-local" | "cloud-from-root";56- timeoutMs?: number;57- islandLabels?: [string, string];58- hmrPath?: string;59- initialCommands?: CommandRecord[];60- sourceRevisionRoot?: string;61- resetArtifactRoot?: boolean;62- journeyStartedAt?: Date;63- journeyStartedPerformanceMs?: number;64- createDurationMs?: number;65- developerCommandCount?: number;66-};67-68-const defaultCommands: GoldenPathCommands = {69- dev: "dev",70- check: "check",71- test: "test",72- build: "build",73- preview: "preview",74-};75-76-const defaultAuthorPaths = [77- "apps/reference/src/app.ts",78- "apps/reference/src/islands",79- "apps/reference/src/pages",80- "apps/reference/src/permissions.ts",81- "apps/reference/src/schema.ts",82- "apps/reference/src/styles",83-];84-85-const retainedBody = "Golden path retained item";86-const updatedBody = "Golden path retained item updated";87-const disposableBody = "Golden path disposable item";88-const offlineDevelopmentBody = "Golden path development offline item";89-const offlineProductionBody = "Golden path production offline item";90-const offlineProductionSyncedBody = "Golden path production offline item synced";91-const replicaClearBody = "Golden path replica-clear disposable item";92-const convergenceTextSeedBody = "Golden path two-client text item";93-const convergenceEditedBody = "Golden path two-client text item edited";94-const convergenceCompletionBody = "Golden path two-client completion item";95-96-function taskArgs(task: string, extra: string[] = []): string[] {97- return ["task", task, ...(extra.length > 0 ? ["--", ...extra] : [])];98-}99-100-function escaped(value: string): string {101- return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");102-}103-104-async function waitForItem(105- page: Page,106- islandLabels: readonly string[],107- body: string,108-): Promise<void> {109- for (const [index, label] of islandLabels.entries()) {110- const island = page.locator("[data-island]").filter({ hasText: label });111- const item = island.getByRole("listitem").filter({ hasText: body });112- if (index === 0) {113- const failure = island.getByRole("status").filter({ hasText: checklistUi.writeFailed });114- await item.or(failure).waitFor({ state: "visible" });115- if (await failure.isVisible()) {116- throw new Error((await failure.textContent()) ?? "Write failed");117- }118- } else {119- await item.waitFor({ state: "visible" });120- }121- }122-}123-124-async function waitForItemAbsent(125- page: Page,126- islandLabels: readonly string[],127- body: string,128-): Promise<void> {129- for (const label of islandLabels) {130- const island = page.locator("[data-island]").filter({ hasText: label });131- await island.getByRole("listitem").filter({ hasText: body }).waitFor({ state: "detached" });132- }133-}134-135-async function addItem(page: Page, island: string, body: string): Promise<void> {136- const section = page.locator("[data-island]").filter({ hasText: island });137- const textbox = section.getByRole("textbox", { name: checklistUi.newItem });138- try {139- await textbox.waitFor({ state: "visible" });140- } catch (error) {141- throw new Error(142- `reference UI is missing accessible textbox "${checklistUi.newItem}" in ${island}; #9 must expose CRUD controls`,143- { cause: error },144- );145- }146- const add = section.getByRole("button", { name: checklistUi.addFrom(island) });147- try {148- await add.waitFor({ state: "visible" });149- } catch (error) {150- throw new Error(151- `reference UI is missing accessible button "${152- checklistUi.addFrom(island)153- }"; #9 must expose CRUD controls`,154- { cause: error },155- );156- }157- await textbox.fill(body);158- await add.click();159-}160-161-async function waitForLocalDurability(page: Page): Promise<void> {162- await page.getByRole("status").filter({163- hasText: new RegExp(164- `(?:${escaped(checklistUi.lastWrite("local"))}|${escaped(checklistUi.lastWrite("global"))})`,165- ),166- }).first().waitFor({ state: "visible" });167-}168-169-async function assertRuntimeCardinality(170- page: Page,171- islandLabels: readonly string[],172-): Promise<void> {173- assert(islandLabels.length === 2, "golden path cardinality requires two mounted islands");174- await page.waitForFunction(async () => {175- const inspector = (globalThis as typeof globalThis & {176- __LOFI_INSPECTOR__?: {177- readSnapshot(): Promise<{178- storage: { driver: string };179- runtime: {180- clients: number;181- consumers: number;182- vendorSubscriptions: number;183- mutationListeners: number;184- };185- }>;186- };187- }).__LOFI_INSPECTOR__;188- const snapshot = await inspector?.readSnapshot();189- return snapshot?.storage.driver === "persistent open" &&190- snapshot.runtime.clients === 1 &&191- snapshot.runtime.consumers === 2 &&192- snapshot.runtime.vendorSubscriptions === 1 &&193- snapshot.runtime.mutationListeners === 1;194- });195-}196-197-async function restartClientThroughInspector(198- page: Page,199- islandLabels: readonly string[],200- retainedItem: string,201- timeoutMs: number,202-): Promise<void> {203- const identity = await armIdentityComparison(page);204- const before = await page.evaluate(() => ({205- timeOrigin: performance.timeOrigin,206- url: location.href,207- }));208- const navigated = page.waitForEvent("domcontentloaded", { timeout: timeoutMs });209- await page.getByRole("button", { name: "Restart client" }).click();210- await navigated;211- const after = await page.evaluate(() => ({212- timeOrigin: performance.timeOrigin,213- url: location.href,214- }));215- assert(after.timeOrigin !== before.timeOrigin, "inspector restart did not replace the document");216- assert(after.url === before.url, "inspector restart changed the application URL");217- assert(218- await identityComparisonMatches(page, identity.token),219- "inspector restart changed the device identity",220- );221- await waitForItem(page, islandLabels, retainedItem);222- for (const label of islandLabels) {223- const island = page.locator("[data-island]").filter({ hasText: label });224- await island.getByRole("status").filter({ hasText: "1 item(s)" }).waitFor({225- state: "visible",226- timeout: timeoutMs,227- });228- }229- await assertRuntimeCardinality(page, islandLabels);230-}231-232-async function armIdentityComparison(page: Page): Promise<{ token: string; present: boolean }> {233- const token = crypto.randomUUID();234- const present = await page.evaluate(async (comparisonToken) => {235- const identity = JSON.stringify(236- Object.entries(localStorage).sort(([left], [right]) => left.localeCompare(right)),237- );238- const digest = new Uint8Array(239- await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)),240- );241- const hex = Array.from(digest).map((byte) => byte.toString(16).padStart(2, "0")).join("");242- (globalThis as typeof globalThis & { name: string }).name =243- `lofi-golden:${comparisonToken}:${hex}`;244- return identity !== "[]";245- }, token);246- return { token, present };247-}248-249-async function identityComparisonMatches(page: Page, token: string): Promise<boolean> {250- return await page.evaluate(async (comparisonToken) => {251- const browserGlobal = globalThis as typeof globalThis & { name: string };252- const expected = browserGlobal.name;253- browserGlobal.name = "";254- const identity = JSON.stringify(255- Object.entries(localStorage).sort(([left], [right]) => left.localeCompare(right)),256- );257- const digest = new Uint8Array(258- await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)),259- );260- const hex = Array.from(digest).map((byte) => byte.toString(16).padStart(2, "0")).join("");261- return expected === `lofi-golden:${comparisonToken}:${hex}`;262- }, token);263-}264-265-async function exerciseHmr(266- page: Page,267- projectRoot: string,268- hmrPath: string,269- islandLabels: readonly string[],270- retainedItem: string,271- timeoutMs: number,272-): Promise<{ samples: number[]; restore: () => Promise<void> }> {273- const path = resolve(projectRoot, hmrPath);274- const original = await Deno.readTextFile(path);275- const samples: number[] = [];276- for (let cycle = 1; cycle <= 5; cycle++) {277- const started = performance.now();278- await Deno.writeTextFile(279- path,280- `${original}\n:root {\n --lofi-golden-hmr-cycle: ${cycle};\n}\n`,281- );282- await page.waitForFunction(283- (expected) =>284- getComputedStyle(document.documentElement).getPropertyValue("--lofi-golden-hmr-cycle")285- .trim() === expected,286- String(cycle),287- { timeout: timeoutMs },288- );289- samples.push(Math.round(performance.now() - started));290- await waitForItem(page, islandLabels, retainedItem);291- await assertRuntimeCardinality(page, islandLabels);292- }293- return {294- samples,295- restore: async () => await Deno.writeTextFile(path, original),296- };297-}298-299-async function setReferenceConnection(page: Page, connected: boolean): Promise<void> {300- await page.evaluate(async (shouldConnect) => {301- const inspector = (globalThis as typeof globalThis & {302- __LOFI_INSPECTOR__?: { setTransportPaused(paused: boolean): Promise<void> };303- }).__LOFI_INSPECTOR__;304- if (!inspector) throw new Error("development inspector is unavailable");305- await inspector.setTransportPaused(!shouldConnect);306- }, connected);307-}308-309-async function exerciseReplicaClear(310- browser: Browser,311- url: string,312- islandLabels: readonly string[],313- timeoutMs: number,314-): Promise<void> {315- const context = await browser.newContext();316- try {317- const page = await context.newPage();318- await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });319- await page.getByRole("textbox", { name: checklistUi.newItem }).first().waitFor({320- state: "visible",321- });322- await assertRuntimeCardinality(page, islandLabels);323- await addItem(page, islandLabels[0], replicaClearBody);324- await waitForItem(page, islandLabels, replicaClearBody);325- await waitForLocalDurability(page);326- const identity = await armIdentityComparison(page);327- assert(identity.present, "replica-clear fixture did not create an identity state");328- const navigated = page.waitForEvent("domcontentloaded", { timeout: timeoutMs });329- page.once("dialog", (dialog) => dialog.accept());330- await page.getByRole("button", { name: "Clear local replica" }).click();331- await navigated;332- await waitForItemAbsent(page, islandLabels, replicaClearBody);333- assert(334- await identityComparisonMatches(page, identity.token),335- "replica clear changed the device-local identity",336- );337- } finally {338- await context.close();339- }340-}341-342-async function inspectorMarkerFiles(root: string): Promise<string[]> {343- const matches: string[] = [];344- async function visit(path: string): Promise<void> {345- for await (const entry of Deno.readDir(path)) {346- const child = join(path, entry.name);347- if (entry.isDirectory) await visit(child);348- else if (entry.isFile) {349- const content = await Deno.readTextFile(child).catch(() => "");350- if (content.includes("lofi-development-inspector")) matches.push(child);351- }352- }353- }354- await visit(root);355- return matches.sort();356-}357-358-async function observeReconnectSettlement(359- page: Page,360- timeoutMs: number,361- required: boolean,362-): Promise<JourneyAssertion> {363- if (!required) {364- return {365- name: "development reconnect settlement",366- status: "blocked",367- detail:368- "Local-only mode has no global durability endpoint; offline retention passed, but this run makes no transport convergence claim.",369- };370- }371- try {372- const global = page.getByRole("status").filter({373- hasText: checklistUi.lastWrite("global"),374- }).first();375- const failure = page.getByRole("status").filter({ hasText: checklistUi.writeFailed }).first();376- await global.or(failure).waitFor({377- state: "visible",378- timeout: timeoutMs,379- });380- if (await failure.isVisible()) {381- throw new Error((await failure.textContent()) ?? "Write failed");382- }383- return {384- name: "development reconnect settlement",385- status: "passed",386- detail: "The configured server exposed global settlement after reconnection.",387- };388- } catch {389- throw new Error(390- "cloud evidence failed: reconnect did not expose global settlement before the readiness deadline",391- );392- }393-}394-395-async function journeyEnvironment(396- sourceRoot: string,397- mode: "isolated-local" | "cloud-from-root",398- artifactRoot: string,399-): Promise<Record<string, string>> {400- const isolated = safeChildEnvironment();401- if (mode === "isolated-local") return await installNodeSentinel(isolated, artifactRoot);402-403- const configured = await loadEnvironment(join(sourceRoot, ".env"));404- const validation = validateEnvironment(configured);405- if (!validation.ok) throw new Error(validation.errors.join(" "));406- if (validation.mode !== "cloud-configured") {407- throw new Error(408- "cloud evidence requires the complete JAZZ_APP_ID/JAZZ_SERVER_URL pair in the root .env or process environment",409- );410- }411- const allowlisted = { ...isolated };412- for (const name of clientEnvironmentNames) {413- allowlisted[name] = configured[name]?.trim() ?? "";414- }415- for (const name of serverEnvironmentNames) allowlisted[name] = "";416- return await installNodeSentinel(allowlisted, artifactRoot);417-}418-419-async function updateItem(page: Page, body: string, nextBody: string): Promise<void> {420- const edit = page.getByRole("button", { name: checklistUi.edit(body), exact: true }).first();421- try {422- await edit.click();423- } catch (error) {424- throw new Error(`reference UI is missing accessible button "${checklistUi.edit(body)}"`, {425- cause: error,426- });427- }428- const textbox = page.getByRole("textbox", { name: checklistUi.edit(body), exact: true }).first();429- try {430- await textbox.fill(nextBody);431- } catch (error) {432- throw new Error(`reference UI is missing accessible textbox "${checklistUi.edit(body)}"`, {433- cause: error,434- });435- }436- const save = page.getByRole("button", { name: checklistUi.save(body), exact: true }).first();437- try {438- await save.click();439- } catch (error) {440- throw new Error(`reference UI is missing accessible button "${checklistUi.save(body)}"`, {441- cause: error,442- });443- }444-}445-446-async function completeItem(page: Page, body: string): Promise<void> {447- const name = checklistUi.complete(body);448- const checkboxes = page.getByRole("checkbox", { name });449- try {450- await checkboxes.first().waitFor({ state: "visible" });451- } catch (error) {452- throw new Error(453- `reference UI needs checkbox "${checklistUi.complete(body)}"`,454- { cause: error },455- );456- }457- await checkboxes.first().click();458- await page.waitForFunction((accessibleName) => {459- const matches = [...document.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')]460- .filter((element) => element.getAttribute("aria-label") === accessibleName);461- return matches.length === 2 && matches.every((element) => element.checked);462- }, checklistUi.complete(body));463-}464-465-async function assertCompleted(page: Page, body: string): Promise<void> {466- const name = checklistUi.complete(body);467- const checkboxes = page.getByRole("checkbox", { name });468- await checkboxes.first().waitFor({ state: "visible" });469- await page.waitForFunction((accessibleName) => {470- const matches = [...document.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')]471- .filter((element) => element.getAttribute("aria-label") === accessibleName);472- return matches.length === 2 && matches.every((element) => element.checked);473- }, name);474- assert(await checkboxes.count() === 2, `expected two completion controls for ${body}`);475- assert(476- await checkboxes.evaluateAll((elements) =>477- elements.every((element) => (element as HTMLInputElement).checked)478- ),479- `completion for ${body} did not survive reload in both islands`,480- );481-}482-483-async function deleteItem(page: Page, body: string): Promise<void> {484- const button = page.getByRole("button", { name: checklistUi.delete(body), exact: true }).first();485- try {486- await button.click();487- } catch (error) {488- throw new Error(`reference UI is missing accessible button "${checklistUi.delete(body)}"`, {489- cause: error,490- });491- }492-}493-494-async function waitForProductionPwa(page: Page, timeoutMs: number): Promise<void> {495- await page.waitForFunction(496- () => {497- return (globalThis as typeof globalThis & { __LOFI_PWA_STATE__?: string })498- .__LOFI_PWA_STATE__ === "ready";499- },500- undefined,501- { timeout: timeoutMs },502- );503- await page.waitForFunction(() => navigator.serviceWorker.controller !== null, undefined, {504- timeout: timeoutMs,505- });506-}507-508-async function waitForChecklistReady(page: Page, timeoutMs: number): Promise<void> {509- await page.getByRole("textbox", { name: checklistUi.newItem }).first().waitFor({510- state: "visible",511- timeout: timeoutMs,512- });513- await page.getByRole("status").filter({ hasText: /\d+ item\(s\)/ }).first().waitFor({514- state: "visible",515- timeout: timeoutMs,516- });517-}518-519-async function valueFreeClientState(client: BrowserTestClient) {520- const renderedRows = await client.page.locator("[data-task-id]").count();521- const completedControls = await client.page.locator('input[type="checkbox"]:checked').count();522- return { offline: client.offline, renderedRows, completedControls };523-}524-525-async function exerciseTwoClientCloudConvergence(526- browser: Browser,527- url: string,528- islandLabels: readonly string[],529- timeoutMs: number,530- artifactRoot: string,531- redactValues: readonly string[],532-): Promise<void> {533- const fixture = await createTwoClientFixture({534- baseURL: url,535- browser,536- identity: {537- mode: "shared",538- async preparePrimary(client) {539- await waitForProductionPwa(client.page, timeoutMs);540- await waitForChecklistReady(client.page, timeoutMs);541- },542- },543- artifacts: {544- directory: join(artifactRoot, "two-client-convergence"),545- secretValues: redactValues,546- },547- });548- try {549- await Promise.all(fixture.clients.map(async (client) => {550- await waitForProductionPwa(client.page, timeoutMs);551- await waitForChecklistReady(client.page, timeoutMs);552- }));553- await addItem(fixture.first.page, islandLabels[0], convergenceTextSeedBody);554- await waitForItem(fixture.first.page, islandLabels, convergenceTextSeedBody);555- await addItem(fixture.first.page, islandLabels[0], convergenceCompletionBody);556- await waitForItem(fixture.first.page, islandLabels, convergenceCompletionBody);557- await observeReconnectSettlement(fixture.first.page, timeoutMs, true);558- await waitForItem(fixture.second.page, islandLabels, convergenceTextSeedBody);559- await waitForItem(fixture.second.page, islandLabels, convergenceCompletionBody);560-561- type ConvergenceEdit = { kind: "text" } | { kind: "completion" };562- await runConcurrentOfflineConvergence<ConvergenceEdit, BrowserTestClient>(fixture, {563- edits: [{ kind: "text" }, { kind: "completion" }],564- async ready(client) {565- await waitForProductionPwa(client.page, timeoutMs);566- await waitForItem(client.page, islandLabels, convergenceTextSeedBody);567- await waitForItem(client.page, islandLabels, convergenceCompletionBody);568- },569- async apply(client, edit) {570- if (edit.kind === "text") {571- await updateItem(client.page, convergenceTextSeedBody, convergenceEditedBody);572- } else {573- await completeItem(client.page, convergenceCompletionBody);574- }575- },576- async locallyApplied(client, edit) {577- if (edit.kind === "text") {578- await waitForItem(client.page, islandLabels, convergenceEditedBody);579- } else {580- await assertCompleted(client.page, convergenceCompletionBody);581- }582- await waitForLocalDurability(client.page);583- },584- async whilePending(current) {585- await current.first.reloadPage();586- await waitForProductionPwa(current.first.page, timeoutMs);587- await waitForItem(current.first.page, islandLabels, convergenceEditedBody);588-589- await current.second.restartPage();590- await waitForProductionPwa(current.second.page, timeoutMs);591- await waitForItem(current.second.page, islandLabels, convergenceCompletionBody);592- await assertCompleted(current.second.page, convergenceCompletionBody);593- },594- async converged(current) {595- for (const client of current.clients) {596- await waitForItem(client.page, islandLabels, convergenceEditedBody);597- await assertCompleted(client.page, convergenceCompletionBody);598- }599- },600- snapshot: valueFreeClientState,601- failureLabel: "cloud-two-client-convergence",602- });603-604- for (const body of [convergenceEditedBody, convergenceCompletionBody]) {605- await deleteItem(fixture.first.page, body);606- await waitForItemAbsent(fixture.first.page, islandLabels, body);607- await observeReconnectSettlement(fixture.first.page, timeoutMs, true);608- await waitForItemAbsent(fixture.second.page, islandLabels, body);609- }610- } catch (error) {611- await fixture.captureFailure("cloud-two-client-cleanup", valueFreeClientState).catch(() => {612- // The original convergence or cleanup failure remains authoritative.613- });614- throw error;615- } finally {616- await fixture.close();617- }618-}619-620-async function screenshotFailure(page: Page | null, path: string): Promise<void> {621- if (!page || page.isClosed()) return;622- try {623- await page.screenshot({ path, fullPage: true });624- } catch {625- // Process logs and the trace remain authoritative when the page itself is unavailable.626- }627-}628-629-function byteSequencePresent(haystack: Uint8Array, needle: Uint8Array): boolean {630- if (needle.length === 0 || haystack.length < needle.length) return false;631- outer:632- for (let offset = 0; offset <= haystack.length - needle.length; offset++) {633- for (let index = 0; index < needle.length; index++) {634- if (haystack[offset + index] !== needle[index]) continue outer;635- }636- return true;637- }638- return false;639-}640-641-async function sanitizeRetainedTrace(642- path: string,643- configuredValues: readonly string[],644-): Promise<void> {645- await sanitizeTraceArchive(646- path,647- (value) => redactDiagnosticText(value, configuredValues),648- );649- const entries = await readTraceArchive(path);650- assert(651- Object.keys(entries).every((name) => !name.startsWith("resources/")),652- "sanitized trace retained a response/source resource",653- );654- const encoder = new TextEncoder();655- for (const configured of configuredValues.filter(Boolean)) {656- const needle = encoder.encode(configured);657- assert(658- Object.values(entries).every((bytes) => !byteSequencePresent(bytes, needle)),659- "sanitized trace retained a configured environment value",660- );661- }662- const traceText = Object.entries(entries)663- .filter(([name]) => name.endsWith(".trace"))664- .map(([, bytes]) => new TextDecoder().decode(bytes))665- .join("\n");666- assert(667- !/"identity"\s*:\s*"[^"\n]+"/.test(traceText),668- "sanitized trace retained an identity-valued Playwright result",669- );670-}671-672-function commandFailure(record: CommandRecord): Error {673- return new Error(674- `${record.name} failed with exit ${record.exitCode}; rerun ${Deno.execPath()} ${675- record.args.join(" ")676- } and inspect ${record.stderrPath}`,677- );678-}679-680-async function sourceRevision(681- sourceRoot: string,682- environment: Record<string, string>,683-): Promise<string> {684- const output = await new Deno.Command("git", {685- args: ["rev-parse", "HEAD"],686- cwd: sourceRoot,687- clearEnv: true,688- env: environment,689- stdout: "piped",690- stderr: "null",691- }).output();692- return output.success ? new TextDecoder().decode(output.stdout).trim() : "unavailable";693-}694-695-export async function runJourney(options: GoldenPathOptions): Promise<JourneyReport> {696- const sourceRoot = resolve(options.projectRoot ?? Deno.cwd());697- const revisionRoot = resolve(options.sourceRevisionRoot ?? sourceRoot);698- const environmentMode = options.environmentMode ?? "isolated-local";699- const artifacts = artifactPaths(700- resolve(701- options.artifactRoot ??702- join(703- sourceRoot,704- environmentMode === "cloud-from-root"705- ? "test-results/golden-path-cloud"706- : "test-results/golden-path",707- ),708- ),709- );710- if (options.resetArtifactRoot !== false) {711- await Deno.remove(artifacts.root, { recursive: true }).catch((error) => {712- if (!(error instanceof Deno.errors.NotFound)) throw error;713- });714- }715- await Deno.mkdir(artifacts.root, { recursive: true });716-717- const startedAt = options.journeyStartedAt ?? new Date();718- const timeoutMs = options.timeoutMs ?? 60_000;719- const commands = { ...defaultCommands, ...options.commands };720- const islandLabels = options.islandLabels ?? ["North island", "South island"];721- const environment = await journeyEnvironment(sourceRoot, environmentMode, artifacts.root);722- const configuredEnvironment = await loadEnvironment(join(sourceRoot, ".env"));723- const redactValues = [724- ...new Set(725- environmentNames.flatMap((name) => [configuredEnvironment[name], environment[name]]).filter(726- Boolean,727- ),728- ),729- ];730- const commit = await sourceRevision(revisionRoot, environment);731- const commandRecords: CommandRecord[] = [...options.initialCommands ?? []];732- const assertions: JourneyAssertion[] = [];733- let projectRoot = sourceRoot;734- let browser: Browser | null = null;735- let context: BrowserContext | null = null;736- let page: Page | null = null;737- let dev: CapturedProcess | null = null;738- let preview: CapturedProcess | null = null;739- let browserVersion = "not launched";740- let devReadyMs: number | undefined;741- let firstRetainedWriteMs: number | undefined;742- let createToFirstRetainedWriteMs: number | undefined;743- let hmrMs: number[] | undefined;744- let restoreHmr: (() => Promise<void>) | null = null;745- let failure: unknown;746-747- const report: JourneyReport = {748- schemaVersion: 1,749- source: options.source,750- environmentMode: environmentMode === "cloud-from-root" ? "cloud-allowlisted" : "isolated-local",751- status: "failed",752- startedAt: startedAt.toISOString(),753- completedAt: startedAt.toISOString(),754- runtime: {755- deno: Deno.version.deno,756- typescript: Deno.version.typescript,757- v8: Deno.version.v8,758- os: Deno.build.os,759- arch: Deno.build.arch,760- browser: browserVersion,761- commit,762- },763- cacheMode: options.cacheMode ?? "existing",764- measurements: {765- developerCommandCount: options.developerCommandCount ?? (options.source === "create" ? 3 : 1),766- createMs: options.createDurationMs,767- },768- commands: commandRecords,769- assertions,770- authorBoundaryViolations: [],771- artifacts,772- };773-774- try {775- if (options.source === "checkout") {776- projectRoot = await materializeCleanProject(sourceRoot);777- commandRecords.push(778- ...await initializeFixtureGit(projectRoot, environment, artifacts.root, redactValues),779- );780- }781-782- report.authorBoundaryViolations = await scanAuthorBoundary(783- projectRoot,784- options.authorPaths ?? defaultAuthorPaths,785- );786- assert(787- report.authorBoundaryViolations.length === 0,788- `author boundary leaked framework plumbing: ${789- report.authorBoundaryViolations.map((item) => `${item.path}:${item.line} ${item.rule}`)790- .join(791- ", ",792- )793- }`,794- );795- assertions.push({796- name: "author boundary",797- status: "passed",798- detail: "No forbidden runtime, worker, transport, Workbox, or capability plumbing found.",799- });800- if (environmentMode === "cloud-from-root") {801- assert(802- clientEnvironmentNames.every((name) => Boolean(environment[name])) &&803- serverEnvironmentNames.every((name) => environment[name] === ""),804- "cloud child environment did not isolate server credentials",805- );806- assertions.push({807- name: "cloud credential isolation",808- status: "passed",809- detail:810- "The application child received only the public Jazz pair; server credentials were explicitly blank.",811- });812- }813-814- if (commands.doctor) {815- const doctor = await runCapturedCommand({816- cwd: projectRoot,817- args: taskArgs(commands.doctor),818- environment,819- artifactRoot: artifacts.root,820- name: "doctor",821- redactValues,822- });823- commandRecords.push(doctor);824- if (doctor.exitCode !== 0) throw commandFailure(doctor);825- assertions.push({826- name: "doctor preflight",827- status: "passed",828- detail: "Generated diagnostics completed without starting the application.",829- });830- }831-832- try {833- browser = await chromium.launch({ headless: true });834- } catch (error) {835- throw new Error(836- "Chromium is unavailable; install the pinned browser through the repository's explicit Deno browser-install task.",837- { cause: error },838- );839- }840- browserVersion = browser.version();841- report.runtime.browser = browserVersion;842- context = await browser.newContext();843- await context.tracing.start({ screenshots: false, snapshots: false, sources: false });844- page = await context.newPage();845-846- const port = reserveLocalPort();847- const devStarted = performance.now();848- dev = startReadyProcess({849- cwd: projectRoot,850- args: taskArgs(commands.dev, ["--host", "127.0.0.1", "--port", String(port)]),851- environment,852- artifactRoot: artifacts.root,853- name: "dev",854- readyPattern: /(http:\/\/(?:127\.0\.0\.1|localhost):\d+\/?)/,855- readyCheck: probeHttpReady,856- timeoutMs,857- redactValues,858- });859- const devUrl = await dev.ready;860- devReadyMs = Math.round(performance.now() - devStarted);861- await page.goto(devUrl, { waitUntil: "domcontentloaded", timeout: timeoutMs });862- await page.getByRole("textbox", { name: checklistUi.newItem }).first().waitFor({863- state: "visible",864- });865- await assertRuntimeCardinality(page, islandLabels);866- await page.getByRole("status").filter({ hasText: /\d+ item\(s\)/ }).first().waitFor({867- state: "visible",868- });869- const inspector = page.locator('[data-lofi-inspector="lofi-development-inspector"]');870- await inspector.waitFor({ state: "attached" });871- const inspectorSnapshot = await page.evaluate(async () => {872- const bridge = (globalThis as typeof globalThis & {873- __LOFI_INSPECTOR__?: { readSnapshot(): Promise<unknown> };874- }).__LOFI_INSPECTOR__;875- if (!bridge) throw new Error("development inspector bridge is unavailable");876- return await bridge.readSnapshot();877- });878- const serializedInspector = JSON.stringify(inspectorSnapshot);879- assert(880- serializedInspector.includes("live detail unavailable") ||881- serializedInspector.includes("not configured"),882- "inspector must name unavailable transport precision",883- );884- assert(885- !/secret|token|password/i.test(serializedInspector),886- "inspector snapshot exposed secrets",887- );888- if (environmentMode === "isolated-local") {889- assert(890- await page.getByRole("button", { name: "Transport pause unavailable in local-only" })891- .isDisabled(),892- "local-only inspector must disable cloud transport controls",893- );894- }895- assertions.push({896- name: "development inspector",897- status: "passed",898- detail:899- "The development-only surface exposed value-free storage, durability, runtime, and explicitly unavailable vendor signals.",900- });901- devReadyMs = Math.round(performance.now() - devStarted);902- assertions.push({903- name: "development readiness",904- status: "passed",905- detail: `Hydrated author UI and the persistent runtime were usable in ${devReadyMs}ms.`,906- });907-908- await addItem(page, islandLabels[0], retainedBody);909- await waitForItem(page, islandLabels, retainedBody);910- await waitForLocalDurability(page);911- firstRetainedWriteMs = Math.round(performance.now() - devStarted);912- if (options.journeyStartedPerformanceMs !== undefined) {913- createToFirstRetainedWriteMs = Math.round(914- performance.now() - options.journeyStartedPerformanceMs,915- );916- }917- await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });918- await waitForItem(page, islandLabels, retainedBody);919- await assertRuntimeCardinality(page, islandLabels);920- assertions.push({921- name: "retained development write",922- status: "passed",923- detail:924- "The local write survived reload and both islands kept one shared runtime subscription.",925- });926-927- await restartClientThroughInspector(page, islandLabels, retainedBody, timeoutMs);928- assertions.push({929- name: "inspector client restart",930- status: "passed",931- detail: "The development-only client restart retained data and restored runtime cardinality.",932- });933-934- const hmr = await exerciseHmr(935- page,936- projectRoot,937- options.hmrPath ??938- (options.source === "create"939- ? "src/styles/global.css"940- : "apps/reference/src/styles/global.css"),941- islandLabels,942- retainedBody,943- timeoutMs,944- );945- hmrMs = hmr.samples;946- restoreHmr = hmr.restore;947- assertions.push({948- name: "HMR state and cardinality",949- status: "passed",950- detail:951- `Five observable style edits retained data with one client, two consumers, and one vendor subscription (${952- hmrMs.join(", ")953- }ms).`,954- });955-956- await updateItem(page, retainedBody, updatedBody);957- await waitForItem(page, islandLabels, updatedBody);958- await completeItem(page, updatedBody);959- await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });960- await waitForItem(page, islandLabels, updatedBody);961- await assertCompleted(page, updatedBody);962-963- await addItem(page, islandLabels[0], disposableBody);964- await waitForItem(page, islandLabels, disposableBody);965- if (environmentMode === "cloud-from-root") {966- await observeReconnectSettlement(page, timeoutMs, true);967- }968- await deleteItem(page, disposableBody);969- await waitForItemAbsent(page, islandLabels, disposableBody);970- if (environmentMode === "cloud-from-root") {971- const online = await observeReconnectSettlement(page, timeoutMs, true);972- assertions.push({973- ...online,974- name: "development online settlement",975- detail:976- "The online insert and delete reached global durability before the inspector paused transport.",977- });978- }979- assertions.push({980- name: "accessible CRUD",981- status: "passed",982- detail: "Create, update, complete, and delete worked through named author-facing controls.",983- });984-985- if (environmentMode === "cloud-from-root") {986- await setReferenceConnection(page, false);987- } else {988- await context.setOffline(true);989- }990- await addItem(page, islandLabels[1], offlineDevelopmentBody);991- await waitForItem(page, islandLabels, offlineDevelopmentBody);992- await waitForLocalDurability(page);993- if (environmentMode === "cloud-from-root") await setReferenceConnection(page, true);994- else await context.setOffline(false);995- assertions.push(996- await observeReconnectSettlement(page, timeoutMs, environmentMode === "cloud-from-root"),997- );998- await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });999- await waitForItem(page, islandLabels, offlineDevelopmentBody);1000- assertions.push({1001- name: "development offline write",1002- status: "passed",1003- detail: environmentMode === "cloud-from-root"1004- ? "A loaded page accepted a local write while Jazz transport was paused and retained it after transport resumed."1005- : "A loaded page accepted a local write offline and retained it after network return.",1006- });1007-1008- if (environmentMode === "isolated-local") {1009- await exerciseReplicaClear(browser, devUrl, islandLabels, timeoutMs);1010- assertions.push({1011- name: "inspector replica clear",1012- status: "passed",1013- detail:1014- "A confirmation-gated clear removed an isolated OPFS replica while preserving its in-memory-compared identity state.",1015- });1016- }1017-1018- const devStderrPath = dev.stderrPath;1019- await dev.stop();1020- dev = null;1021- const devStderr = await Deno.readTextFile(devStderrPath);1022- assert(1023- !devStderr.includes("error: development server stopped"),1024- `normal development shutdown printed a fatal diagnostic; inspect ${devStderrPath}`,1025- );1026- assertions.push({1027- name: "development shutdown",1028- status: "passed",1029- detail:1030- "The generated development command stopped on the harness signal without a false failure.",1031- });1032- if (restoreHmr) await restoreHmr();1033- restoreHmr = null;1034- for (1035- const [name, task] of [1036- ["check", commands.check],1037- ["test", commands.test],1038- ["build", commands.build],1039- ] as const1040- ) {1041- const record = await runCapturedCommand({1042- cwd: projectRoot,1043- args: taskArgs(task),1044- environment,1045- artifactRoot: artifacts.root,1046- name,1047- redactValues,1048- });1049- commandRecords.push(record);1050- if (record.exitCode !== 0) throw commandFailure(record);1051- }1052- assertions.push({1053- name: "public verification commands",1054- status: "passed",1055- detail: "check, test, and build completed through Deno tasks.",1056- });1057- const productionRoot = join(1058- projectRoot,1059- options.source === "create" ? "dist" : "apps/reference/dist",1060- );1061- const inspectorFiles = await inspectorMarkerFiles(productionRoot);1062- assert(1063- inspectorFiles.length === 0,1064- `production build contains development inspector markers: ${inspectorFiles.join(", ")}`,1065- );1066- assertions.push({1067- name: "production inspector exclusion",1068- status: "passed",1069- detail: "The production bundle contains no development-inspector marker.",1070- });1071-1072- preview = startReadyProcess({1073- cwd: projectRoot,1074- args: taskArgs(commands.preview, ["--port", String(port)]),1075- environment,1076- artifactRoot: artifacts.root,1077- name: "preview",1078- readyPattern: /(http:\/\/127\.0\.0\.1:\d+\/)/,1079- readyCheck: probeHttpReady,1080- timeoutMs,1081- redactValues,1082- });1083- const previewUrl = await preview.ready;1084- assert(new URL(previewUrl).origin === new URL(devUrl).origin, "preview must reuse dev origin");1085- await page.goto(previewUrl, { waitUntil: "domcontentloaded", timeout: timeoutMs });1086- assert(1087- await page.locator('[data-lofi-inspector="lofi-development-inspector"]').count() === 0,1088- "production preview mounted the development inspector",1089- );1090- await waitForItem(page, islandLabels, updatedBody);1091- await waitForProductionPwa(page, timeoutMs);1092- await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });1093- await waitForProductionPwa(page, timeoutMs);1094-1095- await context.setOffline(true);1096- await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });1097- await waitForItem(page, islandLabels, updatedBody);1098- await addItem(page, islandLabels[0], offlineProductionBody);1099- await waitForItem(page, islandLabels, offlineProductionBody);1100- await waitForLocalDurability(page);1101- await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });1102- await waitForItem(page, islandLabels, offlineProductionBody);1103- await context.setOffline(false);1104- if (environmentMode === "cloud-from-root") {1105- await updateItem(page, offlineProductionBody, offlineProductionSyncedBody);1106- await waitForItem(page, islandLabels, offlineProductionSyncedBody);1107- assertions.push(await observeReconnectSettlement(page, timeoutMs, true));1108- }1109- await page.reload({ waitUntil: "domcontentloaded", timeout: timeoutMs });1110- await waitForItem(1111- page,1112- islandLabels,1113- environmentMode === "cloud-from-root" ? offlineProductionSyncedBody : offlineProductionBody,1114- );1115- assertions.push({1116- name: "production offline cold start",1117- status: "passed",1118- detail: "The service-worker shell and OPFS data survived cold offline reload and editing.",1119- });1120-1121- if (environmentMode === "cloud-from-root") {1122- await exerciseTwoClientCloudConvergence(1123- browser,1124- previewUrl,1125- islandLabels,1126- timeoutMs,1127- artifacts.root,1128- redactValues,1129- );1130- assertions.push({1131- name: "cloud two-client convergence",1132- status: "passed",1133- detail:1134- "Two isolated OPFS replicas shared one in-memory-restored identity, retained orthogonal offline edits through reload/page restart, converged after reconnect, and cleaned up globally.",1135- });1136- }1137-1138- if (environmentMode === "cloud-from-root") {1139- for (const body of [updatedBody, offlineDevelopmentBody, offlineProductionSyncedBody]) {1140- await deleteItem(page, body);1141- await waitForItemAbsent(page, islandLabels, body);1142- assertions.push(await observeReconnectSettlement(page, timeoutMs, true));1143- }1144- assertions.push({1145- name: "cloud fixture cleanup",1146- status: "passed",1147- detail:1148- "All golden-path rows were deleted through the public UI and the delete mutations reached global durability before the throwaway browser identity closed.",1149- });1150- }1151-1152- report.status = "passed";1153- } catch (error) {1154- failure = error;1155- assertions.push({1156- name: "golden journey",1157- status: "failed",1158- detail: error instanceof Error ? error.message : String(error),1159- });1160- await screenshotFailure(page, artifacts.screenshot);1161- } finally {1162- if (context) {1163- await context.setOffline(false).catch(() => undefined);1164- try {1165- await context.tracing.stop({ path: artifacts.trace });1166- await sanitizeRetainedTrace(artifacts.trace, redactValues);1167- assertions.push({1168- name: "trace artifact safety",1169- status: "passed",1170- detail:1171- "The snapshot-free retained trace omitted response/source resources, identity-valued results, and configured environment values.",1172- });1173- } catch (error) {1174- await Deno.remove(artifacts.trace).catch(() => undefined);1175- const traceFailure = new Error(1176- `retained trace could not be made value-free: ${1177- error instanceof Error ? error.message : String(error)1178- }`,1179- { cause: error },1180- );1181- if (!failure) failure = traceFailure;1182- report.status = "failed";1183- assertions.push({1184- name: "trace artifact safety",1185- status: "failed",1186- detail: traceFailure.message,1187- });1188- }1189- }1190- await preview?.stop().catch(() => undefined);1191- await dev?.stop().catch(() => undefined);1192- await restoreHmr?.().catch(() => undefined);1193- await context?.close().catch(() => undefined);1194- await browser?.close().catch(() => undefined);1195- if (options.source === "checkout" && projectRoot !== sourceRoot) {1196- await Deno.remove(resolve(projectRoot, ".."), { recursive: true }).catch(() => {1197- assertions.push({1198- name: "clean checkout cleanup",1199- status: "blocked",1200- detail: `Temporary checkout cleanup failed for ${projectRoot}.`,1201- });1202- });1203- }1204- report.completedAt = new Date().toISOString();1205- report.measurements.devReadyMs = devReadyMs;1206- report.measurements.firstRetainedWriteMs = firstRetainedWriteMs;1207- report.measurements.createToFirstRetainedWriteMs = createToFirstRetainedWriteMs;1208- report.measurements.hmrMs = hmrMs;1209- if (hmrMs?.length) {1210- const sorted = [...hmrMs].sort((left, right) => left - right);1211- report.measurements.hmrMedianMs = sorted[Math.floor(sorted.length / 2)];1212- report.measurements.hmrSlowestMs = sorted.at(-1);1213- }1214- report.runtime.browser = browserVersion;1215- await writeJourneyReport(report);1216- }1217-1218- if (failure) throw failure;1219- return report;1220-}@@ -1,18 +0,0 @@
1-import { runJourney } from "./golden_path.ts";2-3-try {4- const cloud = Deno.args.includes("--cloud");5- const report = await runJourney({6- source: "checkout",7- environmentMode: cloud ? "cloud-from-root" : "isolated-local",8- });9- console.log(`lofi golden path: passed (${report.artifacts.report})`);10-} catch (error) {11- console.error(`lofi golden path: failed: ${error instanceof Error ? error.message : error}`);12- console.error(13- Deno.args.includes("--cloud")14- ? "rerun: deno task test:golden:checkout -- --cloud"15- : "rerun: deno task test:golden:checkout",16- );17- Deno.exit(1);18-}@@ -1,707 +0,0 @@
1-import { basename, dirname, join, relative, resolve } from "node:path";2-import { environmentNames } from "./env_contract.ts";3-4-export type JourneySource = "checkout" | "create";5-6-export type CommandRecord = {7- name: string;8- args: string[];9- durationMs: number;10- exitCode: number;11- stdoutPath: string;12- stderrPath: string;13-};14-15-export type JourneyAssertion = {16- name: string;17- status: "passed" | "failed" | "blocked";18- detail: string;19-};20-21-export type AuthorBoundaryViolation = {22- rule: string;23- path: string;24- line: number;25- excerpt: string;26-};27-28-export type JourneyReport = {29- schemaVersion: 1;30- source: JourneySource;31- environmentMode: "isolated-local" | "cloud-allowlisted";32- status: "passed" | "failed";33- startedAt: string;34- completedAt: string;35- runtime: {36- deno: string;37- typescript: string;38- v8: string;39- os: string;40- arch: string;41- browser: string;42- commit: string;43- };44- cacheMode: "existing" | "cold";45- measurements: {46- developerCommandCount: number;47- createMs?: number;48- createToFirstRetainedWriteMs?: number;49- devReadyMs?: number;50- firstRetainedWriteMs?: number;51- hmrMs?: number[];52- hmrMedianMs?: number;53- hmrSlowestMs?: number;54- };55- commands: CommandRecord[];56- assertions: JourneyAssertion[];57- authorBoundaryViolations: AuthorBoundaryViolation[];58- artifacts: {59- root: string;60- report: string;61- trace: string;62- screenshot: string;63- };64-};65-66-export type BoundaryRule = {67- id: string;68- pattern: RegExp;69- allow?: (path: string) => boolean;70-};71-72-export const defaultBoundaryRules: readonly BoundaryRule[] = [73- {74- id: "raw-jazz-runtime-import",75- pattern: /(?:from\s+|import\s*\()["']jazz-tools(?:\/[^"']*)?["']/,76- // The M1 contract still explicitly permits the pinned Jazz schema DSL in schema.ts.77- allow: (path) => basename(path) === "schema.ts" || basename(path) === "permissions.ts",78- },79- {80- id: "raw-jazz-client-api",81- pattern: /\b(?:BrowserAuthSecretStore|createDb|JazzProvider|useJazzContext)\b/,82- },83- {84- id: "worker-plumbing",85- pattern: /\b(?:SharedWorker|navigator\.serviceWorker|new\s+Worker)\b/,86- },87- {88- id: "transport-plumbing",89- pattern: /\b(?:JAZZ_SERVER_URL|VITE_JAZZ_[A-Z0-9_]*|transportUrl)\b/,90- },91- { id: "workbox-plumbing", pattern: /\bworkbox\b/i },92- {93- id: "browser-capability-branch",94- pattern: /\b(?:navigator\.storage|navigator\.locks|isSecureContext)\b/,95- },96-];97-98-const ignoredDirectoryNames = new Set([99- ".astro",100- ".git",101- ".playwright-cli",102- ".vite",103- "coverage",104- "dist",105- "node_modules",106- "output",107- "playwright-report",108- "test-results",109-]);110-111-const childEnvironmentAllowlist = [112- "CI",113- "DENO_DIR",114- "GITHUB_ACTIONS",115- "LANG",116- "LC_ALL",117- "NIX_SSL_CERT_FILE",118- "NO_PROXY",119- "PATH",120- "PLAYWRIGHT_BROWSERS_PATH",121- "RUNNER_ARCH",122- "RUNNER_OS",123- "SSL_CERT_FILE",124- "TEMP",125- "TMP",126- "TMPDIR",127-] as const;128-129-function assertWithin(root: string, path: string) {130- const child = relative(resolve(root), resolve(path));131- if (child === "" || (!child.startsWith("..") && !child.startsWith("/"))) return;132- throw new Error(`path escapes project root: ${path}`);133-}134-135-function shouldExclude(name: string, isDirectory: boolean, relativePath: string): boolean {136- if (isDirectory && (ignoredDirectoryNames.has(name) || name.startsWith(".nzip-"))) return true;137- // The M2 reference application supersedes this ignored M1 working artifact. A working-tree copy138- // must not accidentally resurrect it just because another agent is moving tracked paths.139- if (isDirectory && relativePath === join("apps", "prototype")) return true;140- if (name === ".env.example") return false;141- if (name === ".env" || name.startsWith(".env.")) return true;142- return name.endsWith(".sqlite") || name.includes(".sqlite-");143-}144-145-async function copyDirectory(146- sourceRoot: string,147- source: string,148- destination: string,149-): Promise<void> {150- await Deno.mkdir(destination, { recursive: true });151- const entries: Deno.DirEntry[] = [];152- for await (const entry of Deno.readDir(source)) entries.push(entry);153- entries.sort((left, right) => left.name.localeCompare(right.name));154-155- for (const entry of entries) {156- const from = join(source, entry.name);157- const to = join(destination, entry.name);158- const displayPath = relative(sourceRoot, from);159- if (shouldExclude(entry.name, entry.isDirectory, displayPath)) continue;160- if (entry.isDirectory) {161- await copyDirectory(sourceRoot, from, to);162- } else if (entry.isFile) {163- await Deno.copyFile(from, to);164- } else if (entry.isSymlink) {165- throw new Error(`clean checkout refuses symlink: ${relative(source, from)}`);166- }167- }168-}169-170-async function trackedFiles(source: string): Promise<string[] | null> {171- const rootOutput = await new Deno.Command("git", {172- args: ["-C", source, "rev-parse", "--show-toplevel"],173- stdout: "piped",174- stderr: "null",175- }).output();176- if (!rootOutput.success) return null;177- const repositoryRoot = new TextDecoder().decode(rootOutput.stdout).trim();178- if (resolve(repositoryRoot) !== resolve(source)) return null;179-180- const filesOutput = await new Deno.Command("git", {181- args: ["-C", source, "ls-files", "-z"],182- stdout: "piped",183- stderr: "null",184- }).output();185- if (!filesOutput.success) return null;186- return new TextDecoder().decode(filesOutput.stdout).split("\0").filter(Boolean).sort();187-}188-189-async function copyTrackedFiles(190- source: string,191- destination: string,192- files: readonly string[],193-): Promise<void> {194- await Deno.mkdir(destination, { recursive: true });195- for (const displayPath of files) {196- const segments = displayPath.split(/[\\/]/);197- if (198- segments.some((name, index) =>199- shouldExclude(name, index < segments.length - 1, segments.slice(0, index + 1).join("/"))200- )201- ) continue;202- const from = join(source, displayPath);203- const to = join(destination, displayPath);204- const stat = await Deno.lstat(from);205- if (stat.isSymlink) throw new Error(`clean checkout refuses symlink: ${displayPath}`);206- if (!stat.isFile) continue;207- await Deno.mkdir(dirname(to), { recursive: true });208- await Deno.copyFile(from, to);209- }210-}211-212-export async function materializeCleanProject(213- sourceRoot: string,214- destinationRoot?: string,215-): Promise<string> {216- const source = resolve(sourceRoot);217- const destination = destinationRoot218- ? resolve(destinationRoot)219- : join(await Deno.makeTempDir({ prefix: "lofi-golden-" }), "project");220- if (source === destination) throw new Error("clean project destination must differ from source");221- const files = await trackedFiles(source);222- if (files) await copyTrackedFiles(source, destination, files);223- else await copyDirectory(source, source, destination);224- return destination;225-}226-227-export function safeChildEnvironment(228- source: Record<string, string> = Deno.env.toObject(),229-): Record<string, string> {230- const environment: Record<string, string> = {};231- for (const name of childEnvironmentAllowlist) {232- const value = source[name];233- if (value) environment[name] = value;234- }235- environment.CI = "1";236- environment.NO_COLOR = "1";237- // Explicit empties prevent a parent shell's sync credentials from influencing local mode.238- for (const name of environmentNames) environment[name] = "";239- return environment;240-}241-242-export async function installNodeSentinel(243- environment: Record<string, string>,244- artifactRoot: string,245-): Promise<Record<string, string>> {246- if (Deno.build.os === "windows") return { ...environment };247- const directory = join(artifactRoot, "node-sentinel");248- const executable = join(directory, "node");249- await Deno.mkdir(directory, { recursive: true });250- await Deno.writeTextFile(251- executable,252- '#!/bin/sh\necho "error: hidden Node invocation is forbidden by the lofi golden path" >&2\nexit 97\n',253- );254- await Deno.chmod(executable, 0o755);255- return {256- ...environment,257- PATH: environment.PATH ? `${directory}:${environment.PATH}` : directory,258- };259-}260-261-async function sourceFiles(path: string): Promise<string[]> {262- const stat = await Deno.stat(path);263- if (stat.isFile) return [path];264- const files: string[] = [];265- const entries: Deno.DirEntry[] = [];266- for await (const entry of Deno.readDir(path)) entries.push(entry);267- entries.sort((left, right) => left.name.localeCompare(right.name));268- for (const entry of entries) {269- const child = join(path, entry.name);270- if (entry.isDirectory) files.push(...await sourceFiles(child));271- else if (entry.isFile && /\.(?:astro|css|js|jsx|ts|tsx)$/.test(entry.name)) files.push(child);272- }273- return files;274-}275-276-export async function scanAuthorBoundary(277- projectRoot: string,278- authorPaths: readonly string[],279- rules: readonly BoundaryRule[] = defaultBoundaryRules,280-): Promise<AuthorBoundaryViolation[]> {281- const root = resolve(projectRoot);282- const files: string[] = [];283- for (const authorPath of authorPaths) {284- const path = resolve(root, authorPath);285- assertWithin(root, path);286- try {287- files.push(...await sourceFiles(path));288- } catch (error) {289- if (!(error instanceof Deno.errors.NotFound)) throw error;290- }291- }292-293- const violations: AuthorBoundaryViolation[] = [];294- for (const file of [...new Set(files)].sort()) {295- const displayPath = relative(root, file);296- const lines = (await Deno.readTextFile(file)).split("\n");297- for (let index = 0; index < lines.length; index += 1) {298- for (const rule of rules) {299- rule.pattern.lastIndex = 0;300- if (rule.allow?.(displayPath) || !rule.pattern.test(lines[index])) continue;301- violations.push({302- rule: rule.id,303- path: displayPath,304- line: index + 1,305- excerpt: lines[index].trim().slice(0, 160),306- });307- }308- }309- }310- return violations;311-}312-313-export async function withDeadline<T>(314- operation: Promise<T>,315- timeoutMs: number,316- description: string,317-): Promise<T> {318- let timeout: ReturnType<typeof setTimeout> | undefined;319- const deadline = new Promise<never>((_, reject) => {320- timeout = setTimeout(321- () => reject(new Error(`${description} did not become ready within ${timeoutMs}ms`)),322- timeoutMs,323- );324- });325- try {326- return await Promise.race([operation, deadline]);327- } finally {328- clearTimeout(timeout);329- }330-}331-332-export type CapturedProcess = {333- child: Deno.ChildProcess;334- ready: Promise<string>;335- completed: Promise<Deno.CommandStatus>;336- stdoutPath: string;337- stderrPath: string;338- stop(): Promise<void>;339-};340-341-function isAlreadyTerminated(error: unknown): boolean {342- return error instanceof Deno.errors.NotFound ||343- (error instanceof TypeError && error.message.includes("already terminated"));344-}345-346-async function captureLines(347- stream: ReadableStream<Uint8Array>,348- path: string,349- onLine: (line: string) => void,350- redact: (text: string) => string,351-): Promise<void> {352- const file = await Deno.open(path, { create: true, truncate: true, write: true });353- const writer = file.writable.getWriter();354- const decoder = new TextDecoder();355- const encoder = new TextEncoder();356- let pending = "";357- try {358- for await (const chunk of stream) {359- pending += decoder.decode(chunk, { stream: true });360- const lines = pending.split(/\r?\n/);361- pending = lines.pop() ?? "";362- for (const line of lines) {363- const redacted = redact(line);364- await writer.write(encoder.encode(`${redacted}\n`));365- onLine(redacted);366- }367- }368- pending += decoder.decode();369- if (pending) {370- const redacted = redact(pending);371- await writer.write(encoder.encode(redacted));372- onLine(redacted);373- }374- } finally {375- await writer.close();376- }377-}378-379-export function redactText(text: string, values: readonly string[]): string {380- return createRedactor(values)(text);381-}382-383-function createRedactor(values: readonly string[]): (text: string) => string {384- const candidates = [...new Set(values.map((value) => value.trim()).filter(Boolean))]385- .sort((left, right) => right.length - left.length);386- return (text) => {387- let redacted = text;388- for (const value of candidates) redacted = redacted.split(value).join("[redacted]");389- return redacted;390- };391-}392-393-function delay(milliseconds: number): Promise<void> {394- return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));395-}396-397-export async function probeHttpReady(url: string): Promise<boolean> {398- try {399- const response = await fetch(url, { signal: AbortSignal.timeout(1_000) });400- await response.body?.cancel();401- return response.status >= 200 && response.status < 400;402- } catch {403- return false;404- }405-}406-407-async function descendantProcessIds(rootPid: number): Promise<number[]> {408- if (Deno.build.os === "windows") return [];409- const output = await new Deno.Command("ps", {410- args: ["-axo", "pid=,ppid="],411- stdout: "piped",412- stderr: "null",413- }).output();414- if (!output.success) return [];415- const children = new Map<number, number[]>();416- for (const line of new TextDecoder().decode(output.stdout).split("\n")) {417- const [pidText, parentText] = line.trim().split(/\s+/);418- const pid = Number(pidText);419- const parent = Number(parentText);420- if (!Number.isInteger(pid) || !Number.isInteger(parent)) continue;421- const siblings = children.get(parent) ?? [];422- siblings.push(pid);423- children.set(parent, siblings);424- }425- const descendants: number[] = [];426- const visit = (pid: number) => {427- for (const child of children.get(pid) ?? []) visit(child);428- if (pid !== rootPid) descendants.push(pid);429- };430- visit(rootPid);431- return descendants;432-}433-434-function signalProcess(pid: number, signal: Deno.Signal): void {435- try {436- Deno.kill(pid, signal);437- } catch (error) {438- if (!isAlreadyTerminated(error)) throw error;439- }440-}441-442-async function waitForPortRelease(url: string, timeoutMs = 2_000): Promise<void> {443- const parsed = new URL(url);444- const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));445- const hostname = parsed.hostname;446- const started = performance.now();447- while (performance.now() - started < timeoutMs) {448- try {449- const connection = await Deno.connect({ hostname, port });450- connection.close();451- await delay(50);452- } catch {453- return;454- }455- }456- throw new Error(`process shutdown left ${parsed.origin} accepting connections`);457-}458-459-export function startReadyProcess(options: {460- executable?: string;461- cwd: string;462- args: string[];463- environment: Record<string, string>;464- artifactRoot: string;465- name: string;466- readyPattern: RegExp;467- readyCheck?: (value: string) => Promise<boolean>;468- timeoutMs: number;469- redactValues?: readonly string[];470-}): CapturedProcess {471- const stdoutPath = join(options.artifactRoot, `${options.name}.stdout.log`);472- const stderrPath = join(options.artifactRoot, `${options.name}.stderr.log`);473- const redact = createRedactor(options.redactValues ?? []);474- const child = new Deno.Command(options.executable ?? Deno.execPath(), {475- args: options.args,476- cwd: options.cwd,477- clearEnv: true,478- env: options.environment,479- stdin: "null",480- stdout: "piped",481- stderr: "piped",482- }).spawn();483-484- let resolveReady!: (value: string) => void;485- let rejectReady!: (error: Error) => void;486- let settled = false;487- let advertisedValue: string | null = null;488- let terminalStatus: Deno.CommandStatus | null = null;489- const stderrTail: string[] = [];490- const knownDescendants = new Set<number>();491- const rememberDescendants = async () => {492- for (const pid of await descendantProcessIds(child.pid)) knownDescendants.add(pid);493- };494- const diagnostics = (reason: string, status = terminalStatus): Error => {495- const processState = status496- ? `exited with ${status.code}${status.signal ? ` (${status.signal})` : ""}`497- : "is still running";498- const tail = stderrTail.length > 0 ? stderrTail.join("\n") : "(empty)";499- return new Error(500- `${options.name} ${reason}; process ${processState}; stderr tail:\n${tail}\nretained stderr: ${stderrPath}`,501- );502- };503- const readiness = new Promise<string>((resolveReadyPromise, rejectReadyPromise) => {504- resolveReady = resolveReadyPromise;505- rejectReady = rejectReadyPromise;506- });507- const verifyAdvertisedValue = async (value: string) => {508- if (!options.readyCheck) {509- if (!settled) {510- settled = true;511- resolveReady(value);512- }513- return;514- }515- while (!settled) {516- if (await options.readyCheck(value)) {517- settled = true;518- resolveReady(value);519- return;520- }521- await delay(50);522- }523- };524- const inspectLine = (line: string) => {525- options.readyPattern.lastIndex = 0;526- const match = options.readyPattern.exec(line);527- if (match && !advertisedValue) {528- advertisedValue = match[1] ?? match[0];529- void rememberDescendants();530- void verifyAdvertisedValue(advertisedValue).catch((error) => {531- if (!settled) {532- settled = true;533- rejectReady(error instanceof Error ? error : new Error(String(error)));534- }535- });536- }537- };538- const stdout = captureLines(child.stdout, stdoutPath, inspectLine, redact);539- const stderr = captureLines(child.stderr, stderrPath, (line) => {540- stderrTail.push(line);541- if (stderrTail.length > 20) stderrTail.shift();542- inspectLine(line);543- }, redact);544- const completed = (async () => {545- const status = await child.status;546- terminalStatus = status;547- if (!settled) {548- // Give the capture loops one turn to drain buffered stderr without letting an549- // inherited grandchild pipe delay the exit diagnosis indefinitely.550- await Promise.race([Promise.all([stdout, stderr]), delay(100)]);551- settled = true;552- rejectReady(diagnostics("exited before its advertised URL accepted requests", status));553- }554- await Promise.all([stdout, stderr]);555- return status;556- })();557- const ready = withDeadline(readiness, options.timeoutMs, `${options.name} URL`).catch((error) => {558- if (!settled) settled = true;559- if (error instanceof Error && error.message.includes("did not become ready")) {560- throw diagnostics(561- advertisedValue562- ? "printed a URL that never accepted HTTP requests before the readiness deadline"563- : "did not print a usable URL before the readiness deadline",564- );565- }566- throw error;567- });568-569- return {570- child,571- ready,572- completed,573- stdoutPath,574- stderrPath,575- async stop() {576- await rememberDescendants();577- for (const pid of knownDescendants) signalProcess(pid, "SIGTERM");578- try {579- child.kill("SIGTERM");580- } catch (error) {581- if (!isAlreadyTerminated(error)) throw error;582- }583- try {584- await withDeadline(completed, 5_000, `${options.name} shutdown`);585- } catch {586- await rememberDescendants();587- for (const pid of knownDescendants) signalProcess(pid, "SIGKILL");588- try {589- child.kill("SIGKILL");590- } catch (error) {591- if (!isAlreadyTerminated(error)) throw error;592- }593- await completed;594- }595- if (advertisedValue?.startsWith("http://") || advertisedValue?.startsWith("https://")) {596- await waitForPortRelease(advertisedValue);597- }598- },599- };600-}601-602-export async function runCapturedCommand(options: {603- executable?: string;604- cwd: string;605- args: string[];606- environment: Record<string, string>;607- artifactRoot: string;608- name: string;609- redactValues?: readonly string[];610-}): Promise<CommandRecord> {611- const stdoutPath = join(options.artifactRoot, `${options.name}.stdout.log`);612- const stderrPath = join(options.artifactRoot, `${options.name}.stderr.log`);613- const started = performance.now();614- const output = await new Deno.Command(options.executable ?? Deno.execPath(), {615- args: options.args,616- cwd: options.cwd,617- clearEnv: true,618- env: options.environment,619- stdin: "null",620- stdout: "piped",621- stderr: "piped",622- }).output();623- const decoder = new TextDecoder();624- await Promise.all([625- Deno.writeTextFile(626- stdoutPath,627- redactText(decoder.decode(output.stdout), options.redactValues ?? []),628- ),629- Deno.writeTextFile(630- stderrPath,631- redactText(decoder.decode(output.stderr), options.redactValues ?? []),632- ),633- ]);634- return {635- name: options.name,636- args: options.args,637- durationMs: Math.round(performance.now() - started),638- exitCode: output.code,639- stdoutPath,640- stderrPath,641- };642-}643-644-export async function initializeFixtureGit(645- projectRoot: string,646- environment: Record<string, string>,647- artifactRoot: string,648- redactValues: readonly string[] = [],649-): Promise<CommandRecord[]> {650- // Deno does not proxy arbitrary git subcommands. Execute git directly, but retain the same651- // structured evidence as every public Deno command.652- const records: CommandRecord[] = [];653- for (654- const [name, args] of [655- ["fixture-git-init", ["init", "-q"]],656- ["fixture-git-add", ["add", "-A"]],657- [658- "fixture-git-commit",659- [660- "-c",661- "user.name=lofi golden path",662- "-c",663- "user.email=golden-path@invalid",664- "-c",665- "commit.gpgsign=false",666- "commit",667- "-qm",668- "clean checkout fixture",669- ],670- ],671- ] as const672- ) {673- const record = await runCapturedCommand({674- executable: "git",675- args: [...args],676- cwd: projectRoot,677- name,678- environment,679- artifactRoot,680- redactValues,681- });682- records.push(record);683- if (record.exitCode !== 0) throw new Error(`${name} failed; see ${record.stderrPath}`);684- }685- return records;686-}687-688-export function reserveLocalPort(): number {689- const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });690- const address = listener.addr as Deno.NetAddr;691- listener.close();692- return address.port;693-}694-695-export function artifactPaths(root: string) {696- return {697- root,698- report: join(root, "report.json"),699- trace: join(root, "trace.zip"),700- screenshot: join(root, "failure.png"),701- };702-}703-704-export async function writeJourneyReport(report: JourneyReport): Promise<void> {705- await Deno.mkdir(dirname(report.artifacts.report), { recursive: true });706- await Deno.writeTextFile(report.artifacts.report, `${JSON.stringify(report, null, 2)}\n`);707-}@@ -1,340 +0,0 @@
1-import { join, resolve } from "node:path";2-import { assert } from "./assert.ts";3-import {4- installNodeSentinel,5- materializeCleanProject,6- probeHttpReady,7- redactText,8- reserveLocalPort,9- runCapturedCommand,10- safeChildEnvironment,11- scanAuthorBoundary,12- startReadyProcess,13- withDeadline,14-} from "./golden_path_core.ts";15-16-async function exists(path: string): Promise<boolean> {17- try {18- await Deno.stat(path);19- return true;20- } catch (error) {21- if (error instanceof Deno.errors.NotFound) return false;22- throw error;23- }24-}25-26-async function workspaceTemp(prefix: string): Promise<string> {27- return await Deno.makeTempDir({ dir: ".", prefix: `.lofi-${prefix}-` });28-}29-30-async function removeWorkspaceTemp(path: string): Promise<void> {31- await Deno.remove(path, { recursive: true }).catch((error) => {32- if (!(error instanceof Deno.errors.NotFound)) throw error;33- });34-}35-36-Deno.test("safe child environment removes ambient application configuration", () => {37- const environment = safeChildEnvironment({38- PATH: "/test/bin",39- DENO_DIR: "/test/deno",40- JAZZ_APP_ID: "public-id",41- JAZZ_SERVER_URL: "https://sync.invalid",42- JAZZ_ADMIN_SECRET: "admin-secret",43- BACKEND_SECRET: "backend-secret",44- UNRELATED_SECRET: "also-secret",45- });46-47- assert(environment.PATH === "/test/bin", "PATH should remain available to Deno tasks");48- assert(environment.DENO_DIR === "/test/deno", "the explicit Deno cache should remain available");49- assert(environment.CI === "1", "clean-room commands should have deterministic CI output");50- assert(environment.JAZZ_APP_ID === "", "public application configuration must be cleared");51- assert(environment.JAZZ_ADMIN_SECRET === "", "server configuration must be cleared");52- assert(!("UNRELATED_SECRET" in environment), "unrelated ambient secrets must not be inherited");53-});54-55-Deno.test("retained evidence redacts allowlisted configuration values", () => {56- const output = redactText(57- "app id public-id server https://sync.invalid secret server-secret",58- ["public-id", "https://sync.invalid", "server-secret"],59- );60- assert(!output.includes("public-id"), "the app id must be redacted");61- assert(!output.includes("sync.invalid"), "the server URL must be redacted");62- assert(!output.includes("server-secret"), "server secrets must be redacted");63- assert(output.match(/\[redacted\]/g)?.length === 3, "every configured value must be replaced");64-});65-66-Deno.test("Node sentinel is first on PATH", async () => {67- if (Deno.build.os === "windows") return;68- const root = await workspaceTemp("node-sentinel");69- try {70- const environment = await installNodeSentinel({ PATH: "/allowed/bin" }, root);71- const sentinel = environment.PATH.split(":")[0];72- assert(73- resolve(sentinel).startsWith(resolve(root)),74- "the sentinel directory must be first on PATH",75- );76- assert(77- (await Deno.readTextFile(join(sentinel, "node"))).includes("hidden Node invocation"),78- "the sentinel must fail with an actionable message",79- );80- } finally {81- await removeWorkspaceTemp(root);82- }83-});84-85-Deno.test("clean project copy excludes secrets caches and the retired prototype", async () => {86- const source = await workspaceTemp("golden-source");87- const destination = await workspaceTemp("golden-destination");88- try {89- await Deno.mkdir(join(source, "apps/prototype"), { recursive: true });90- await Deno.mkdir(join(source, "apps/reference/src"), { recursive: true });91- await Deno.mkdir(join(source, "apps/reference/node_modules/package"), { recursive: true });92- await Deno.mkdir(join(source, "apps/reference/dist"), { recursive: true });93- await Deno.mkdir(join(source, ".nzip-site"), { recursive: true });94- await Promise.all([95- Deno.writeTextFile(join(source, ".env"), "BACKEND_SECRET=do-not-copy\n"),96- Deno.writeTextFile(join(source, ".env.local"), "JAZZ_APP_ID=do-not-copy\n"),97- Deno.writeTextFile(join(source, ".env.example"), "JAZZ_APP_ID=\n"),98- Deno.writeTextFile(join(source, "apps/prototype/retired.ts"), "retired\n"),99- Deno.writeTextFile(join(source, "apps/reference/src/app.ts"), "export const app = {};\n"),100- Deno.writeTextFile(join(source, "apps/reference/node_modules/package/index.js"), "cache\n"),101- Deno.writeTextFile(join(source, "apps/reference/dist/index.html"), "build\n"),102- Deno.writeTextFile(join(source, ".nzip-site/index.html"), "hosted\n"),103- ]);104-105- await materializeCleanProject(source, destination);106- assert(await exists(join(destination, ".env.example")), ".env.example should be copied");107- assert(108- await exists(join(destination, "apps/reference/src/app.ts")),109- "source should be copied",110- );111- assert(!await exists(join(destination, ".env")), ".env must be excluded");112- assert(!await exists(join(destination, ".env.local")), ".env.local must be excluded");113- assert(114- !await exists(join(destination, "apps/prototype")),115- "retired prototype must be excluded",116- );117- assert(118- !await exists(join(destination, "apps/reference/node_modules")),119- "node_modules must be excluded",120- );121- assert(!await exists(join(destination, "apps/reference/dist")), "dist must be excluded");122- assert(!await exists(join(destination, ".nzip-site")), "hosted artifacts must be excluded");123- } finally {124- await Promise.all([removeWorkspaceTemp(source), removeWorkspaceTemp(destination)]);125- }126-});127-128-Deno.test("clean project materialization copies only tracked files from a repository", async () => {129- const source = await workspaceTemp("tracked-source");130- const destination = await workspaceTemp("tracked-destination");131- try {132- await Deno.writeTextFile(join(source, "tracked.ts"), "export const tracked = true;\n");133- await Deno.writeTextFile(join(source, "untracked.ts"), "export const untracked = true;\n");134- for (const args of [["init", "-q"], ["add", "tracked.ts"]]) {135- const output = await new Deno.Command("git", { args, cwd: source }).output();136- assert(output.success, `git ${args[0]} fixture setup failed`);137- }138-139- await materializeCleanProject(source, destination);140- assert(await exists(join(destination, "tracked.ts")), "tracked source must be copied");141- assert(!await exists(join(destination, "untracked.ts")), "untracked source must be excluded");142- } finally {143- await Promise.all([144- removeWorkspaceTemp(source),145- removeWorkspaceTemp(destination),146- ]);147- }148-});149-150-Deno.test("author boundary permits schema declarations and reports UI plumbing", async () => {151- const root = await workspaceTemp("golden-boundary");152- try {153- await Deno.mkdir(join(root, "src/islands"), { recursive: true });154- await Promise.all([155- Deno.writeTextFile(156- join(root, "src/schema.ts"),157- 'import { co } from "jazz-tools";\nexport const schema = co;\n',158- ),159- Deno.writeTextFile(160- join(root, "src/permissions.ts"),161- 'import type { Group } from "jazz-tools";\nexport type Permission = Group;\n',162- ),163- Deno.writeTextFile(164- join(root, "src/islands/App.tsx"),165- 'import { createDb } from "jazz-tools";\nconst ready = navigator.storage.persisted();\n',166- ),167- ]);168-169- const violations = await scanAuthorBoundary(root, ["src"]);170- assert(violations.length === 3, `expected three violations, found ${violations.length}`);171- assert(172- violations.every((violation) => violation.path === "src/islands/App.tsx"),173- "schema and permissions should retain the explicit M1 exception",174- );175- assert(176- violations.some((violation) => violation.rule === "raw-jazz-runtime-import"),177- "raw imports in product UI should be reported",178- );179- assert(180- violations.some((violation) => violation.rule === "raw-jazz-client-api"),181- "raw client construction should be reported",182- );183- assert(184- violations.some((violation) => violation.rule === "browser-capability-branch"),185- "capability branching should be reported",186- );187- } finally {188- await removeWorkspaceTemp(root);189- }190-});191-192-Deno.test("process readiness resolves from output rather than a fixed sleep", async () => {193- const artifacts = await workspaceTemp("golden-process");194- try {195- const process = startReadyProcess({196- executable: "git",197- cwd: Deno.cwd(),198- args: ["--version"],199- environment: safeChildEnvironment({}),200- artifactRoot: artifacts,201- name: "ready-control",202- readyPattern: /(git version [^\n]+)/,203- timeoutMs: 5_000,204- });205- assert(206- (await process.ready).startsWith("git version "),207- "the emitted control line should satisfy readiness",208- );209- assert((await process.completed).success, "the readiness control should exit successfully");210- await process.stop();211- } finally {212- await removeWorkspaceTemp(artifacts);213- }214-});215-216-Deno.test("HTTP readiness waits until an early advertised URL accepts requests", async () => {217- const artifacts = await workspaceTemp("golden-http-ready");218- const port = reserveLocalPort();219- const url = `http://127.0.0.1:${port}/`;220- const started = performance.now();221- const process = startReadyProcess({222- executable: Deno.execPath(),223- cwd: Deno.cwd(),224- args: [225- "eval",226- `console.log("READY ${url}"); await new Promise((resolve) => setTimeout(resolve, 200)); Deno.serve({ hostname: "127.0.0.1", port: ${port} }, () => new Response("ready")); await new Promise(() => {});`,227- ],228- environment: safeChildEnvironment({}),229- artifactRoot: artifacts,230- name: "early-banner",231- readyPattern: /READY (http:\/\/127\.0\.0\.1:\d+\/)/,232- readyCheck: probeHttpReady,233- timeoutMs: 5_000,234- });235- try {236- assert(await process.ready === url, "the accepting URL should satisfy readiness");237- assert(238- performance.now() - started >= 150,239- "a printed banner must not resolve before the listener accepts requests",240- );241- } finally {242- await process.stop();243- await removeWorkspaceTemp(artifacts);244- }245-});246-247-Deno.test("post-banner exit rejects with retained diagnostics", async () => {248- const artifacts = await workspaceTemp("golden-http-exit");249- const port = reserveLocalPort();250- const process = startReadyProcess({251- executable: Deno.execPath(),252- cwd: Deno.cwd(),253- args: [254- "eval",255- `console.log("READY http://127.0.0.1:${port}/"); console.error("unique post-banner failure"); Deno.exit(42);`,256- ],257- environment: safeChildEnvironment({}),258- artifactRoot: artifacts,259- name: "post-banner-exit",260- readyPattern: /READY (http:\/\/127\.0\.0\.1:\d+\/)/,261- readyCheck: probeHttpReady,262- timeoutMs: 5_000,263- });264- try {265- let message = "";266- try {267- await process.ready;268- } catch (error) {269- message = error instanceof Error ? error.message : String(error);270- }271- assert(message.includes("exited with 42"), "readiness must report the process exit code");272- assert(message.includes("unique post-banner failure"), "readiness must include stderr tail");273- assert(message.includes(process.stderrPath), "readiness must point to retained stderr");274- await process.completed;275- } finally {276- await process.stop();277- await removeWorkspaceTemp(artifacts);278- }279-});280-281-Deno.test("stopping a ready wrapper terminates its serving grandchild", async () => {282- if (Deno.build.os === "windows") return;283- const artifacts = await workspaceTemp("golden-process-tree");284- const port = reserveLocalPort();285- const url = `http://127.0.0.1:${port}/`;286- const serverSource =287- `Deno.serve({ hostname: "127.0.0.1", port: ${port} }, () => new Response("child")); await new Promise(() => {});`;288- const wrapperSource = `const child = new Deno.Command(Deno.execPath(), { args: ["eval", ${289- JSON.stringify(serverSource)290- }], stdout: "inherit", stderr: "inherit" }).spawn(); console.log("READY ${url}"); await child.status;`;291- const process = startReadyProcess({292- executable: Deno.execPath(),293- cwd: Deno.cwd(),294- args: ["eval", wrapperSource],295- environment: safeChildEnvironment({}),296- artifactRoot: artifacts,297- name: "wrapper-tree",298- readyPattern: /READY (http:\/\/127\.0\.0\.1:\d+\/)/,299- readyCheck: probeHttpReady,300- timeoutMs: 5_000,301- });302- try {303- await process.ready;304- await process.stop();305- const rebound = Deno.listen({ hostname: "127.0.0.1", port });306- rebound.close();307- } finally {308- await process.stop().catch(() => undefined);309- await removeWorkspaceTemp(artifacts);310- }311-});312-313-Deno.test("captured commands retain output and duration", async () => {314- const artifacts = await workspaceTemp("golden-command");315- try {316- const record = await runCapturedCommand({317- executable: "git",318- cwd: Deno.cwd(),319- args: ["--version"],320- environment: safeChildEnvironment({}),321- artifactRoot: artifacts,322- name: "capture-control",323- });324- assert(record.exitCode === 0, "the control command should pass");325- assert(record.durationMs >= 0, "duration should be retained");326- assert(327- (await Deno.readTextFile(record.stdoutPath)).includes("git version"),328- "stdout should be retained",329- );330- } finally {331- await removeWorkspaceTemp(artifacts);332- }333-});334-335-Deno.test("deadline helper preserves a readiness result", async () => {336- assert(337- await withDeadline(Promise.resolve("ready"), 1_000, "control") === "ready",338- "a ready operation should win the deadline race",339- );340-});@@ -1,79 +0,0 @@
1-import { join, resolve } from "node:path";2-import { fileURLToPath } from "node:url";3-import { runJourney } from "./golden_path.ts";4-import {5- installNodeSentinel,6- runCapturedCommand,7- safeChildEnvironment,8-} from "./golden_path_core.ts";9-import { assert } from "./assert.ts";10-11-const sourceRoot = resolve(Deno.cwd());12-const artifactRoot = join(sourceRoot, "test-results/golden-path-create");13-const temporaryRoot = await Deno.makeTempDir({ prefix: "lofi-create-golden-" });14-const projectRoot = join(temporaryRoot, "lofi-app");15-const journeyStartedAt = new Date();16-const journeyStartedPerformanceMs = performance.now();17-18-try {19- await Deno.remove(artifactRoot, { recursive: true }).catch((error) => {20- if (!(error instanceof Deno.errors.NotFound)) throw error;21- });22- await Deno.mkdir(artifactRoot, { recursive: true });23- const environment = await installNodeSentinel(safeChildEnvironment(), artifactRoot);24- environment.LOFI_CREATE_DEVELOPMENT = "1";25- environment.LOFI_CREATE_PACKAGE_PREFIX = new URL("../package/", import.meta.url).href;26- const create = await runCapturedCommand({27- cwd: temporaryRoot,28- args: [29- "run",30- "-A",31- fileURLToPath(new URL("../package/create.ts", import.meta.url)),32- "lofi-app",33- ],34- environment,35- artifactRoot,36- name: "create",37- });38- if (create.exitCode !== 0) {39- throw new Error(`create failed with exit ${create.exitCode}; inspect ${create.stderrPath}`);40- }41- const createOutput = await Deno.readTextFile(create.stdoutPath);42- assert(createOutput.includes("Created lofi-app"), "create output did not name the created path");43- assert(createOutput.includes(" cd lofi-app"), "create output did not print the exact cd step");44- assert(45- createOutput.includes(" deno task dev"),46- "create output did not print the exact dev step",47- );48-49- const report = await runJourney({50- source: "create",51- projectRoot,52- artifactRoot,53- sourceRevisionRoot: sourceRoot,54- resetArtifactRoot: false,55- initialCommands: [create],56- commands: { doctor: "doctor" },57- authorPaths: [58- "src/app.ts",59- "src/islands",60- "src/pages",61- "src/permissions.ts",62- "src/schema.ts",63- "src/styles",64- ],65- journeyStartedAt,66- journeyStartedPerformanceMs,67- createDurationMs: create.durationMs,68- developerCommandCount: 3,69- });70- console.log(`lofi generated golden path: passed (${report.artifacts.report})`);71-} catch (error) {72- console.error(73- `lofi generated golden path: failed: ${error instanceof Error ? error.message : error}`,74- );75- console.error("rerun: deno task test:golden:create");76- Deno.exitCode = 1;77-} finally {78- await Deno.remove(temporaryRoot, { recursive: true }).catch(() => undefined);79-}@@ -1,49 +0,0 @@
1-const INTERNAL_ENV = "spikes/jazz2-baseline/.env";2-const LOCAL_APP_ID = "00000000-0000-0000-0000-00000000f153";3-4-async function ensureStableLocalAppId() {5- let content = "";6- try {7- content = await Deno.readTextFile(INTERNAL_ENV);8- } catch (error) {9- if (!(error instanceof Deno.errors.NotFound)) throw error;10- }11-12- if (/^VITE_JAZZ_APP_ID=/m.test(content)) return;13- const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";14- await Deno.writeTextFile(15- INTERNAL_ENV,16- `${content}${separator}VITE_JAZZ_APP_ID=${LOCAL_APP_ID}\n`,17- );18-}19-20-const [mode, ...viteArgs] = Deno.args;21-if (mode !== "local" && mode !== "cloud") {22- console.error("usage: run_jazz2_spike.ts <local|cloud> [vite options]");23- Deno.exit(2);24-}25-26-await ensureStableLocalAppId();27-28-const args = [29- "run",30- ...(mode === "cloud" ? ["--env-file=.env"] : []),31- "-A",32- "npm:vite@8.0.1",33- "spikes/jazz2-baseline",34- "--config",35- "spikes/jazz2-baseline/vite.config.ts",36- "--host",37- "127.0.0.1",38- ...viteArgs,39-];40-41-const child = new Deno.Command(Deno.execPath(), {42- args,43- stdin: "inherit",44- stdout: "inherit",45- stderr: "inherit",46-}).spawn();47-48-const status = await child.status;49-Deno.exit(status.code);@@ -1,176 +0,0 @@
1-import { environmentNames, validateEnvironment } from "./env_contract.ts";2-import { loadEnvironment } from "./load_env.ts";3-4-const INTERNAL_ENV = "apps/reference/.env";5-const LOCAL_APP_ID = "00000000-0000-0000-0000-00000000f153";6-7-async function ensureStableLocalAppId() {8- let content = "";9- try {10- content = await Deno.readTextFile(INTERNAL_ENV);11- } catch (error) {12- if (!(error instanceof Deno.errors.NotFound)) throw error;13- }14- if (/^VITE_JAZZ_APP_ID=/m.test(content)) return;15- const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";16- await Deno.writeTextFile(17- INTERNAL_ENV,18- `${content}${separator}VITE_JAZZ_APP_ID=${LOCAL_APP_ID}\n`,19- );20-}21-22-async function forward(stream: ReadableStream<Uint8Array>, target: WritableStream<Uint8Array>) {23- await stream.pipeTo(target, { preventClose: true });24-}25-26-async function run(args: string[], env: Record<string, string>): Promise<number> {27- const child = new Deno.Command(Deno.execPath(), {28- args,29- env,30- stdin: "inherit",31- stdout: "piped",32- stderr: "piped",33- }).spawn();34- const forwardSignal = (signal: Deno.Signal) => {35- try {36- child.kill(signal);37- } catch (error) {38- if (!(error instanceof Deno.errors.NotFound)) throw error;39- }40- };41- const onInterrupt = () => forwardSignal("SIGINT");42- const onTerminate = () => forwardSignal("SIGTERM");43- Deno.addSignalListener("SIGINT", onInterrupt);44- Deno.addSignalListener("SIGTERM", onTerminate);45- const output = forward(child.stdout, Deno.stdout.writable);46- const errors = forward(child.stderr, Deno.stderr.writable);47- try {48- const [status] = await Promise.all([child.status, output, errors]);49- return status.code;50- } finally {51- Deno.removeSignalListener("SIGINT", onInterrupt);52- Deno.removeSignalListener("SIGTERM", onTerminate);53- }54-}55-56-async function routeCount(): Promise<number> {57- let count = 0;58- async function visit(path: string) {59- for await (const entry of Deno.readDir(path)) {60- const child = `${path}/${entry.name}`;61- if (entry.isDirectory) await visit(child);62- else if (entry.isFile && entry.name.endsWith(".html")) count += 1;63- }64- }65- try {66- await visit("apps/reference/dist");67- } catch (error) {68- if (!(error instanceof Deno.errors.NotFound)) throw error;69- }70- return count;71-}72-73-async function buildFiles(root: string): Promise<string[]> {74- const files: string[] = [];75- async function visit(path: string) {76- for await (const entry of Deno.readDir(path)) {77- const child = `${path}/${entry.name}`;78- if (entry.isDirectory) await visit(child);79- else if (entry.isFile) files.push(`./${child.slice(root.length + 1)}`);80- }81- }82- await visit(root);83- return files.sort();84-}85-86-const command = Deno.args[0];87-if (command !== "dev" && command !== "build") {88- console.error("usage: run_reference.ts <dev|build>");89- Deno.exit(2);90-}91-92-const environment = await loadEnvironment();93-const validation = validateEnvironment(environment);94-if (!validation.ok) {95- console.error("lofi environment mode: invalid");96- for (const error of validation.errors) console.error(`error: ${error}`);97- Deno.exit(1);98-}99-for (const warning of validation.warnings) console.warn(`warning: ${warning}`);100-await ensureStableLocalAppId();101-102-const validatedEnvironment = Object.fromEntries(103- environmentNames104- .map((name) => [name, environment[name]?.trim() ?? ""] as const)105- .filter(([, value]) => value.length > 0),106-);107-const childEnvironment = { ...validatedEnvironment };108-if (command === "dev") childEnvironment.ASTRO_DEV_BACKGROUND = "1";109-if (command === "build") {110- childEnvironment.LOFI_SKIP_JAZZ_MANAGED = "1";111- childEnvironment.JAZZ_ADMIN_SECRET = "";112- childEnvironment.BACKEND_SECRET = "";113-}114-115-const forwardedArgs = Deno.args.slice(1).filter((argument, index) => {116- return !(index === 0 && argument === "--");117-});118-119-if (command === "dev") {120- console.log("lofi dev");121- console.log("Storage: persistent/OPFS requested; browser diagnosis in UI");122- console.log("Identity: device-local key; passkey backup blocked by alpha security review");123- console.log(124- `Sync: ${125- validation.mode === "cloud-configured" ? "managed configured" : "local development server"126- }`,127- );128- console.log("PWA: development service worker disabled");129-}130-131-const exitCode = await run(132- [133- "run",134- "-A",135- "npm:astro@7.0.9",136- command,137- "--root",138- "apps/reference",139- ...forwardedArgs,140- ],141- childEnvironment,142-);143-if (exitCode !== 0) Deno.exit(exitCode);144-145-if (command === "build") {146- const revision = new TextDecoder().decode(147- (await new Deno.Command("git", {148- args: ["rev-parse", "--short", "HEAD"],149- stdout: "piped",150- }).output()).stdout,151- ).trim();152- await Deno.writeTextFile(153- "apps/reference/dist/lofi-build.json",154- `${JSON.stringify({ revision, builtAt: new Date().toISOString() })}\n`,155- );156- const serviceWorkerPath = "apps/reference/dist/sw.js";157- const serviceWorker = await Deno.readTextFile(serviceWorkerPath);158- await Deno.writeTextFile(159- serviceWorkerPath,160- serviceWorker.replace("__LOFI_BUILD_REVISION__", revision),161- );162- const precache = (await buildFiles("apps/reference/dist"))163- .filter((path) => path !== "./lofi-precache.json")164- .map((path) => path === "./index.html" ? "./" : path);165- precache.push("./lofi-precache.json");166- await Deno.writeTextFile(167- "apps/reference/dist/lofi-precache.json",168- `${JSON.stringify(precache.sort())}\n`,169- );170- const scanCode = await run(171- ["task", "check:secrets"],172- validatedEnvironment,173- );174- if (scanCode !== 0) Deno.exit(scanCode);175- console.log(`lofi build: apps/reference/dist (${await routeCount()} routes, ${revision})`);176-}