open-design/apps/web/tests/components/pluginFolderActions.test.ts
lefarcen 387dc83b27
Some checks failed
ci / Detect CI change scopes (push) Failing after 2s
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 1s
ci / Preflight (push) Has been skipped
ci / Core package tests (push) Has been skipped
ci / Tools workspace tests (push) Has been skipped
ci / Daemon workspace tests (push) Has been skipped
ci / Web workspace tests (push) Has been skipped
ci / E2E vitest (push) Has been skipped
ci / Playwright critical (push) Has been skipped
ci / Build workspaces (push) Has been skipped
ci / App workspace tests (push) Has been skipped
ci / Validate workspace (push) Failing after 0s
fix(plugins): wire Open Design "Open Design PR" button end-to-end (#2182)
Two-part fix for the Plugin folder card's "Open Design PR" button. The
flow was broken end-to-end: the CLI emitted a 404 URL, and the agent
prompt under-specified the contribution steps so the agent stalled
mid-turn or fell back to the legacy issue-URL path.

Catalog target — `apps/daemon/src/plugins/publish.ts`:

`buildPublishLink({ catalog: 'open-design' })` hardcoded a submission URL
at `github.com/open-design/plugin-registry`, the dedicated registry repo
proposed in docs/plans/plugin-registry.md §1.2. That repo doesn't exist
yet (P3.1 notes "creating the external GitHub repo is an operational
launch step, not a code blocker"), so every generated URL 404'd. Retarget
the catalog at the live `nexu-io/open-design` monorepo and update the
target-path hint in the PR body to `plugins/community/<plugin-name>/`
(the actual layout under main). Plan §1.2 stays the long-term goal — see
the code comment.

Also updates the matching `od plugin yank` issue URL in cli.ts.

Agent prompt — `apps/web/src/components/design-files/pluginFolderActions.ts`:

The `contribute` action prompt only said "use the supported `od plugin
publish` Open Design registry flow", which produces an issue URL (the
legacy path) and left the agent to invent the remaining steps. The agent
ended up calling `AskUserQuestion` mid-turn waiting for input that the
DesignFilesPanel buttons couldn't satisfy, then stalled for 600s.

Rewrite the `contribute` prompt to drive the full PR flow via raw `gh`
commands:
  1. preflight `gh --version` / `gh auth status` (detect-and-instruct on
     missing CLI, never auto-install anything)
  2. read manifest, resolve author login
  3. `gh repo fork nexu-io/open-design --remote=false`
  4. clone fork, branch, cp plugin into `plugins/community/<name>/`,
     commit/push using author's git identity (not a hardcoded bot)
  5. `gh pr create ... --web` so the author reviews and clicks Create

Hard bans on `AskUserQuestion`, retry-on-failure, and the legacy
`od plugin publish --to open-design` CLI keep the turn fire-and-forget.
The `install` / `publish` action prompts are unchanged.

Tests:

* `apps/daemon/tests/plugins-publish.test.ts` — assert URL host +
  catalogLabel match `nexu-io/open-design`, body contains the new path
  hint.
* `apps/web/tests/components/pluginFolderActions.test.ts` (new) — lock
  the contribute prompt's command surface, the `--web` review window,
  the `AskUserQuestion` ban, the install-tool ban, and folder-path
  interpolation. Nine assertions covering the prompt contract without
  coupling to exact wording.

Validation:

* `pnpm --filter @open-design/daemon exec vitest run tests/plugins-publish.test.ts`
  → 11/11 passed
* `pnpm --filter @open-design/web exec vitest run tests/components/pluginFolderActions.test.ts`
  → 9/9 passed
* `pnpm --filter @open-design/web typecheck` clean
* E2E: button click in DesignFilesPanel under namespace=e2e-pr-test ran
  `gh --version` → fork → clone → branch → commit/push → ended at
  `gh pr create --web` opening the GitHub PR draft in browser. No
  AskUserQuestion stall this time.

Doesn't touch: `5f71968f` server-side endpoints (`/contribute-open-design`
+ `/publish-github` still in main as dead code — separate cleanup),
plan/spec docs (per maintainer call to keep the dedicated-repo as the
long-term goal), or `registry-backends.test.ts` (plan terminal-state
fixture; no production caller).
2026-05-19 15:26:59 +08:00

83 lines
3.8 KiB
TypeScript

// Contract test for the prompts the plugin-folder card buttons send to the
// agent. `install` / `publish` use the simple shared template; `contribute`
// is the PR-flow prompt that drives `gh repo fork → branch → commit →
// gh pr create --web` end-to-end. The tests below lock the *shape* of each
// prompt (keywords + folder interpolation) without coupling to exact wording,
// so prose tweaks don't break the suite but accidental removal of a critical
// step would.
import { describe, expect, it } from 'vitest';
import { buildPluginFolderAgentActionPrompt } from '../../src/components/design-files/pluginFolderActions';
const FOLDER = 'generated-plugin';
describe('buildPluginFolderAgentActionPrompt', () => {
describe('install', () => {
it('mentions the folder path and the supported install CLI', () => {
const prompt = buildPluginFolderAgentActionPrompt(FOLDER, 'install');
expect(prompt).toContain(`Plugin folder: \`${FOLDER}\``);
expect(prompt).toContain('od plugin install --source');
});
});
describe('publish', () => {
it('mentions the folder path and the supported publish CLI', () => {
const prompt = buildPluginFolderAgentActionPrompt(FOLDER, 'publish');
expect(prompt).toContain(`Plugin folder: \`${FOLDER}\``);
expect(prompt).toContain('od plugin publish');
});
});
describe('contribute (PR-based flow)', () => {
const prompt = buildPluginFolderAgentActionPrompt(FOLDER, 'contribute');
it('targets the nexu-io/open-design community catalog', () => {
expect(prompt).toContain('nexu-io/open-design');
expect(prompt).toContain('plugins/community/<name>/');
});
it('drives the full PR flow via gh, not via the issue-URL CLI', () => {
// The agent must drive raw gh commands rather than fall back to the
// legacy `od plugin publish --to open-design` issue-URL launcher.
expect(prompt).toContain('gh repo fork nexu-io/open-design');
expect(prompt).toContain('gh repo clone');
expect(prompt).toContain('git checkout -b plugin/');
expect(prompt).toContain('gh pr create');
// The legacy CLI is named in the prompt only as part of an explicit
// ban ("Do NOT call the legacy `od plugin publish --to open-design`")
// — verify the ban is in place, not the bare command.
expect(prompt).toMatch(/do not call the legacy `od plugin publish --to open-design`/i);
});
it('uses --web so the author confirms the PR in browser', () => {
// The "author keeps the final review click" invariant — preserved from
// 45f52d71's "We never POST anywhere" principle.
expect(prompt).toContain('--web');
expect(prompt).toMatch(/do not auto-submit/i);
});
it('hard-bans AskUserQuestion to avoid 600s mid-turn stalls', () => {
// Regression guard for the stall we observed during e2e: agent paused
// mid-turn on an AskUserQuestion tool waiting for a host answer the
// user never sent (they clicked the plugin-folder card instead).
expect(prompt).toContain('AskUserQuestion');
expect(prompt).toMatch(/do not call the `AskUserQuestion` tool|fire-and-forget/i);
});
it('forbids the agent from installing tools or retrying failures', () => {
expect(prompt).toMatch(/do not try to install/i);
expect(prompt).toMatch(/do not retry/i);
});
it('interpolates the actual folder path into manifest and copy steps', () => {
// Sanity check that template-string interpolation didn't regress into
// literal `${folderPath}` substrings (we already shipped that bug once).
expect(prompt).toContain(`${FOLDER}/open-design.json`);
expect(prompt).not.toContain('${folderPath}');
});
it('ends by handing the PR URL back to chat', () => {
expect(prompt).toMatch(/PR URL|pull\/new|paste it into chat/);
});
});
});