Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,8 @@ jobs:
pnpm --filter @mdbase-writer/app test:browser:reliability || result=1
exit "$result"
- name: Verify private feedback with demo data and intercepted delivery
env:
VITE_WRITER_DEMO: "1"
VITE_MDBASE_FEEDBACK_URL: http://127.0.0.1:8892/v1/feedback
VITE_MDBASE_ENV: lab
run: |
pnpm --filter @mdbase-writer/app build
pnpm --filter @mdbase-writer/app build:feedback-test
pnpm --filter @mdbase-writer/app exec vite preview --host 127.0.0.1 --port 8892 --strictPort > "$RUNNER_TEMP/writer-feedback-preview.log" 2>&1 &
preview_pid=$!
trap 'kill "$preview_pid" 2>/dev/null || true' EXIT
Expand Down
1 change: 1 addition & 0 deletions apps/writer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"icons": "node scripts/write-pwa-icons.mjs",
"build": "pnpm manifest && pnpm manifest:validate && vite build",
"build:lab": "VITE_MDBASE_ENV=lab pnpm build",
"build:feedback-test": "VITE_WRITER_DEMO=1 VITE_MDBASE_ENV=lab VITE_MDBASE_FEEDBACK_URL=http://127.0.0.1:8892/v1/feedback pnpm build",
"dev": "pnpm manifest && vite --host 127.0.0.1",
"manifest": "node scripts/write-manifest.mjs",
"manifest:validate": "mdbase-connect-dev validate-manifest public/.well-known/mdbase-app.json",
Expand Down
3 changes: 2 additions & 1 deletion apps/writer/src/LegacyApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import { ThemeSelect } from "@mdbase-dev/ui/theme-select";
import { ConnectLayout, OpeningScreen } from "@mdbase-dev/ui/screens";

import { ManuscriptWorkspace } from "./workspace/workspace.js";
import { writerDemoRequested } from "./demo-mode.js";


const params = new URL(location.href).searchParams;
const demoRequested = (import.meta.env.DEV || import.meta.env.VITE_WRITER_DEMO === "1") && params.has("demo");
const demoRequested = writerDemoRequested();

export function LegacyApp() {
const { reportError } = useFeedback();
Expand Down
6 changes: 6 additions & 0 deletions apps/writer/src/demo-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Explicit development/test fixtures only. A demo query alone never bypasses
* the native entry in ordinary deployment builds. */
export function writerDemoRequested(): boolean {
return (import.meta.env.DEV || import.meta.env.VITE_WRITER_DEMO === "1")
&& new URL(location.href).searchParams.has("demo");
}
67 changes: 67 additions & 0 deletions apps/writer/src/main.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, expect, it, vi } from "vitest";

const bootstrap = vi.hoisted(() => ({
render: vi.fn(),
entry: { application: {} },
create: vi.fn(),
grantReturn: vi.fn(),
deliver: vi.fn(async () => {}),
}));
vi.mock("react-dom/client", () => ({ createRoot: () => ({ render: bootstrap.render }) }));
vi.mock("./App.js", () => ({ App: () => null }));
vi.mock("./FeedbackRoot.js", () => ({ FeedbackRoot: () => null }));
vi.mock("./pwa-install.js", () => ({ setupPwaInstall: () => () => {} }));
vi.mock("./connect/sdk-application.js", () => ({
writerMdbaseForBuild: bootstrap.create,
isWriterGrantReturn: bootstrap.grantReturn,
deliverWriterGrantReturn: bootstrap.deliver,
}));

beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
bootstrap.create.mockReturnValue(bootstrap.entry);
bootstrap.grantReturn.mockReturnValue(false);
document.body.innerHTML = '<div id="root"></div>';
history.replaceState({}, "", "/?demo");
vi.stubEnv("DEV", false);
vi.stubEnv("VITE_MDBASE_ENV", "lab");
vi.stubEnv("VITE_WRITER_DEMO", undefined);
});
afterEach(() => vi.unstubAllEnvs());

it("keeps an ordinary LAB demo query on the native entry without an enabled fixture build", async () => {
await import("./main.js");
expect(bootstrap.create).toHaveBeenCalledExactlyOnceWith("lab");
expect(bootstrap.render.mock.calls[0]![0].props.children.props.children.props.sdk).toBe(bootstrap.entry);
});

it("opens the explicit production demo fixture without allocating a native account entry", async () => {
vi.stubEnv("VITE_WRITER_DEMO", "1");
await import("./main.js");
expect(bootstrap.create).not.toHaveBeenCalled();
expect(bootstrap.render.mock.calls[0]![0].props.children.props.children.props.sdk).toBeUndefined();
});

it("still opens the normal native entry in a fixture-enabled build without the demo query", async () => {
vi.stubEnv("VITE_WRITER_DEMO", "1");
history.replaceState({}, "", "/");
await import("./main.js");
expect(bootstrap.create).toHaveBeenCalledExactlyOnceWith("lab");
});

it("honors the existing development demo without allocating a native entry", async () => {
vi.stubEnv("DEV", true);
await import("./main.js");
expect(bootstrap.create).not.toHaveBeenCalled();
});

it("delivers a grant return ahead of an explicitly enabled demo and never allocates a second entry", async () => {
vi.stubEnv("VITE_WRITER_DEMO", "1");
bootstrap.grantReturn.mockReturnValue(true);
await import("./main.js");
expect(bootstrap.deliver).toHaveBeenCalledExactlyOnceWith(location.href);
expect(bootstrap.create).not.toHaveBeenCalled();
expect(bootstrap.render.mock.calls[0]![0].props.children.type).toBe("p");
});
3 changes: 2 additions & 1 deletion apps/writer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import { App } from "./App.js";
import { writerDemoRequested } from "./demo-mode.js";
import { FeedbackRoot } from "./FeedbackRoot.js";
import { deliverWriterGrantReturn, isWriterGrantReturn, writerMdbaseForBuild } from "./connect/sdk-application.js";

Expand All @@ -27,7 +28,7 @@ if (import.meta.hot) import.meta.hot.dispose(stopPwaInstall);
const grantReturn = isWriterGrantReturn(import.meta.env.VITE_MDBASE_ENV, location.href);
if (grantReturn) void deliverWriterGrantReturn(location.href).catch(() => {});
// One declaration-only singleton outside React/StrictMode. SDK owns its runtime.
const sdk = grantReturn ? undefined : writerMdbaseForBuild(import.meta.env.VITE_MDBASE_ENV);
const sdk = grantReturn || writerDemoRequested() ? undefined : writerMdbaseForBuild(import.meta.env.VITE_MDBASE_ENV);
const root = document.getElementById("root");
if (!root) throw new Error("Missing #root");
createRoot(root).render(
Expand Down
5 changes: 4 additions & 1 deletion apps/writer/src/workspace/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ afterEach(async () => {
function fixture() {
const authority = createRecordTestAuthority();
authority.records.follow(authority.watch);
authority.seed("main.md", { body: "# Main\n\n![[chapters/one]]\n", frontmatter: { type: "custom", local_title: "Mapped title", local_style: "chicago-notes-bibliography" } });
// Use a small real CSL style for workspace-state tests. Repeatedly parsing
// the much larger Chicago style dominates their CI budget; core tests cover it.
authority.seed("main.md", { body: "# Main\n\n![[chapters/one]]\n", frontmatter: { type: "custom", local_title: "Mapped title", local_style: "ieee" } });
authority.seed("chapters/one.md", { body: "# First chapter\n\n![[two]]\n" });
authority.seed("chapters/two.md", { body: "# Nested chapter\n\nComplete text.\n\n![Figure](image.svg)\n" });
const backend: WriterBackend = {
Expand Down Expand Up @@ -286,6 +288,7 @@ describe("workspace reliability", () => {
await ready(workspace);
await vi.waitFor(() => expect(workspace["compile"].send).toHaveBeenCalledWith(expect.objectContaining({ type: "init" })));
expect(workspace.getSnapshot().annotationsLoad.phase).toBe("ready");
expect(workspace.getSnapshot().records.get("main.md")?.snapshot.frontmatter["csl"]).toBe("ieee");
expect((await workspace.exportBundle()).problems).toEqual([]);
paths.mockImplementationOnce(async () => fail("Temporary annotation failure"));
await workspace.retryMetadata();
Expand Down
18 changes: 18 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ The existing browser acceptance coverage uses the demo collection, including the
Acceptance against a real Connect collection has not yet been recorded in the architecture notes.
Do not treat demo coverage as proof of connected-collection behaviour.

## Private feedback acceptance

Build the explicit local fixture, then serve it on the feedback test's loopback port:

```sh
pnpm --filter @mdbase-writer/app build:feedback-test
pnpm --filter @mdbase-writer/app exec vite preview --host 127.0.0.1 --port 8892 --strictPort
# In another terminal:
pnpm --filter @mdbase-writer/app test:feedback
```

This build opts into the in-memory demo with `?demo`, including with the LAB environment
selected. Bootstrap does not create a native account entry for that explicit demo; ordinary
LAB URLs still use the SDK, and grant-return callbacks take precedence. The unchanged test
checks desktop/mobile feedback, focus and draft restoration, redacted diagnostics and
consented payloads. All feedback delivery is intercepted. Do not deploy this fixture build:
its feedback endpoint is loopback, and it is not connected-collection acceptance evidence.

## Against a real collection

The hosted mdbase Connect service accepts applications served over HTTPS only, so a localhost
Expand Down