From b6d35744ef686f0fcae87d0059c84c9cccc6c6b6 Mon Sep 17 00:00:00 2001 From: Miya Date: Mon, 21 Sep 2026 18:09:13 +0200 Subject: [PATCH 1/2] fix(surface): ship Babysitter GitHub actions Session-Id: 01a0c4a6-dd65-7ce1-a90e-de1b0b4e86c3 Session-Id: 01a0c4a6-dd65-7ce1-a90e-de1b0b4e86c3 --- catalog/plugins.json | 2 +- docs/SURFACE.md | 8 +-- examples/babysitter/README.md | 9 +-- packages/sdk/package-lock.json | 71 ++----------------- packages/sdk/package.json | 2 +- .../sdk/tests/flow-extension-compose.test.ts | 24 +++---- packages/sdk/tests/plugin-extension.test.ts | 9 ++- packages/sdk/tests/preflight.test.ts | 2 +- packages/surface/src/triggers/PROVIDERS.md | 4 +- packages/surface/src/triggers/github.ts | 9 +++ packages/surface/src/triggers/index.ts | 2 +- .../tests/triggers-github-events.test.ts | 20 ++++++ .../plugins/extension-babysitter/README.md | 11 ++- .../extension-babysitter/babysitter.flow.ts | 3 + .../extension-babysitter/flows-plugin.json | 5 +- 15 files changed, 72 insertions(+), 109 deletions(-) diff --git a/catalog/plugins.json b/catalog/plugins.json index d7323b91e..77243f12c 100644 --- a/catalog/plugins.json +++ b/catalog/plugins.json @@ -3,7 +3,7 @@ "plugins": [ { "name": "babysitter", - "description": "Live-state PR babysitter: parallel review lenses, deterministic reconciliation, exact-head merge gate. Fail-closed: GitHub pull_request.ready_for_review, labeled, and unlabeled are not in the surface registry, so a manifest that declares them is refused plugin_event_unroutable until the relayfile adapter catalog grows.", + "description": "Live-state PR babysitter: parallel review lenses, deterministic reconciliation, and an exact-head merge gate across the full eleven-event subscription contract.", "source": { "owner": "AgentWorkforce", "repo": "flows", "path": "examples/babysitter" }, "ref": "05c3dff138883322e80cb793b1f5a097ad510572", "digest": "ae6af3335eb6d4e54559327acc1465419244b47911d8ff356850b61f6228d862", diff --git a/docs/SURFACE.md b/docs/SURFACE.md index 8b98dad6f..d2385f550 100644 --- a/docs/SURFACE.md +++ b/docs/SURFACE.md @@ -649,11 +649,9 @@ execute nowhere (#301); what composition changes today is the declared trigger set that `flows check`, requirements, and future dispatch read. GitHub `pull_request.ready_for_review`, `pull_request.labeled`, and -`pull_request.unlabeled` are **not** in the surface registry. The registry is -generated from the pinned relayfile adapter mappings (`scripts/generate-triggers.mjs`); -this repo cannot add those actions without an adapter-package change. A -Babysitter manifest that declares them is refused `plugin_event_unroutable` -until that upstream catalog grows. +`pull_request.unlabeled` are in the generated surface registry through the +pinned relayfile adapter catalog. Babysitter declares all three and installs +without narrowing its eleven-subscription contract. ## 4. Build: the immutable bundle diff --git a/examples/babysitter/README.md b/examples/babysitter/README.md index 695a52c0f..0d92773df 100644 --- a/examples/babysitter/README.md +++ b/examples/babysitter/README.md @@ -75,12 +75,9 @@ flows add github:AgentWorkforce/flows@#examples/babysitter The manifest's `triggers` are the eleven-subscription contract in `subscriptions.ts`, family by family, and `tests/manifest.test.ts` pins the -two lists to each other. Three of those actions — `pull_request.ready_for_review`, -`labeled`, `unlabeled` — are not yet in the surface event registry -(`providerEventTypes`), so the flows CLI refuses this manifest with -`plugin_event_unroutable` until that registry change lands; the manifest -deliberately declares the full contract rather than the routable subset, so -that an install grants exactly the events the flow registers. +two lists to each other. The Surface registry includes all eleven events, +including `pull_request.ready_for_review`, `labeled`, and `unlabeled`, so an +install grants exactly the events the flow registers. ## Operator input diff --git a/packages/sdk/package-lock.json b/packages/sdk/package-lock.json index 6ab9fa638..2c3b484fa 100644 --- a/packages/sdk/package-lock.json +++ b/packages/sdk/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", - "@relayfile/adapter-core": "0.5.26", + "@relayfile/adapter-core": "0.6.2", "@relayfile/relay-helpers": "0.4.12", "@relayflows/surface": "2.0.25", "@types/js-yaml": "^4.0.9", @@ -533,9 +533,9 @@ } }, "node_modules/@relayfile/adapter-core": { - "version": "0.5.26", - "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.5.26.tgz", - "integrity": "sha512-PhRVYb2FxZ0rTqqmq0WMBDVlTb/gr63V2TFKCyNCiMZY7pTEPVYGmx0gzTVSALiPqCtKG6xlekpn64fmOjjagQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.6.2.tgz", + "integrity": "sha512-fVBwiK1W0af4wQ4w/Jr7GA1VBH1qz9Qw1lm90MrOJN0zQfVq1YRwRu9fPk4rp50TQDveLBKE0+MoaLMUmqr5Jg==", "license": "Apache-2.0", "dependencies": { "@scalar/postman-to-openapi": "^0.6.0", @@ -568,27 +568,6 @@ "@relayfile/sdk": ">=0.6.0 <1" } }, - "node_modules/@relayfile/adapter-linear/node_modules/@relayfile/adapter-core": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.6.2.tgz", - "integrity": "sha512-fVBwiK1W0af4wQ4w/Jr7GA1VBH1qz9Qw1lm90MrOJN0zQfVq1YRwRu9fPk4rp50TQDveLBKE0+MoaLMUmqr5Jg==", - "license": "Apache-2.0", - "dependencies": { - "@scalar/postman-to-openapi": "^0.6.0", - "cheerio": "^1.2.0", - "minimatch": "^10.0.3", - "yaml": "^2.8.1" - }, - "bin": { - "adapter-core": "dist/src/cli.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@relayfile/sdk": ">=0.6.0 <1" - } - }, "node_modules/@relayfile/adapter-reddit": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/@relayfile/adapter-reddit/-/adapter-reddit-0.2.10.tgz", @@ -604,27 +583,6 @@ "@relayfile/sdk": ">=0.6.0 <1" } }, - "node_modules/@relayfile/adapter-reddit/node_modules/@relayfile/adapter-core": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.6.2.tgz", - "integrity": "sha512-fVBwiK1W0af4wQ4w/Jr7GA1VBH1qz9Qw1lm90MrOJN0zQfVq1YRwRu9fPk4rp50TQDveLBKE0+MoaLMUmqr5Jg==", - "license": "Apache-2.0", - "dependencies": { - "@scalar/postman-to-openapi": "^0.6.0", - "cheerio": "^1.2.0", - "minimatch": "^10.0.3", - "yaml": "^2.8.1" - }, - "bin": { - "adapter-core": "dist/src/cli.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@relayfile/sdk": ">=0.6.0 <1" - } - }, "node_modules/@relayfile/cli-darwin-arm64": { "version": "0.10.69", "resolved": "https://registry.npmjs.org/@relayfile/cli-darwin-arm64/-/cli-darwin-arm64-0.10.69.tgz", @@ -786,27 +744,6 @@ "@relayfile/adapter-reddit": "^0.2.10" } }, - "node_modules/@relayfile/relay-helpers/node_modules/@relayfile/adapter-core": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.6.2.tgz", - "integrity": "sha512-fVBwiK1W0af4wQ4w/Jr7GA1VBH1qz9Qw1lm90MrOJN0zQfVq1YRwRu9fPk4rp50TQDveLBKE0+MoaLMUmqr5Jg==", - "license": "Apache-2.0", - "dependencies": { - "@scalar/postman-to-openapi": "^0.6.0", - "cheerio": "^1.2.0", - "minimatch": "^10.0.3", - "yaml": "^2.8.1" - }, - "bin": { - "adapter-core": "dist/src/cli.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@relayfile/sdk": ">=0.6.0 <1" - } - }, "node_modules/@relayfile/sdk": { "version": "0.10.69", "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.10.69.tgz", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 9e2fe91da..adeeefd69 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -47,7 +47,7 @@ "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", - "@relayfile/adapter-core": "0.5.26", + "@relayfile/adapter-core": "0.6.2", "@relayfile/relay-helpers": "0.4.12", "@relayflows/surface": "2.0.25", "@types/js-yaml": "^4.0.9", diff --git a/packages/sdk/tests/flow-extension-compose.test.ts b/packages/sdk/tests/flow-extension-compose.test.ts index 922d886d8..a1eb3fac0 100644 --- a/packages/sdk/tests/flow-extension-compose.test.ts +++ b/packages/sdk/tests/flow-extension-compose.test.ts @@ -70,10 +70,11 @@ describe('composing flow extensions onto a base flow', () => { const p = project(); await install(p); const loaded = await loadAuthoredFlow(p.flow, { versions }); - expect(loaded.extensions.map(e => ({ name: e.name, ref: e.ref, handlers: e.handlers.length }))).toEqual([{ name: 'babysitter', ref: REF, handlers: 8 }]); + expect(loaded.extensions.map(e => ({ name: e.name, ref: e.ref, handlers: e.handlers.length }))).toEqual([{ name: 'babysitter', ref: REF, handlers: 11 }]); expect(subscriptions(loaded)).toEqual([ 'issues.opened', - 'pull_request.opened', 'pull_request.synchronize', 'pull_request.reopened', 'pull_request.closed', + 'pull_request.opened', 'pull_request.synchronize', 'pull_request.reopened', 'pull_request.ready_for_review', + 'pull_request.closed', 'pull_request.labeled', 'pull_request.unlabeled', 'pull_request_review.submitted', 'pull_request_review.dismissed', 'check_run.completed', 'issue_comment.created', ]); const composed = loaded.getDefinition(loaded.handle); @@ -89,12 +90,12 @@ describe('composing flow extensions onto a base flow', () => { // Every composed subscription is one the surface registry can lower. expect(preflightProviderTriggers(composed.handlers.map(h => h.trigger))).toEqual([]); // The extension's own handle is not the root: asking for its definition goes to the surface, not the composition. - expect(loaded.getDefinition(loaded.extensions[0]!.handle).handlers).toHaveLength(8); + expect(loaded.getDefinition(loaded.extensions[0]!.handle).handlers).toHaveLength(11); // Its graph node resolves through the accessor its own entry import returned, not the root's. const node = loaded.graph[1]!; expect(node.getDefinition).toBe(loaded.extensions[0]!.getDefinition); expect(node.getDefinition(node.handle).name).toBe('babysitter'); - expect(node.getDefinition(node.handle).handlers).toHaveLength(8); + expect(node.getDefinition(node.handle).handlers).toHaveLength(11); }); it('loads the root alone with extensions: none, and helper loading ignores extension entries', async () => { const p = project(); @@ -129,12 +130,12 @@ describe('composing flow extensions onto a base flow', () => { await install(p); const { report } = await checkAuthoredTriggers(p.flow); expect(report.ok).toBe(true); - expect(report.extensions).toEqual([{ name: 'babysitter', version: '0.1.0', ref: REF, digest: expect.stringMatching(/^[0-9a-f]{64}$/), handlers: 8, hooks: [] }]); + expect(report.extensions).toEqual([{ name: 'babysitter', version: '0.1.0', ref: REF, digest: expect.stringMatching(/^[0-9a-f]{64}$/), handlers: 11, hooks: [] }]); expect(report.requirements?.integrations.map(i => i.provider)).toContain('github'); expect(report.requirements?.harnessUses).toContainEqual({ harness: 'claude', detail: 'plugin "babysitter"' }); expect(await runCli(['check', p.flow], p.io)).toBe(0); expect(p.text()).toContain(`EXTENSION babysitter@0.1.0 ${REF} sha256:`); - expect(p.text()).toContain('8 handler(s) composed after the base flow'); + expect(p.text()).toContain('11 handler(s) composed after the base flow'); const loaded = await loadAuthoredFlow(p.flow, { versions }); const submissions = await collectExtensionSubmissions(loaded); expect(submissions).toHaveLength(1); @@ -271,8 +272,8 @@ describe('composition fails closed', () => { }); it.each([ ['an entry subscribing beyond its manifest', undefined, - "import { flow, github } from '@relayflows/surface';\nexport default flow('babysitter', async f => { f.done('success'); }).on(github.pull_request('opened'), async f => { f.done('success'); }).on(github.pull_request('labeled'), async f => { f.done('success'); });\n", - 'plugin_manifest_invalid', 'subscribes to github pull_request.labeled'], + "import { flow, github } from '@relayflows/surface';\nexport default flow('babysitter', async f => { f.done('success'); }).on(github.pull_request('opened'), async f => { f.done('success'); }).on(github.pull_request('edited'), async f => { f.done('success'); });\n", + 'plugin_manifest_invalid', 'subscribes to github pull_request.edited'], ['an entry with a schedule handler', undefined, "import { flow, schedule } from '@relayflows/surface';\nexport default flow('babysitter', async f => { f.done('success'); }).on(schedule.every('1h'), async f => { f.done('success'); });\n", 'plugin_unsupported', 'schedule trigger'], @@ -293,11 +294,4 @@ describe('composition fails closed', () => { await install(p, variant(m => m, entry)); await expect(loadAuthoredFlow(p.flow, { versions })).rejects.toMatchObject({ code, message: expect.stringContaining(message) }); }); - it('never composes an event the surface registry cannot lower, even if an entry asks for it', async () => { - // The manifest gate refuses ready_for_review at install; an entry alone cannot smuggle it past the manifest. - const p = project(); - const entries = variant(m => m, "import { flow, github } from '@relayflows/surface';\nexport default flow('babysitter', async f => { f.done('success'); }).on(github.pull_request('ready_for_review'), async f => { f.done('success'); });\n"); - await install(p, entries); - await expect(loadAuthoredFlow(p.flow, { versions })).rejects.toMatchObject({ code: 'plugin_manifest_invalid', message: expect.stringContaining('pull_request.ready_for_review') }); - }); }); diff --git a/packages/sdk/tests/plugin-extension.test.ts b/packages/sdk/tests/plugin-extension.test.ts index 47c16c2af..1f620fc98 100644 --- a/packages/sdk/tests/plugin-extension.test.ts +++ b/packages/sdk/tests/plugin-extension.test.ts @@ -92,7 +92,7 @@ describe('flows add ', () => { expect(existsSync(join(store, 'babysitter.flow.ts'))).toBe(true); expect(existsSync(join(store, 'manifest.json'))).toBe(true); expect(p.text()).toContain(`Added babysitter@0.1.0 (flow-extension) from ${REF}`); - expect(p.text()).toContain('events: github pull_request[opened,synchronize,reopened,closed]; github pull_request_review[submitted,dismissed]; github check_run[completed]; github issue_comment[created]'); + expect(p.text()).toContain('events: github pull_request[opened,synchronize,reopened,ready_for_review,closed,labeled,unlabeled]; github pull_request_review[submitted,dismissed]; github check_run[completed]; github issue_comment[created]'); expect(p.text()).toContain('writes (declared, unenforced): github:pull_request:comment'); expect(p.text()).toContain('recorded in flows.json and flows.lock.json'); expect(gh.calls.some(url => url.includes('/commits/feat%2Fbabysitter-v2'))).toBe(true); @@ -209,11 +209,14 @@ describe('schema-2 manifest validation', () => { const m = validateFlowExtensionManifest(manifestJson); expect(m).toMatchObject({ schema: 2, kind: 'flow-extension', name: 'babysitter', entry: 'babysitter.flow.ts', extends: { handlers: true, hooks: [] } }); expect(m.triggers).toHaveLength(4); + expect(m.triggers[0]).toMatchObject({ + provider: 'github', event: 'pull_request', + actions: ['opened', 'synchronize', 'reopened', 'ready_for_review', 'closed', 'labeled', 'unlabeled'], + }); expect(Object.isFrozen(m) && Object.isFrozen(m.permissions) && Object.isFrozen(m.triggers)).toBe(true); }); it.each([ - ['an event the surface registry cannot lower', (m: Record) => ({ ...m, triggers: [{ provider: 'github', event: 'pull_request', actions: ['ready_for_review'] }] }), 'plugin_event_unroutable'], - ['labeled/unlabeled, which the registry lacks', (m: Record) => ({ ...m, triggers: [{ provider: 'github', event: 'pull_request', actions: ['labeled', 'unlabeled'] }] }), 'plugin_event_unroutable'], + ['an event the surface registry cannot lower', (m: Record) => ({ ...m, triggers: [{ provider: 'github', event: 'pull_request', actions: ['future_action'] }] }), 'plugin_event_unroutable'], ['an unknown provider', (m: Record) => ({ ...m, triggers: [{ provider: 'nope', event: 'x', actions: [] }] }), 'plugin_event_unroutable'], ['an unknown kind', (m: Record) => ({ ...m, kind: 'banana' }), 'plugin_kind_invalid'], ['schema 1 with the extension kind', (m: Record) => ({ ...m, schema: 1 }), 'plugin_manifest_invalid'], diff --git a/packages/sdk/tests/preflight.test.ts b/packages/sdk/tests/preflight.test.ts index 95f09626e..d7726699e 100644 --- a/packages/sdk/tests/preflight.test.ts +++ b/packages/sdk/tests/preflight.test.ts @@ -681,7 +681,7 @@ describe('preflight: CLI resolution and refusal predicates', () => { { ref: 'github:o/r@main#ext', fetch: repo([...files(manifest), { path: 'ext/link', data: Buffer.from('x'), mode: '120000' }]) }, { ref: 'github:o/r@main#ext', fetch: repo([...files(manifest), { path: 'ext/big', data: Buffer.alloc(256_001) }]) }, { ref: 'github:o/r@main#ext', fetch: repo(files({ ...manifest, kind: 'banana' })) }, - { ref: 'github:o/r@main#ext', fetch: repo(files({ ...manifest, triggers: [{ provider: 'github', event: 'pull_request', actions: ['ready_for_review'] }] })) }, + { ref: 'github:o/r@main#ext', fetch: repo(files({ ...manifest, triggers: [{ provider: 'github', event: 'pull_request', actions: ['future_action'] }] })) }, { ref: 'github:o/r@main#ext', fetch: repo(files({ ...manifest, compat: { ...manifest.compat, surface: '^1.0.0' } })) }, { ref: 'github:o/r@main#ext', fetch: repo(files({ ...manifest, source: { host: 'github', owner: 'someone', repo: 'else', path: 'ext' } })) }, ]; diff --git a/packages/surface/src/triggers/PROVIDERS.md b/packages/surface/src/triggers/PROVIDERS.md index 25651daf9..2b406b50e 100644 --- a/packages/surface/src/triggers/PROVIDERS.md +++ b/packages/surface/src/triggers/PROVIDERS.md @@ -1,7 +1,7 @@ # Provider trigger namespaces -47 providers, 570 events. `mapping` rows come from the adapter's `webhooks:` block (payload-aware signatures); `catalog` rows come from `KNOWN_TRIGGER_CATALOG` (`supportedEvents()`) only. Every provider also includes its catalog events, so the count is what ingress delivers. +47 providers, 573 events. `mapping` rows come from the adapter's `webhooks:` block (payload-aware signatures); `catalog` rows come from `KNOWN_TRIGGER_CATALOG` (`supportedEvents()`) only. Every provider also includes its catalog events, so the count is what ingress delivers. | Provider | Namespace | Events | Source | Registry-only events | |---|---|---:|---|---| @@ -19,7 +19,7 @@ | `fathom` | `fathom` | 1 | mapping | | | `gcp` | `gcp` | 9 | mapping | | | `gcs` | `gcs` | 3 | mapping | | -| `github` | `github` | 26 | mapping | | +| `github` | `github` | 29 | mapping | | | `gitlab` | `gitlab` | 53 | mapping | | | `gmail` | `gmail` | 3 | mapping | | | `google-calendar` | `google_calendar` | 3 | catalog | | diff --git a/packages/surface/src/triggers/github.ts b/packages/surface/src/triggers/github.ts index 32d35ef27..eb94b5655 100644 --- a/packages/surface/src/triggers/github.ts +++ b/packages/surface/src/triggers/github.ts @@ -76,10 +76,19 @@ export const github = Object.freeze({ pull_request_closed(filter?: WebhookFilter) { return providerTrigger("github", "pull_request.closed", filter); }, + pull_request_labeled(filter?: WebhookFilter) { + return providerTrigger("github", "pull_request.labeled", filter); + }, pull_request_opened(filter?: WebhookFilter) { return providerTrigger("github", "pull_request.opened", filter); }, + pull_request_ready_for_review(filter?: WebhookFilter) { + return providerTrigger("github", "pull_request.ready_for_review", filter); + }, pull_request_synchronize(filter?: WebhookFilter) { return providerTrigger("github", "pull_request.synchronize", filter); }, + pull_request_unlabeled(filter?: WebhookFilter) { + return providerTrigger("github", "pull_request.unlabeled", filter); + }, }); diff --git a/packages/surface/src/triggers/index.ts b/packages/surface/src/triggers/index.ts index 496d6344b..4555ffa37 100644 --- a/packages/surface/src/triggers/index.ts +++ b/packages/surface/src/triggers/index.ts @@ -64,7 +64,7 @@ export const providerEventTypes = Object.freeze({ "fathom": Object.freeze(["new-meeting-content-ready"] as const), "gcp": Object.freeze(["billing.budget.alert","cloud-run.service.created","cloud-run.service.deleted","cloud-run.service.updated","error-reporting.event.logged","error-reporting.group.opened","error-reporting.group.reopened","monitoring.incident.closed","monitoring.incident.open"] as const), "gcs": Object.freeze(["file.created","file.deleted","file.updated"] as const), - "github": Object.freeze(["check_run","check_run.completed","deployment_status.created","issue_comment","issue_comment.created","issues","issues.closed","issues.edited","issues.labeled","issues.opened","issues.reopened","issues.unlabeled","pull_request","pull_request.closed","pull_request.edited","pull_request.opened","pull_request.reopened","pull_request.synchronize","pull_request_review","pull_request_review.dismissed","pull_request_review.edited","pull_request_review.submitted","pull_request_review_comment.created","pull_request_review_thread.resolved","push","status"] as const), + "github": Object.freeze(["check_run","check_run.completed","deployment_status.created","issue_comment","issue_comment.created","issues","issues.closed","issues.edited","issues.labeled","issues.opened","issues.reopened","issues.unlabeled","pull_request","pull_request.closed","pull_request.edited","pull_request.labeled","pull_request.opened","pull_request.ready_for_review","pull_request.reopened","pull_request.synchronize","pull_request.unlabeled","pull_request_review","pull_request_review.dismissed","pull_request_review.edited","pull_request_review.submitted","pull_request_review_comment.created","pull_request_review_thread.resolved","push","status"] as const), "gitlab": Object.freeze(["build","build.canceled","build.created","build.failed","build.manual","build.pending","build.running","build.skipped","build.success","deployment","deployment.canceled","deployment.created","deployment.failed","deployment.running","deployment.success","issue","issue.close","issue.open","issue.reopen","issue.update","job.canceled","job.created","job.failed","job.manual","job.pending","job.running","job.skipped","job.success","merge_request","merge_request.approved","merge_request.close","merge_request.merge","merge_request.open","merge_request.reopen","merge_request.unapproved","merge_request.update","note","note.Commit","note.Issue","note.MergeRequest","note.Snippet","pipeline","pipeline.canceled","pipeline.created","pipeline.failed","pipeline.manual","pipeline.pending","pipeline.running","pipeline.skipped","pipeline.success","pipeline.waiting_for_resource","push","tag_push"] as const), "gmail": Object.freeze(["file.created","file.deleted","file.updated"] as const), "google-calendar": Object.freeze(["calendar.exists","calendar.not_exists","calendar.sync"] as const), diff --git a/packages/surface/tests/triggers-github-events.test.ts b/packages/surface/tests/triggers-github-events.test.ts index 948934223..7bd2fc3aa 100644 --- a/packages/surface/tests/triggers-github-events.test.ts +++ b/packages/surface/tests/triggers-github-events.test.ts @@ -1,6 +1,13 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { github, providerEventTypes } from "../src/triggers/index.js"; +const babysitterActions = [ + "pull_request.ready_for_review", + "pull_request.labeled", + "pull_request.unlabeled", +] as const; + describe("generated GitHub trigger vocabulary", () => { it("offers check_run and issue_comment with an action filter, like pull_request", () => { // The adapter's own mapping supersedes the core fallback, so the vocabulary @@ -13,4 +20,17 @@ describe("generated GitHub trigger vocabulary", () => { expect(github.issue_comment().filter).toEqual({ provider: "github", type: "issue_comment" }); expect(() => github.check_run("")).toThrow(TypeError); }); + + it("ships every pull-request action Babysitter declares in the source and built package", () => { + expect([...providerEventTypes.github]).toEqual(expect.arrayContaining([...babysitterActions])); + expect(github.pull_request_ready_for_review().filter).toEqual({ provider: "github", type: "pull_request.ready_for_review" }); + expect(github.pull_request_labeled().filter).toEqual({ provider: "github", type: "pull_request.labeled" }); + expect(github.pull_request_unlabeled().filter).toEqual({ provider: "github", type: "pull_request.unlabeled" }); + + // @relayflows/surface publishes dist/ (package.json "files"). The package + // test builds before Vitest, so this pins the artifact consumers receive, + // not only the TypeScript generator input. + const built = readFileSync(new URL("../dist/triggers/index.js", import.meta.url), "utf8"); + for (const event of babysitterActions) expect(built).toContain(JSON.stringify(event)); + }); }); diff --git a/testdata/plugins/extension-babysitter/README.md b/testdata/plugins/extension-babysitter/README.md index bccfa64d3..a4cf85427 100644 --- a/testdata/plugins/extension-babysitter/README.md +++ b/testdata/plugins/extension-babysitter/README.md @@ -11,9 +11,8 @@ because this fixture does not declare a hook; declared hooks are composed when the base flow names them. The `merge-gate` hook from the design is not included in this fixture. -Babysitter's own subscription contract (branch `feat/babysitter-v2`) names -eleven GitHub subscriptions. Three of them — `pull_request.ready_for_review`, -`pull_request.labeled`, `pull_request.unlabeled` — are not in the surface -event registry (`providerEventTypes`), so a manifest declaring them is refused -with `plugin_event_unroutable`; this fixture lists only the eight the registry -can lower. Extending the registry is a separate change and is not claimed here. +Babysitter's subscription contract names eleven GitHub subscriptions. The +fixture declares and composes all eleven, including +`pull_request.ready_for_review`, `pull_request.labeled`, and +`pull_request.unlabeled`, so the install and composition tests exercise the +same event set as the installable example. diff --git a/testdata/plugins/extension-babysitter/babysitter.flow.ts b/testdata/plugins/extension-babysitter/babysitter.flow.ts index e27553f09..a46f273b9 100644 --- a/testdata/plugins/extension-babysitter/babysitter.flow.ts +++ b/testdata/plugins/extension-babysitter/babysitter.flow.ts @@ -10,7 +10,10 @@ export default flow('babysitter', { budget: { dollars: 8, wallclock: '45m' } }, .on(github.pull_request('opened'), babysit) .on(github.pull_request('synchronize'), babysit) .on(github.pull_request('reopened'), babysit) + .on(github.pull_request('ready_for_review'), babysit) .on(github.pull_request('closed'), babysit) + .on(github.pull_request('labeled'), babysit) + .on(github.pull_request('unlabeled'), babysit) .on(github.pull_request_review({ action: 'submitted' }), babysit) .on(github.pull_request_review({ action: 'dismissed' }), babysit) .on(github.check_run('completed'), babysit) diff --git a/testdata/plugins/extension-babysitter/flows-plugin.json b/testdata/plugins/extension-babysitter/flows-plugin.json index 5f7afe7eb..1114b2c8f 100644 --- a/testdata/plugins/extension-babysitter/flows-plugin.json +++ b/testdata/plugins/extension-babysitter/flows-plugin.json @@ -27,7 +27,10 @@ "opened", "synchronize", "reopened", - "closed" + "ready_for_review", + "closed", + "labeled", + "unlabeled" ] }, { From b1e9822ae096540d7d2cbdb3bc7391e2093ceeb7 Mon Sep 17 00:00:00 2001 From: Miya Date: Mon, 21 Sep 2026 18:18:33 +0200 Subject: [PATCH 2/2] test(catalog): remove stale Babysitter block assertion Session-Id: 01a0c4a6-dd65-7ce1-a90e-de1b0b4e86c3 Session-Id: 01a0c4a6-dd65-7ce1-a90e-de1b0b4e86c3 --- packages/sdk/tests/catalog-plugins.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/tests/catalog-plugins.test.ts b/packages/sdk/tests/catalog-plugins.test.ts index 6ee33050a..9fa31582f 100644 --- a/packages/sdk/tests/catalog-plugins.test.ts +++ b/packages/sdk/tests/catalog-plugins.test.ts @@ -22,7 +22,7 @@ describe('catalog/plugins.json', () => { expect(new Set(names).size).toBe(names.length); }); - it('records a fail-closed babysitter entry with a pinned sha and digest', () => { + it('records the full Babysitter contract with a pinned sha and digest', () => { const babysitter = catalog.plugins.find(p => p.name === 'babysitter'); expect(babysitter).toMatchObject({ source: { owner: 'AgentWorkforce', repo: 'flows', path: 'examples/babysitter' }, @@ -31,7 +31,8 @@ describe('catalog/plugins.json', () => { }); expect(babysitter!.ref).toMatch(SHA); expect(babysitter!.digest).toMatch(HEX64); - expect(String(babysitter!.description)).toContain('plugin_event_unroutable'); + expect(String(babysitter!.description)).toContain('eleven-event subscription contract'); + expect(String(babysitter!.description)).not.toContain('plugin_event_unroutable'); expect(TIERS.has(String(babysitter!.tier))).toBe(true); }); });