Skip to content

Commit 2fb2a57

Browse files
authored
Merge pull request #100 from BGforgeNet/dev/dialog-editor
Dev/dialog editor
2 parents 6bf4515 + 852dff8 commit 2fb2a57

215 files changed

Lines changed: 30283 additions & 3745 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/harness.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: "Harness"
2+
3+
# Render/drive the binary-editor and dialog-editor webview harnesses in headless Chromium as a regression
4+
# suite (scripts/test-harness.sh). Each driver mounts the real webview bundle and asserts through PASS/FAIL
5+
# gates - catching render/mount/CSP/layout regressions the node/jsdom vitest suites cannot see.
6+
#
7+
# Kept in its OWN job rather than the main "Build" gate so the Chromium download (~150MB) and the browser
8+
# runs add no wall-clock to every other check. See the harness READMEs and docs/architecture.md.
9+
10+
on:
11+
pull_request:
12+
push:
13+
branches:
14+
- "**"
15+
16+
# Cancel superseded runs on the same ref (branch pushes and PR updates).
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
harness:
26+
name: harness
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
31+
with:
32+
persist-credentials: false
33+
34+
- name: Install pnpm
35+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
36+
with:
37+
run_install: false
38+
39+
- name: Install Node.js
40+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
41+
with:
42+
node-version: 22
43+
cache: pnpm
44+
45+
- name: Install dependencies
46+
run: pnpm install --frozen-lockfile
47+
48+
# The dialog-editor edit drivers parse real .d through the weidu-d tree-sitter WASM (via the fake host),
49+
# which is a gitignored build artifact.
50+
- name: Build tree-sitter grammar
51+
run: pnpm build:grammar
52+
53+
# playwright is a pinned devDep, but pnpm's build-script gate skips its browser postinstall, so pull the
54+
# one browser the harnesses launch here. --with-deps installs the OS libraries Chromium needs on the runner.
55+
- name: Install Chromium for Playwright
56+
run: pnpm exec playwright install --with-deps chromium
57+
58+
# Several binary-editor drivers read real .itm/.cre/.map files from the gitignored-but-reproducible
59+
# external/ trees (the dialog drivers and the deep-jump driver use committed testFixture/ and need none).
60+
- name: Reset external fixture repos
61+
run: ./scripts/reset-external.sh
62+
63+
- name: Run render/drive harnesses
64+
run: ./scripts/test-harness.sh

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ bgforge-mls-kate*/
1111
.reports/
1212
.vscode-test/
1313
.dev/
14+
.playwright-mcp/
1415

1516
client/node_modules
1617
client/out

.oxfmtrc.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"themes/vs-seti-icon-theme.json",
3333
"themes/bgforge-monokai.json",
3434
"themes/bgforge-icon-theme.json",
35-
"binary-editor/test/harness/*.html"
35+
"binary-editor/test/harness/*.html",
36+
"client/src/dialog-editor/test/harness/app.html",
37+
"client/src/dialog-editor/test/harness/real-model.ts"
3638
]
3739
}

.oxlintrc.json

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,21 +266,33 @@
266266
// `document.getElementById` is clearer and faster than
267267
// `.querySelector('#id')`; the rule's premise doesn't hold.
268268
// Webview scripts are the only DOM consumers in the codebase.
269-
"files": ["client/src/**/*-webview*.ts"],
269+
"files": ["client/src/**/*-webview*.ts", "client/src/dialog-editor/webview/**"],
270270
"rules": {
271271
"unicorn/prefer-query-selector": "off"
272272
}
273273
},
274+
{
275+
// Svelte 5 runes require `let`: `let x = $state(...)` and
276+
// `let { ... } = $props()` are reassigned by the compiler's reactivity,
277+
// which oxlint cannot see - it reports them as never-reassigned. `sort-vars`
278+
// likewise fights the rune/prop declaration order. Scope: the dialog editor's
279+
// Svelte webview components.
280+
"files": ["client/src/dialog-editor/webview/**/*.svelte"],
281+
"rules": {
282+
"prefer-const": "off",
283+
"sort-vars": "off"
284+
}
285+
},
274286
{
275287
// VS Code's `webview.postMessage()` (extension side),
276288
// `acquireVsCodeApi().postMessage()` (webview side), and
277289
// `worker_threads` `parentPort.postMessage()` are not the browser's
278290
// `window.postMessage`; none take a `targetOrigin`.
279291
"files": [
280292
"client/src/editors/**",
281-
"client/src/dialog-tree/**",
282293
"client/src/webview-utils.ts",
283-
"client/src/binary-editor/**"
294+
"client/src/binary-editor/**",
295+
"client/src/dialog-editor/**"
284296
],
285297
"rules": {
286298
"unicorn/require-post-message-target-origin": "off"
@@ -366,6 +378,7 @@
366378
"**/out/**",
367379
"**/*.d.ts",
368380
"server/src/user-messages.ts",
369-
"binary-editor/test/harness/**"
381+
"binary-editor/test/harness/**",
382+
"client/src/dialog-editor/test/harness/**"
370383
]
371384
}

AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Dependabot version-update PRs are intentionally disabled - there is no `.github/
1818
- **User-facing messages:** Never call `connection.window.showInformationMessage/showWarningMessage/showErrorMessage` directly in server code. Use `showInfo()`, `showWarning()`, `showError()`, or `showErrorWithActions()` from `user-messages.ts` - they auto-decode `file://` URIs to readable paths. An oxlint rule enforces this.
1919
- **Temporary artifacts:** Put transient test/build files under the repo-level `tmp/` directory (or `os.tmpdir()` when system temp is required). Do not create ad hoc temp directories under source trees like `server/test/`, `binary/test/`, or `scripts/**`.
2020
- **Webview CSP - styles need `cspSource`, not a bare nonce.** A webview's `style-src` must include `{{cspSource}}`. VS Code's wrapped webview silently drops a `style-src 'nonce-...'` stylesheet that lacks `cspSource` (raw Chromium honors it, so headless/standalone renders pass while the real panel renders fully unstyled). Load CSS as `webview.asWebviewUri()` `<link>` elements with `style-src {{cspSource}}` (keep the nonce for `script-src` only); add each CSS dir to `localResourceRoots`. See `docs/architecture.md` (Webview CSP); guarded by `client/test/webview-csp.test.ts`.
21-
- **Binary-editor webview changes can be rendered - don't fall back to a sketch without checking.** A headless Playwright harness in `binary-editor/test/harness/` loads the real `App.svelte` bundle in Chromium and writes per-format PNG screenshots (e.g. `render-pro-eff.mts` -> `shot-pro.png` + `shot-eff.png`, `render-itm.mts`, `render-spl.mts`, `render-cre.mts`, `render-map.mts`, `render-primitives.mts`). For any visual/CSS/layout change to the binary editor, render and inspect the screenshot rather than reasoning about the cascade blind. Run order: `cd binary && pnpm build` (only if `binary/src` changed), then `pnpm exec tsx binary-editor/test/harness/build.mts` (rebuilds `app.html` after any webview/Svelte/`styles.css` edit), then a driver `pnpm exec tsx binary-editor/test/harness/render-pro-eff.mts`. Prereqs (environment, not repo deps): Playwright + a Chromium browser on `PATH`. It is e2e-tier and intentionally excluded from `pnpm test`/`pnpm test:all`. See `binary-editor/test/harness/README.md` for the harness, and **`docs/binary-editor-ui-guidelines.md`** for the project's UI conventions and the review brief - it indexes the co-located writer-facing rules (`client/src/binary-editor/webview/AGENTS.md` for the render layer, `binary/src/AGENTS.md` for the layout schema), which auto-load when you edit those dirs. Review rendered screenshots against the guidelines.
21+
- **Binary-editor webview changes can be rendered - don't fall back to a sketch without checking.** A headless Playwright harness in `binary-editor/test/harness/` loads the real `App.svelte` bundle in Chromium and writes per-format PNG screenshots (e.g. `render-pro-eff.mts` -> `shot-pro.png` + `shot-eff.png`, `render-itm.mts`, `render-spl.mts`, `render-cre.mts`, `render-map.mts`, `render-primitives.mts`). For any visual/CSS/layout change to the binary editor, render and inspect the screenshot rather than reasoning about the cascade blind. Run order: `cd binary && pnpm build` (only if `binary/src` changed), then `pnpm exec tsx binary-editor/test/harness/build.mts` (rebuilds `app.html` after any webview/Svelte/`styles.css` edit), then a driver `pnpm exec tsx binary-editor/test/harness/render-pro-eff.mts`. `playwright` is a pinned devDep; the Chromium browser it drives is the environment prereq (`pnpm exec playwright install chromium`). The drivers assert through PASS/FAIL gates and now run in CI as a regression suite via the separate `Harness` workflow (`scripts/test-harness.sh` - both editors' harnesses); they stay excluded from `pnpm test`/`pnpm test:all`, which run browserless. See `binary-editor/test/harness/README.md` for the harness, and **`docs/binary-editor-ui-guidelines.md`** for the project's UI conventions and the review brief - it indexes the co-located writer-facing rules (`client/src/binary-editor/webview/AGENTS.md` for the render layer, `binary/src/AGENTS.md` for the layout schema), which auto-load when you edit those dirs. Review rendered screenshots against the guidelines.
2222
- **To run the whole extension in a real VS Code instance, use `pnpm dev:web`.** It starts [code-server](https://github.com/coder/code-server) (VS Code in a browser) with this repo loaded as an unpacked extension, so the LSP server and binary custom editor actually run - the screenshot harness above only renders the webview in isolation. Default serves plain HTTP on `0.0.0.0:8080` (override with `CODE_SERVER_PORT`/`CODE_SERVER_HOST`); it bootstraps a pinned code-server into the gitignored `.dev/` on first run, then builds via `build:dev`. The command runs a long-lived server in the foreground - background it (or run it as a separate task) if you need the shell, bind to the port your environment exposes, and confirm it is up before reporting a URL. The binary editor is a webview, so the browser must reach it over a secure context (`http://localhost` via a port-forward, or a trusted cert) or the editor renders blank. Full details and configuration: **`scripts/dev-web.md`**.
2323
- **Use `pnpm` exclusively. Never use `npx`.** This is a hard requirement. Every time you reach for `npx`, `npm`, or any npm-series command, stop and use `pnpm exec` instead. Example: `pnpm exec playwright` not `npx playwright`. This applies to all contexts including one-off commands, scripts, subagents, and delegated tasks. If pnpm is not available in a context, install it first or use the workspace package.json scripts.
2424
- **Milestone close-out commands (scope to what you touched).** Binary changes -> `pnpm exec vitest run --config binary/vitest.config.ts` (run from the repo root, not `cd binary` - several tests resolve `external/` and `client/testFixture/` fixtures relative to cwd); webview/Svelte changes -> the client tests plus the binary-editor render harness. Reserve `pnpm build:all` + `pnpm test:all` (~9 min) for changes that span subsystems or touch shared build infra, grammars, transpilers, or the server.
@@ -120,6 +120,8 @@ cd grammars/weidu-tp2 && pnpm test # or any grammars/*/
120120

121121
```
122122

123+
**Testing against real external files.** The `external/` mod trees are gitignored but REPRODUCIBLE - `pnpm test:external` (via `scripts/reset-external.sh` + `scripts/external-repos-lib.sh`) clones/checks them out at pinned refs. So real-corpus coverage belongs in a committed test, not a throwaway: tests that exercise real external files live under `server/test/integration/**` (run by `pnpm test:integration`, config `server/vitest.integration.config.ts`), using `test/integration/test-helpers.ts` (`FALLOUT_FIXTURES` = `external/fallout`, `IE_FIXTURES` = `external/infinity-engine`, `loadFixture`/`loadFixtures`). Gate a corpus sweep with `describe.skipIf(files.length === 0)` so it skips cleanly when the corpus is not checked out. Before placing any real-file test, read a sibling there (`integration/weidu-d.test.ts`, `fallout-ssl/rename.test.ts`) for the fixture/init conventions. Do NOT commit copies of gitignored `external/` files as fixtures, and do NOT hand-run a one-off script where this suite is the home.
124+
123125
## Publishing & Release
124126

125127
Releases are tag-driven via GitHub Actions. `docs/releasing.md` is the canonical reference: the tag scheme (which tag form triggers which workflow), and the per-stream release procedures for the extension, the three libraries, and the reusable Action - including the root/`server` version-identity invariant. The server and VSIX bundle their `@bgforge/*` libraries rather than depending on them at runtime, so the extension and the libraries release in any order. Consult it before cutting any release. See `docs/architecture.md` for packaging mechanics.
@@ -132,6 +134,7 @@ Releases are tag-driven via GitHub Actions. `docs/releasing.md` is the canonical
132134
- `ts-morph` is pinned to `^27.0.2` across the server runtime and all transpiler subpackages so the bundled TypeScript matches the project's own `typescript ^5.9.3` pin. ts-morph bundles its TypeScript dependency rather than treating it as a peer, so the ts-morph version effectively chooses the TS compiler that runs against transpiler ASTs regardless of the workspace `typescript` pin. ts-morph 28 bundles TS 6.0 - TS 6 is on a 6.0.x line with no 6.1 published, fails the standard "matured target major" upgrade rule, and ts-morph 28 is therefore deferred until TS 6.1 ships. ts-morph 27.0.x is technically also on a 0-minor line, but the TS 5.9 alignment is the deciding factor. Bump in lockstep with the project's `typescript` pin: when `typescript` moves to 6.x, ts-morph moves to whichever ts-morph major bundles that TS line.
133135
- `@types/node` tracks the latest LTS Node major, currently `^24.x`. The extension targets LTS Node only (minimum supported is 20), so do not bump `@types/node` to odd-numbered "Current" majors (e.g. 25.x) - that would expose type definitions for APIs not present at the supported runtime floor. Move it forward only when a new even-numbered Node release reaches LTS.
134136
- `ini` (runtime dep of `@bgforge/format`) is held at `^6.x`; `7.0.0` is a major with potential parse/stringify behavior changes that need a changelog review before adoption.
137+
- `playwright` (devDep) is pinned to an EXACT version (no caret) because the webview harnesses launch a browser from Playwright's version-keyed cache: the devbox and the `Harness` CI job download that browser via `playwright install`, and a caret drift to a version whose browser revision is not cached would break the harness run until a re-download. Bump the pin and re-run `playwright install` together. Its browser postinstall is skipped by pnpm's build-script gate (not in `onlyBuiltDependencies`), so a plain `pnpm install` never pulls ~150MB of browser - the harness paths install Chromium explicitly.
135138
- `sslc-emscripten-noderawfs` (the built-in SSL compiler WASM, `server/package.json`) is an HTTPS GitHub-release tarball, and its `pnpm-lock.yaml` entry carries a hand-maintained `integrity` field. pnpm 11.4+ fails closed (`ERR_PNPM_MISSING_TARBALL_INTEGRITY`) on any tarball lockfile entry that lacks integrity, and URL-tarball resolvers only learn the hash on download - so when the package is reused from the store, the field is never emitted. Practical rule: **evolve the lockfile incrementally** (`pnpm install` / `pnpm update`); pnpm 11.5.0+ preserves the existing integrity across those, so `pnpm update` keeps working. Do **not** `rm pnpm-lock.yaml` to regenerate from scratch - that drops the field and breaks `pnpm update` until it is re-added. The release asset is immutable, so the hash is stable; if a full regen is ever unavoidable, restore the line `resolution: {integrity: sha512-oQEOCRLmCodC1MlcIVAqGyTb3aWLpJ7c9tQm4CF8hrf9feZoxcIvSfwKpkcq0FykigqVGdpFpkbwQBS8yqo28Q==, tarball: <url>}`. The same dep is also listed under `minimumReleaseAgeExclude` in `pnpm-workspace.yaml` (a non-registry tarball has no publish timestamp for the 24h maturity window to check).
136139

137140
## Architecture

binary-editor/test/harness/README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,14 @@ It is type-checked via `test/harness/tsconfig.json`, which includes the DOM lib
7474

7575
## Prerequisites
7676

77-
- **Playwright + a Chromium browser** must be available on `PATH` in whatever environment runs the harness.
78-
Install Playwright and its browsers globally:
77+
- **Playwright** is a pinned devDependency (`pnpm install` provides it), so `pnpm exec tsx <driver>` resolves
78+
`import { chromium } from "playwright"` with no global install. Its browser postinstall is skipped by pnpm's
79+
build-script gate, so install the one browser the drivers launch:
7980

8081
```
81-
npm install -g playwright
82-
playwright install chromium
82+
pnpm exec playwright install chromium
8383
```
8484

85-
Playwright is intentionally NOT listed in any `package.json` in this repo. It is an environment
86-
prerequisite, not a repo dependency.
87-
8885
- **Node 20+** (matched to the project's minimum supported runtime).
8986

9087
- **`tsx`** - available via the repo's dev dependencies (`pnpm exec tsx ...`).
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { mkdirSync } from "node:fs";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
5+
// Repo-level tmp/ for harness screenshots, keeping the source tree clean (project convention).
6+
// This module lives in binary-editor/test/harness/, so the repo root is three levels up.
7+
const here = path.dirname(fileURLToPath(import.meta.url));
8+
export const SHOT_DIR = path.resolve(here, "../../../tmp");
9+
mkdirSync(SHOT_DIR, { recursive: true });
10+
11+
/** Absolute path for a harness screenshot, under the repo tmp/ dir. */
12+
export function shotPath(name: string): string {
13+
return path.join(SHOT_DIR, name);
14+
}

binary-editor/test/harness/render-clip-sweep.mts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ async function runFormat(browser: Browser, label: string, uri: string, bytes: Ui
9999
});
100100
await page.goto("file://" + path.join(here, "app.html"));
101101
await page.waitForSelector(".layout-root", { timeout: 5000 });
102-
await page.waitForTimeout(200);
103102

104103
const found: ClipViolation[] = [];
105104
// Check whatever a freshly-opened detail/list selection renders too: select the first list row if one is
@@ -109,7 +108,11 @@ async function runFormat(browser: Browser, label: string, uri: string, bytes: Ui
109108
const firstRow = page.locator(".master .vlist .vrow").first();
110109
if (await firstRow.count()) {
111110
await firstRow.click().catch(() => undefined);
112-
await page.waitForTimeout(150);
111+
await page
112+
.waitForFunction(() => document.querySelector(".detail .layout-root .field") !== null, undefined, {
113+
timeout: 5000,
114+
})
115+
.catch(() => undefined);
113116
found.push(...(await collectClipViolations(page, ctx + " (detail)")));
114117
}
115118
};
@@ -123,7 +126,18 @@ async function runFormat(browser: Browser, label: string, uri: string, bytes: Ui
123126
const tab = tabs.nth(i);
124127
const name = ((await tab.textContent()) ?? `tab${i}`).replace(/\s*\(\d.*\)\s*$/, "").trim();
125128
await tab.click();
126-
await page.waitForTimeout(200);
129+
await page
130+
.waitForFunction(
131+
(expectedLabel) => {
132+
const active = document.querySelector(
133+
'.bb-tabs.primary button[role="tab"][aria-selected="true"]',
134+
);
135+
return !!active && (active.textContent ?? "").includes(expectedLabel);
136+
},
137+
name,
138+
{ timeout: 5000 },
139+
)
140+
.catch(() => undefined);
127141
await sweepCurrentView(`${label} > ${name}`);
128142
}
129143
}

0 commit comments

Comments
 (0)