chore: merge v2/main into main for the v2.7.0 release
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
commit
cf3023bdf8
5 changed files with 74 additions and 71 deletions
|
|
@ -207,6 +207,8 @@ it — the split is the design.
|
||||||
|
|
||||||
`npm run pack:verify` builds, packs the real tarball, installs it into a clean
|
`npm run pack:verify` builds, packs the real tarball, installs it into a clean
|
||||||
throwaway consumer, and drives the installed `mcp-inspector` bin end to end. It
|
throwaway consumer, and drives the installed `mcp-inspector` bin end to end. It
|
||||||
needs **network access**, so it is a local/release check and is in neither
|
needs **network access**, so it is a local/release check: it is **not** a
|
||||||
`local:gate` nor CI. Run it when touching packaging (the `files` allowlist,
|
`local:gate` stage, and CI runs it only in the `publish` job on a published
|
||||||
a bundler `external` list, anything read from disk at runtime).
|
GitHub Release — never on a push or PR. Run it when touching packaging (the
|
||||||
|
`files` allowlist, a bundler `external` list, anything read from disk at
|
||||||
|
runtime), and as its own step when cutting a release (the `release` skill).
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ other.
|
||||||
| Branch | `v2/chore/<ISSUE>-bump-<X-Y-Z>`, cut from `origin/v2/main` | the milestone-merge branch, cut from `origin/main` |
|
| Branch | `v2/chore/<ISSUE>-bump-<X-Y-Z>`, cut from `origin/v2/main` | the milestone-merge branch, cut from `origin/main` |
|
||||||
| Base | **`v2/main`** | **`main`** |
|
| Base | **`v2/main`** | **`main`** |
|
||||||
| Carries | the `npm audit` report, **any fixes the audit forces**, and the **version bump** — all three, one PR | the milestone's work, arriving whole from `v2/main`. **No commits of its own.** |
|
| Carries | the `npm audit` report, **any fixes the audit forces**, and the **version bump** — all three, one PR | the milestone's work, arriving whole from `v2/main`. **No commits of its own.** |
|
||||||
| Verified by | `npm run local:gate` | `npm run local:gate` **plus** a hand-driven smoke of every contribution in the milestone, from the **production build**, written up as a **ledger artifact** |
|
| Verified by | `npm run local:gate` | `npm run local:gate`, **plus** `npm run pack:verify` (not a gate stage), **plus** a hand-driven smoke of every contribution in the milestone, from the **production build**, written up as a **ledger artifact** |
|
||||||
| Merged when | reviewed and green | the ledger is reviewed by the maintainers and clean |
|
| Merged when | reviewed and green | the ledger is reviewed by the maintainers and clean |
|
||||||
|
|
||||||
Then, and only then, a maintainer tags and publishes the **GitHub Release**
|
Then, and only then, a maintainer tags and publishes the **GitHub Release**
|
||||||
|
|
@ -143,9 +143,16 @@ Then drive it. Work from a **dedicated worktree** with its own full
|
||||||
`npm install` (a symlinked `node_modules` passes lint and tests and then fails
|
`npm install` (a symlinked `node_modules` passes lint and tests and then fails
|
||||||
every story file), run `npm run local:gate` there, and exercise the app from the
|
every story file), run `npm run local:gate` there, and exercise the app from the
|
||||||
**production build** — the packaged bin and the built bundles, not `vite dev`.
|
**production build** — the packaged bin and the built bundles, not `vite dev`.
|
||||||
The `local-dev`, `test-servers` and `pre-push-gate` skills cover the mechanics;
|
The `local-dev`, `test-servers` and `pre-push-gate` skills cover the mechanics.
|
||||||
`pack:verify` inside the gate is what proves the tarball a consumer installs
|
|
||||||
actually resolves.
|
**Then run `npm run pack:verify` there as its own step.** It is what proves the
|
||||||
|
tarball a consumer installs actually resolves, and ⚠️ **`local:gate` does not
|
||||||
|
run it** — `local:gate:stages` has no packaging stage, and a green gate says
|
||||||
|
nothing about the published tarball (#2380). CI runs it only in the `publish`
|
||||||
|
job, which fires on the published GitHub Release — after the tag exists — so
|
||||||
|
skipping it here means the first signal of a broken package arrives too late to
|
||||||
|
stop the release. It needs network access; record its result (tarball size and
|
||||||
|
the `pack:verify OK` line) for the ledger below.
|
||||||
|
|
||||||
**Every contribution closed in the milestone gets driven, not read.** The bar is
|
**Every contribution closed in the milestone gets driven, not read.** The bar is
|
||||||
observed behavior from the running app — a rendered panel, a status attribute, a
|
observed behavior from the running app — a rendered panel, a status attribute, a
|
||||||
|
|
@ -165,11 +172,13 @@ and link it from PR 2. Shape it like the
|
||||||
standfirst saying what tree was tested and that its hash matches
|
standfirst saying what tree was tested and that its hash matches
|
||||||
`origin/v2/main`, plus whether the milestone payload is complete (the only
|
`origin/v2/main`, plus whether the milestone payload is complete (the only
|
||||||
issue left open should be the merge itself).
|
issue left open should be the merge itself).
|
||||||
- **Verdict band** — `local:gate` result, milestone issues verified as `N / N`,
|
- **Verdict band** — `local:gate` and `pack:verify` results, milestone issues
|
||||||
distinct test count, regressions found.
|
verified as `N / N`, distinct test count, regressions found.
|
||||||
- **The automated gate** — one cell per stage with its number (file counts,
|
- **The automated gate** — one cell per `local:gate` stage with its number
|
||||||
test counts, smoke count, `pack:verify` size), and a note on what is new this
|
(file counts, test counts, smoke count), and a note on what is new this
|
||||||
milestone.
|
milestone.
|
||||||
|
- **The packaging check** — `pack:verify` in its own cell, apart from the gate
|
||||||
|
stages because it is not one of them: its result and the tarball size.
|
||||||
- **One section per theme**, each a table of *Issue · What was driven ·
|
- **One section per theme**, each a table of *Issue · What was driven ·
|
||||||
Observed · Status*. One row per closed issue, issue-linked, with the actual
|
Observed · Status*. One row per closed issue, issue-linked, with the actual
|
||||||
output in the Observed cell.
|
output in the Observed cell.
|
||||||
|
|
|
||||||
8
clients/web/package-lock.json
generated
8
clients/web/package-lock.json
generated
|
|
@ -46,7 +46,7 @@
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"eslint-plugin-storybook": "^10.5.5",
|
"eslint-plugin-storybook": "^10.5.5",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.11.2",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"storybook": "^10.5.5",
|
"storybook": "^10.5.5",
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
|
|
@ -5264,9 +5264,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/happy-dom": {
|
"node_modules/happy-dom": {
|
||||||
"version": "20.11.1",
|
"version": "20.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz",
|
||||||
"integrity": "sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==",
|
"integrity": "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"eslint-plugin-storybook": "^10.5.5",
|
"eslint-plugin-storybook": "^10.5.5",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.11.2",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"storybook": "^10.5.5",
|
"storybook": "^10.5.5",
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createRef, StrictMode } from "react";
|
import { createRef, StrictMode } from "react";
|
||||||
import { act, waitFor } from "@testing-library/react";
|
import { act } from "@testing-library/react";
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import type { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge";
|
import type { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||||
import type { CallToolResult, Tool } from "@modelcontextprotocol/client";
|
import type { CallToolResult, Tool } from "@modelcontextprotocol/client";
|
||||||
|
|
@ -79,6 +79,34 @@ async function flushAsync(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runs `mutate` and resolves once every MutationObserver that was already
|
||||||
|
// watching `data-mantine-color-scheme` on `documentElement` has had its
|
||||||
|
// callback delivered — which is what the AppRenderer theme push rides on.
|
||||||
|
//
|
||||||
|
// This is an awaited condition rather than a wall-clock budget, per #2384.
|
||||||
|
// A probe observer registered *after* the component's is notified after it:
|
||||||
|
// observers are queued in registration order and each delivery is an ordinary
|
||||||
|
// microtask, so by the time the probe's callback runs the component's has
|
||||||
|
// already run and the assertion below is settled. No polling and no timeout is
|
||||||
|
// involved, so nothing here is load-sensitive.
|
||||||
|
async function flipThemeAttributeAndAwaitObservers(
|
||||||
|
value: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const probe = new MutationObserver(() => {
|
||||||
|
probe.disconnect();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
probe.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["data-mantine-color-scheme"],
|
||||||
|
});
|
||||||
|
document.documentElement.setAttribute("data-mantine-color-scheme", value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("AppRenderer", () => {
|
describe("AppRenderer", () => {
|
||||||
it("renders an iframe with the sandbox path and tool title", () => {
|
it("renders an iframe with the sandbox path and tool title", () => {
|
||||||
const bridge = createMockBridge();
|
const bridge = createMockBridge();
|
||||||
|
|
@ -538,47 +566,17 @@ describe("AppRenderer", () => {
|
||||||
// flip we trigger below.
|
// flip we trigger below.
|
||||||
bridge.sendHostContextChange.mockClear();
|
bridge.sendHostContextChange.mockClear();
|
||||||
|
|
||||||
await act(async () => {
|
await flipThemeAttributeAndAwaitObservers("dark");
|
||||||
document.documentElement.setAttribute(
|
|
||||||
"data-mantine-color-scheme",
|
expect(bridge.sendHostContextChange).toHaveBeenCalledWith(
|
||||||
"dark",
|
expect.objectContaining({
|
||||||
);
|
theme: "dark",
|
||||||
// MutationObserver callbacks are delivered on a microtask.
|
styles: expect.objectContaining({
|
||||||
await Promise.resolve();
|
variables: expect.objectContaining({
|
||||||
});
|
"--color-background-primary": "#101113",
|
||||||
// `waitFor`, not a bare assertion after the single microtask above: how
|
|
||||||
// many ticks happy-dom takes to deliver the MutationObserver callback is
|
|
||||||
// load-dependent, so under a full parallel run the observer can still be
|
|
||||||
// pending here. Observed failing exactly once that way, with 0 calls.
|
|
||||||
//
|
|
||||||
// The default 1000ms then turned out not to be enough either — it failed
|
|
||||||
// twice in a row in `npm run local:gate` (at ~1021ms, still 0 calls) while
|
|
||||||
// passing every time this file runs alone, which is the signature of a
|
|
||||||
// budget that is too tight rather than of a broken observer. Raised
|
|
||||||
// rather than retried: a `waitFor` that is generous costs nothing on the
|
|
||||||
// passing path, since it returns as soon as the assertion holds.
|
|
||||||
//
|
|
||||||
// #2323 briefly deleted this argument as one of its "a budget inside an
|
|
||||||
// equal budget can never be observed" cases, which it was: 5000 inside a
|
|
||||||
// 5000ms test could never win. That is fixed from the other end now — the
|
|
||||||
// enclosing budget is 15000 — so the raise is observable again and has to
|
|
||||||
// stay, because the value it beats is a measured failure rather than a
|
|
||||||
// default. It does NOT ride on `asyncUtilTimeout`, which is pinned at
|
|
||||||
// 1000 precisely because raising it globally was measured worse
|
|
||||||
// (Copilot).
|
|
||||||
await waitFor(
|
|
||||||
() =>
|
|
||||||
expect(bridge.sendHostContextChange).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
theme: "dark",
|
|
||||||
styles: expect.objectContaining({
|
|
||||||
variables: expect.objectContaining({
|
|
||||||
"--color-background-primary": "#101113",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
),
|
}),
|
||||||
{ timeout: 5000 },
|
}),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
getComputedStyleSpy.mockRestore();
|
getComputedStyleSpy.mockRestore();
|
||||||
|
|
@ -599,13 +597,10 @@ describe("AppRenderer", () => {
|
||||||
// No `initialized` emitted — the theme observer is gated, like the
|
// No `initialized` emitted — the theme observer is gated, like the
|
||||||
// container and displayMode pushes, so a pre-handshake flip is dropped.
|
// container and displayMode pushes, so a pre-handshake flip is dropped.
|
||||||
bridge.sendHostContextChange.mockClear();
|
bridge.sendHostContextChange.mockClear();
|
||||||
await act(async () => {
|
// Same awaited-delivery helper as the positive case: it is what makes this
|
||||||
document.documentElement.setAttribute(
|
// "not called" a real gate rather than a vacuous pass on a flip that was
|
||||||
"data-mantine-color-scheme",
|
// never delivered at all.
|
||||||
"dark",
|
await flipThemeAttributeAndAwaitObservers("dark");
|
||||||
);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
expect(bridge.sendHostContextChange).not.toHaveBeenCalled();
|
expect(bridge.sendHostContextChange).not.toHaveBeenCalled();
|
||||||
document.documentElement.removeAttribute("data-mantine-color-scheme");
|
document.documentElement.removeAttribute("data-mantine-color-scheme");
|
||||||
});
|
});
|
||||||
|
|
@ -627,13 +622,10 @@ describe("AppRenderer", () => {
|
||||||
});
|
});
|
||||||
bridge.sendHostContextChange.mockClear();
|
bridge.sendHostContextChange.mockClear();
|
||||||
|
|
||||||
await act(async () => {
|
// Same awaited-delivery helper as the positive case: it is what makes this
|
||||||
document.documentElement.setAttribute(
|
// "not called" a real gate rather than a vacuous pass on a flip that was
|
||||||
"data-mantine-color-scheme",
|
// never delivered at all.
|
||||||
"dark",
|
await flipThemeAttributeAndAwaitObservers("dark");
|
||||||
);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
expect(bridge.sendHostContextChange).not.toHaveBeenCalled();
|
expect(bridge.sendHostContextChange).not.toHaveBeenCalled();
|
||||||
document.documentElement.removeAttribute("data-mantine-color-scheme");
|
document.documentElement.removeAttribute("data-mantine-color-scheme");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue