@openhands/extensions 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/custom-codereview-guide.md +7 -0
- package/.github/dependabot.yml +24 -0
- package/.github/scripts/check_issue_readiness.py +272 -0
- package/.github/scripts/check_pr_description.py +226 -0
- package/.github/scripts/post-readiness-comment.mjs +184 -0
- package/.github/scripts/refresh_linked_pr_checks.py +171 -0
- package/.github/workflows/issue-readiness-check.yml +142 -0
- package/.github/workflows/pr-description-check.yml +37 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +16 -0
- package/README.md +13 -2
- package/automations/README.md +3 -2
- package/automations/bundle-index.js +10 -1
- package/automations/catalog/github-agents-md-maintainer/manifest.json +101 -0
- package/automations/catalog/github-issue-to-pr/manifest.json +112 -0
- package/automations/catalog/github-pr-reviewer/manifest.json +6 -0
- package/automations/catalog/github-repo-monitor/manifest.json +6 -0
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +23 -0
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-bitbucket-pr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-github-pr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-gitlab-mr/manifest.json +23 -0
- package/automations/catalog/news-digest/manifest.json +81 -0
- package/automations/catalog/qa-changes/manifest.json +72 -0
- package/automations/catalog/slack-github-linear-daily-organization.json +27 -0
- package/automations/catalog-index.js +25 -7
- package/automations/catalog.schema.json +35 -2
- package/automations/index.d.ts +34 -1
- package/automations/interface.json +3 -1
- package/integrations/catalog/datadog.json +2 -2
- package/integrations/catalog/gitlab.json +41 -0
- package/integrations/catalog/notion.json +1 -1
- package/integrations/catalog-index.js +71 -69
- package/integrations/index.d.ts +1 -1
- package/marketplaces/openhands-extensions.json +59 -6
- package/package.json +1 -1
- package/plugins/pr-review/scripts/prompt.py +2 -0
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/github-agents-md-maintainer/.plugin/plugin.json +19 -0
- package/skills/github-agents-md-maintainer/README.md +59 -0
- package/skills/github-agents-md-maintainer/SKILL.md +271 -0
- package/skills/github-agents-md-maintainer/commands/agents-md-setup.md +8 -0
- package/skills/github-agents-md-maintainer/references/state-schema.md +113 -0
- package/skills/github-agents-md-maintainer/scripts/main.py +1187 -0
- package/skills/github-issue-to-pr/.plugin/plugin.json +19 -0
- package/skills/github-issue-to-pr/README.md +84 -0
- package/skills/github-issue-to-pr/SKILL.md +373 -0
- package/skills/github-issue-to-pr/commands/issue-to-pr-setup.md +8 -0
- package/skills/github-issue-to-pr/references/state-schema.md +139 -0
- package/skills/github-issue-to-pr/scripts/main.py +1305 -0
- package/skills/github-pr-reviewer/SKILL.md +2 -1
- package/skills/github-pr-reviewer/scripts/main.py +45 -4
- package/skills/github-pr-reviewer/tests/test_main.py +74 -0
- package/skills/index.js +38 -2
- package/skills/news-digest/.plugin/plugin.json +19 -0
- package/skills/news-digest/README.md +71 -0
- package/skills/news-digest/SKILL.md +284 -0
- package/skills/news-digest/commands/news-digest-setup.md +8 -0
- package/skills/news-digest/references/state-schema.md +178 -0
- package/skills/news-digest/scripts/main.py +1250 -0
- package/skills/openhands-sdk/SKILL.md +1 -0
- package/skills/ticket-to-code-change/.plugin/plugin.json +21 -0
- package/skills/ticket-to-code-change/README.md +5 -0
- package/skills/ticket-to-code-change/SKILL.md +50 -0
- package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +8 -0
- package/tests/fixtures/automations/github-repo-monitor.json +77 -0
- package/tests/test_automation_setup.py +56 -0
- package/tests/test_catalogs.py +5 -3
- package/tests/test_check_issue_readiness.py +135 -0
- package/tests/test_check_pr_description.py +228 -0
- package/tests/test_github_agents_md_maintainer.py +352 -0
- package/tests/test_github_issue_to_pr.py +539 -0
- package/tests/test_news_digest.py +646 -0
- package/tests/test_pr_review_prompt.py +10 -0
- package/tests/test_refresh_linked_pr_checks.py +130 -0
|
@@ -198,6 +198,7 @@ Source: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/
|
|
|
198
198
|
- [`55_persistent_memory.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/55_persistent_memory.py)
|
|
199
199
|
- [`56_structured_output.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/56_structured_output.py)
|
|
200
200
|
- [`57_prompt_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/57_prompt_hooks)
|
|
201
|
+
- [`58_ask_oracle_tool`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/58_ask_oracle_tool)
|
|
201
202
|
|
|
202
203
|
### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)
|
|
203
204
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ticket-to-code-change",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create pull or merge requests from Jira or Linear tickets across GitHub, GitLab, and Bitbucket.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "OpenHands",
|
|
7
|
+
"email": "contact@all-hands.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/OpenHands/extensions",
|
|
10
|
+
"repository": "https://github.com/OpenHands/extensions",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"jira",
|
|
14
|
+
"linear",
|
|
15
|
+
"github",
|
|
16
|
+
"gitlab",
|
|
17
|
+
"bitbucket",
|
|
18
|
+
"pull-request",
|
|
19
|
+
"automation"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ticket-to-code-change
|
|
3
|
+
description: >
|
|
4
|
+
Set up a ticket-to-code-change automation using Jira or Linear as the issue
|
|
5
|
+
tracker and GitHub, GitLab, or Bitbucket as the source-control provider.
|
|
6
|
+
Watches for implementation-ready tickets, starts an OpenHands conversation
|
|
7
|
+
to implement and test the request, opens a pull or merge request, and links
|
|
8
|
+
the result back to the ticket.
|
|
9
|
+
triggers:
|
|
10
|
+
- /ticket-to-code-change:setup
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Ticket to code change
|
|
14
|
+
|
|
15
|
+
Create an automation that turns implementation-ready tickets into tested pull
|
|
16
|
+
requests or merge requests.
|
|
17
|
+
|
|
18
|
+
## Information to collect
|
|
19
|
+
|
|
20
|
+
Ask the user for:
|
|
21
|
+
|
|
22
|
+
1. The issue tracker: Jira Cloud or Linear.
|
|
23
|
+
2. The source-control provider: GitHub, GitLab, or Bitbucket Cloud.
|
|
24
|
+
3. The project or team to watch and the label or workflow state that means
|
|
25
|
+
"implementation ready".
|
|
26
|
+
4. How a ticket identifies its target repository and base branch.
|
|
27
|
+
5. The polling schedule or issue event to use.
|
|
28
|
+
6. The ticket state to set when work starts and when the change request opens.
|
|
29
|
+
7. Whether linked dependencies must be completed before dispatch.
|
|
30
|
+
|
|
31
|
+
## Setup workflow
|
|
32
|
+
|
|
33
|
+
1. Verify that both required integrations are connected and can read the
|
|
34
|
+
selected project and repository.
|
|
35
|
+
2. Prefer an issue event trigger when the deployment can receive events;
|
|
36
|
+
otherwise use cron polling with durable issue-ID deduplication.
|
|
37
|
+
3. Limit each run to a small configurable number of new tickets. On initial
|
|
38
|
+
deployment, establish a baseline instead of dispatching the entire backlog.
|
|
39
|
+
4. Build a prompt that includes the full ticket, acceptance criteria,
|
|
40
|
+
dependency status, repository, base branch, and provider-specific request
|
|
41
|
+
terminology. Require the agent to run the repository's tests before opening
|
|
42
|
+
the pull request or merge request.
|
|
43
|
+
5. Create the automation through the Automation backend described in
|
|
44
|
+
`<RUNTIME_SERVICES>`. Use its prompt-preset endpoint and authenticate with
|
|
45
|
+
the runtime-provided automation API key.
|
|
46
|
+
6. Configure the automation to post the OpenHands conversation URL immediately
|
|
47
|
+
and the resulting pull-request or merge-request URL back to the ticket.
|
|
48
|
+
|
|
49
|
+
Do not dispatch tickets with unresolved dependencies or without an unambiguous
|
|
50
|
+
target repository; comment on the ticket with the missing information instead.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
# auto-generated by sync_extensions.py
|
|
3
|
+
description: Set up a ticket-to-code-change automation using Jira or Linear as the issue tracker and GitHub, GitLab, or Bitbucket as the source-control provider. Watches for implementation-ready tickets, starts an OpenHands conversation to implement and test the request, opens a pull or merge request, and links the result back to the ticket.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
|
|
7
|
+
|
|
8
|
+
$ARGUMENTS
|
|
@@ -37,6 +37,17 @@
|
|
|
37
37
|
"type": "cron",
|
|
38
38
|
"schedule": "*/15 * * * *",
|
|
39
39
|
"timezone": "UTC"
|
|
40
|
+
},
|
|
41
|
+
"template": {
|
|
42
|
+
"id": "github-repo-monitor",
|
|
43
|
+
"version": "1.0.0",
|
|
44
|
+
"config": {
|
|
45
|
+
"repository": "OpenHands/agent-server-gui",
|
|
46
|
+
"triggerPhrase": "@openhands",
|
|
47
|
+
"ref": "main",
|
|
48
|
+
"schedule": "*/15 * * * *",
|
|
49
|
+
"timezone": "UTC"
|
|
50
|
+
}
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
}
|
|
@@ -67,6 +78,17 @@
|
|
|
67
78
|
"type": "cron",
|
|
68
79
|
"schedule": "*/15 * * * *",
|
|
69
80
|
"timezone": "UTC"
|
|
81
|
+
},
|
|
82
|
+
"template": {
|
|
83
|
+
"id": "github-repo-monitor",
|
|
84
|
+
"version": "1.0.0",
|
|
85
|
+
"config": {
|
|
86
|
+
"repository": "OpenHands/agent-server-gui",
|
|
87
|
+
"triggerPhrase": "@openhands",
|
|
88
|
+
"ref": "main",
|
|
89
|
+
"schedule": "*/15 * * * *",
|
|
90
|
+
"timezone": "UTC"
|
|
91
|
+
}
|
|
70
92
|
}
|
|
71
93
|
}
|
|
72
94
|
},
|
|
@@ -79,6 +101,28 @@
|
|
|
79
101
|
"model": null,
|
|
80
102
|
"name": "GitHub repository monitor - OpenHands/agent-server-gui",
|
|
81
103
|
"prompt": "Poll OpenHands/agent-server-gui for any new issue or pull request comments since the last run. For every comment mentioning '@openhands', read the surrounding issue or pull request context and post a helpful reply as a comment on the same thread.",
|
|
104
|
+
"preset_metadata": {
|
|
105
|
+
"preset_type": "prompt",
|
|
106
|
+
"prompt": "Poll OpenHands/agent-server-gui for any new issue or pull request comments since the last run. For every comment mentioning '@openhands', read the surrounding issue or pull request context and post a helpful reply as a comment on the same thread.",
|
|
107
|
+
"repos": [
|
|
108
|
+
{
|
|
109
|
+
"url": "OpenHands/agent-server-gui",
|
|
110
|
+
"ref": "main",
|
|
111
|
+
"provider": "github"
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"template": {
|
|
115
|
+
"id": "github-repo-monitor",
|
|
116
|
+
"version": "1.0.0",
|
|
117
|
+
"config": {
|
|
118
|
+
"repository": "OpenHands/agent-server-gui",
|
|
119
|
+
"triggerPhrase": "@openhands",
|
|
120
|
+
"ref": "main",
|
|
121
|
+
"schedule": "*/15 * * * *",
|
|
122
|
+
"timezone": "UTC"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
82
126
|
"trigger": {
|
|
83
127
|
"type": "cron",
|
|
84
128
|
"schedule": "*/15 * * * *",
|
|
@@ -128,6 +172,17 @@
|
|
|
128
172
|
"type": "cron",
|
|
129
173
|
"schedule": "*/1 * * * *",
|
|
130
174
|
"timezone": "UTC"
|
|
175
|
+
},
|
|
176
|
+
"template": {
|
|
177
|
+
"id": "github-repo-monitor",
|
|
178
|
+
"version": "1.0.0",
|
|
179
|
+
"config": {
|
|
180
|
+
"repository": "OpenHands/agent-server-gui",
|
|
181
|
+
"triggerPhrase": "@openhands",
|
|
182
|
+
"ref": "main",
|
|
183
|
+
"schedule": "*/1 * * * *",
|
|
184
|
+
"timezone": "UTC"
|
|
185
|
+
}
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
188
|
}
|
|
@@ -178,6 +233,17 @@
|
|
|
178
233
|
"type": "cron",
|
|
179
234
|
"schedule": "0 0 31 2 *",
|
|
180
235
|
"timezone": "UTC"
|
|
236
|
+
},
|
|
237
|
+
"template": {
|
|
238
|
+
"id": "github-repo-monitor",
|
|
239
|
+
"version": "1.0.0",
|
|
240
|
+
"config": {
|
|
241
|
+
"repository": "OpenHands/agent-server-gui",
|
|
242
|
+
"triggerPhrase": "@openhands",
|
|
243
|
+
"ref": "main",
|
|
244
|
+
"schedule": "0 0 31 2 *",
|
|
245
|
+
"timezone": "UTC"
|
|
246
|
+
}
|
|
181
247
|
}
|
|
182
248
|
}
|
|
183
249
|
},
|
|
@@ -229,6 +295,17 @@
|
|
|
229
295
|
"type": "cron",
|
|
230
296
|
"schedule": "*/15 * * * *",
|
|
231
297
|
"timezone": "UTC"
|
|
298
|
+
},
|
|
299
|
+
"template": {
|
|
300
|
+
"id": "github-repo-monitor",
|
|
301
|
+
"version": "1.0.0",
|
|
302
|
+
"config": {
|
|
303
|
+
"repository": "OpenHands/agent-server-gui",
|
|
304
|
+
"triggerPhrase": "@openhands",
|
|
305
|
+
"ref": "main",
|
|
306
|
+
"schedule": "*/15 * * * *",
|
|
307
|
+
"timezone": "UTC"
|
|
308
|
+
}
|
|
232
309
|
}
|
|
233
310
|
}
|
|
234
311
|
},
|
|
@@ -278,6 +278,16 @@ def _render_payload(entry: dict, form_values: dict, tarball_path: str = "") -> d
|
|
|
278
278
|
|
|
279
279
|
body["trigger"] = _derive_trigger(entry, form_values)
|
|
280
280
|
|
|
281
|
+
# A versioned entry sends its provenance: the service stores it opaquely,
|
|
282
|
+
# keyed by id for idempotent creation. The form values are non-secret by
|
|
283
|
+
# design (credentials come only from connected integrations).
|
|
284
|
+
if "version" in entry:
|
|
285
|
+
body["template"] = {
|
|
286
|
+
"id": entry["id"],
|
|
287
|
+
"version": entry["version"],
|
|
288
|
+
"config": dict(form_values),
|
|
289
|
+
}
|
|
290
|
+
|
|
281
291
|
return body
|
|
282
292
|
|
|
283
293
|
|
|
@@ -497,6 +507,10 @@ def test_schema_rejects_content_a_setup_block_must_never_carry() -> None:
|
|
|
497
507
|
with_repeated_identity["setup"]["description"] = "a second description"
|
|
498
508
|
rejected.append(("description", with_repeated_identity))
|
|
499
509
|
|
|
510
|
+
with_bad_version = deepcopy(entry)
|
|
511
|
+
with_bad_version["version"] = "1.0"
|
|
512
|
+
rejected.append(("'1.0' does not match", with_bad_version))
|
|
513
|
+
|
|
500
514
|
for expected_fragment, invalid in rejected:
|
|
501
515
|
errors = list(VALIDATOR.iter_errors(invalid))
|
|
502
516
|
assert any(expected_fragment in error.message for error in errors), (
|
|
@@ -504,6 +518,28 @@ def test_schema_rejects_content_a_setup_block_must_never_carry() -> None:
|
|
|
504
518
|
)
|
|
505
519
|
|
|
506
520
|
|
|
521
|
+
IMPACT_REJECTIONS: list[tuple[str, dict]] = [
|
|
522
|
+
("a basis the host does not know how to compute", {"basis": "run-counter"}),
|
|
523
|
+
("markup in a phrase", {"one": "<b>1 sweep</b>"}),
|
|
524
|
+
("a placeholder from another namespace", {"other": "{{count}} sweeps for {{form.repo}}"}),
|
|
525
|
+
("a plural phrase that hides the count", {"other": "many sweeps completed"}),
|
|
526
|
+
("an extra key beside the declared three", {"detail": "and saved hours"}),
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@pytest.mark.parametrize(
|
|
531
|
+
("case", "override"),
|
|
532
|
+
[pytest.param(case, override, id=case) for case, override in IMPACT_REJECTIONS],
|
|
533
|
+
)
|
|
534
|
+
def test_schema_refuses_an_impact_statement_the_host_must_never_render(
|
|
535
|
+
case: str, override: dict
|
|
536
|
+
) -> None:
|
|
537
|
+
entry = deepcopy(_load(CATALOG_DIR / "github-pr-reviewer" / "manifest.json"))
|
|
538
|
+
entry["impact"].update(override)
|
|
539
|
+
|
|
540
|
+
assert list(VALIDATOR.iter_errors(entry)), f"schema admitted {case}"
|
|
541
|
+
|
|
542
|
+
|
|
507
543
|
@pytest.mark.parametrize("entry_path", list(_setup_paths()))
|
|
508
544
|
def test_form_placeholders_reference_declared_fields(entry_path: Path) -> None:
|
|
509
545
|
"""A {{form.x}} that names no field renders as an empty value at runtime."""
|
|
@@ -520,6 +556,26 @@ def test_form_placeholders_reference_declared_fields(entry_path: Path) -> None:
|
|
|
520
556
|
assert referenced - fields == set()
|
|
521
557
|
|
|
522
558
|
|
|
559
|
+
@pytest.mark.parametrize("entry_path", list(_setup_paths()))
|
|
560
|
+
def test_the_declared_features_match_the_archetype(entry_path: Path) -> None:
|
|
561
|
+
"""A bundle needs a deployment that runs a client-supplied tarball; a prompt
|
|
562
|
+
needs the preset endpoint. Declaring the other one's features is a check the
|
|
563
|
+
host runs against the wrong capability, so the entry is either offered where
|
|
564
|
+
it cannot run or withheld where it can."""
|
|
565
|
+
entry = _load(entry_path)
|
|
566
|
+
features = set(entry["requires"].get("features", []))
|
|
567
|
+
|
|
568
|
+
if _is_bundle(entry):
|
|
569
|
+
assert "customTarball" in features, f"{entry['id']}: a bundle must declare customTarball"
|
|
570
|
+
assert "presetPrompt" not in features, (
|
|
571
|
+
f"{entry['id']}: a bundle creates through {BUNDLE_CREATE_PATH}, not the preset endpoint"
|
|
572
|
+
)
|
|
573
|
+
else:
|
|
574
|
+
assert "customTarball" not in features, (
|
|
575
|
+
f"{entry['id']}: only a bundle uploads a tarball"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
523
579
|
@pytest.mark.parametrize("entry_path", list(_setup_paths()))
|
|
524
580
|
def test_select_fields_offer_options(entry_path: Path) -> None:
|
|
525
581
|
"""A select without options is an empty dropdown the user cannot get past.
|
package/tests/test_catalogs.py
CHANGED
|
@@ -34,8 +34,10 @@ def test_catalog_ids_are_unique_and_automations_reference_things_that_exist():
|
|
|
34
34
|
|
|
35
35
|
known_integration_ids = set(integration_ids)
|
|
36
36
|
for automation in automations:
|
|
37
|
+
# May be empty: an automation that needs nothing connected says so with
|
|
38
|
+
# an empty object rather than by leaving the key out. What it may not do
|
|
39
|
+
# is name an integration that does not exist.
|
|
37
40
|
required = automation["requires"]["integrations"]
|
|
38
|
-
assert required
|
|
39
41
|
assert set(required) - known_integration_ids == set()
|
|
40
42
|
|
|
41
43
|
# The launch command is looked up from this skill rather than stored on
|
|
@@ -91,7 +93,7 @@ def test_remote_no_auth_mcp_entries_are_intentionally_public():
|
|
|
91
93
|
transport = option.get("transport", {})
|
|
92
94
|
# An option with `headerFields` still requires the user to supply
|
|
93
95
|
# credentials (just via named headers, e.g. Datadog's
|
|
94
|
-
#
|
|
96
|
+
# DD-API-KEY/DD-APPLICATION-KEY), so it is NOT "public/no-auth".
|
|
95
97
|
has_header_credentials = bool(transport.get("headerFields"))
|
|
96
98
|
if (
|
|
97
99
|
option["provider"] == "mcp"
|
|
@@ -117,7 +119,7 @@ def test_datadog_mcp_uses_documented_api_key_headers():
|
|
|
117
119
|
|
|
118
120
|
assert [
|
|
119
121
|
field["key"] for field in api_option["transport"]["headerFields"]
|
|
120
|
-
] == ["
|
|
122
|
+
] == ["DD-API-KEY", "DD-APPLICATION-KEY"]
|
|
121
123
|
|
|
122
124
|
|
|
123
125
|
def test_credential_fields_have_helper_text_and_link():
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_prod_module():
|
|
9
|
+
repo_root = Path(__file__).resolve().parents[1]
|
|
10
|
+
script_path = repo_root / ".github" / "scripts" / "check_issue_readiness.py"
|
|
11
|
+
name = "check_issue_readiness"
|
|
12
|
+
spec = importlib.util.spec_from_file_location(name, script_path)
|
|
13
|
+
assert spec and spec.loader
|
|
14
|
+
mod = importlib.util.module_from_spec(spec)
|
|
15
|
+
sys.modules[name] = mod
|
|
16
|
+
spec.loader.exec_module(mod)
|
|
17
|
+
return mod
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_prod = _load_prod_module()
|
|
21
|
+
evaluate_readiness = _prod.evaluate_readiness
|
|
22
|
+
extract_sections = _prod.extract_sections
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ENHANCEMENT_READY = """### Problem or Use Case
|
|
26
|
+
|
|
27
|
+
The integration catalog lacks a Linear entry.
|
|
28
|
+
|
|
29
|
+
### Desired Behavior
|
|
30
|
+
|
|
31
|
+
`integrations/catalog/linear.json` describes the Linear MCP server so the
|
|
32
|
+
catalog build picks it up.
|
|
33
|
+
|
|
34
|
+
### Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- [ ] `npm run build:integrations` succeeds with the new entry
|
|
37
|
+
- [ ] `getIntegrationCatalogEntry("linear")` returns the Linear catalog model
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
BUG_READY = """### Actual Behavior
|
|
41
|
+
|
|
42
|
+
Running `uv run --group test pytest tests/` fails in
|
|
43
|
+
`test_skills_catalog.py` with a KeyError when a skill is missing from the
|
|
44
|
+
marketplace.
|
|
45
|
+
|
|
46
|
+
### Acceptance Criteria
|
|
47
|
+
|
|
48
|
+
- [ ] `uv run --group test pytest tests/` passes with a clear error message
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_extract_sections_splits_on_headings():
|
|
53
|
+
sections = extract_sections("### Alpha\n\ntext\n\n### Beta\n\nmore\n")
|
|
54
|
+
assert sections["alpha"] == "\ntext\n\n"
|
|
55
|
+
assert sections["beta"] == "\nmore\n"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_enhancement_ready_passes():
|
|
59
|
+
result = evaluate_readiness(ENHANCEMENT_READY, ["enhancement"])
|
|
60
|
+
assert result.ready is True
|
|
61
|
+
assert result.reasons == []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_enhancement_missing_acceptance_criteria_fails():
|
|
65
|
+
body = "### Desired Behavior\n\nSome desired change.\n"
|
|
66
|
+
result = evaluate_readiness(body, ["enhancement"])
|
|
67
|
+
assert result.ready is False
|
|
68
|
+
assert any("Acceptance Criteria" in r for r in result.reasons)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_enhancement_missing_desired_behavior_fails():
|
|
72
|
+
body = ENHANCEMENT_READY.replace(
|
|
73
|
+
"### Desired Behavior\n\n"
|
|
74
|
+
"`integrations/catalog/linear.json` describes the Linear MCP server so the\n"
|
|
75
|
+
"catalog build picks it up.\n\n",
|
|
76
|
+
"",
|
|
77
|
+
)
|
|
78
|
+
result = evaluate_readiness(body, ["enhancement"])
|
|
79
|
+
assert result.ready is False
|
|
80
|
+
assert any("Desired Behavior" in r for r in result.reasons)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_bug_ready_passes():
|
|
84
|
+
result = evaluate_readiness(BUG_READY, ["bug"])
|
|
85
|
+
assert result.ready is True
|
|
86
|
+
assert result.reasons == []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_bug_missing_run_method_fails():
|
|
90
|
+
body = BUG_READY.replace(
|
|
91
|
+
"Running `uv run --group test pytest tests/` fails",
|
|
92
|
+
"Running the test suite fails",
|
|
93
|
+
)
|
|
94
|
+
result = evaluate_readiness(body, ["bug"])
|
|
95
|
+
assert result.ready is False
|
|
96
|
+
assert any("reproducible command" in r for r in result.reasons)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_bug_npm_build_command_is_a_valid_run_method():
|
|
100
|
+
body = BUG_READY.replace(
|
|
101
|
+
"Running `uv run --group test pytest tests/` fails in\n"
|
|
102
|
+
"`test_skills_catalog.py` with a KeyError when a skill is missing from the\n"
|
|
103
|
+
"marketplace.",
|
|
104
|
+
"Running `npm run build:skills` fails with a schema error.",
|
|
105
|
+
)
|
|
106
|
+
result = evaluate_readiness(body, ["bug"])
|
|
107
|
+
assert result.ready is True
|
|
108
|
+
assert result.reasons == []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_bug_acceptance_needs_checklist_item():
|
|
112
|
+
body = BUG_READY.replace(
|
|
113
|
+
"- [ ] `uv run --group test pytest tests/` passes with a clear error message",
|
|
114
|
+
"Make the test error message clearer",
|
|
115
|
+
)
|
|
116
|
+
result = evaluate_readiness(body, ["bug"])
|
|
117
|
+
assert result.ready is False
|
|
118
|
+
assert any("checklist item" in r for r in result.reasons)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_no_response_field_counts_as_empty():
|
|
122
|
+
body = BUG_READY.replace(
|
|
123
|
+
"### Acceptance Criteria\n\n- [ ] `uv run --group test pytest tests/` "
|
|
124
|
+
"passes with a clear error message\n",
|
|
125
|
+
"### Acceptance Criteria\n\n_No response_\n",
|
|
126
|
+
)
|
|
127
|
+
result = evaluate_readiness(body, ["bug"])
|
|
128
|
+
assert result.ready is False
|
|
129
|
+
assert any("Acceptance Criteria" in r for r in result.reasons)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_no_bug_or_enhancement_label_not_ready():
|
|
133
|
+
result = evaluate_readiness(ENHANCEMENT_READY, [])
|
|
134
|
+
assert result.ready is False
|
|
135
|
+
assert any("bug" in r and "enhancement" in r for r in result.reasons)
|