feat: add generic openclaw target onboarding
Some checks failed
Some checks failed
This commit is contained in:
parent
fdd213207f
commit
b9a420e71b
@ -36,6 +36,9 @@ checkpoint, and status-only commits are intentionally omitted.
|
||||
- Scoped sweep record/status publishing to the active target repository slug so
|
||||
concurrent runs for other repositories cannot overwrite newly added target
|
||||
records from stale generated state.
|
||||
- Added data-driven target repository config plus a conservative `openclaw/*`
|
||||
fallback so newly installed OpenClaw repositories can use exact event review
|
||||
without a TypeScript profile change.
|
||||
- Reduced default worker fan-out by about 20% across review shards, hot intake,
|
||||
commit review pages, repair live-worker caps, and automatic implementation
|
||||
dispatches.
|
||||
|
||||
46
config/target-repositories.json
Normal file
46
config/target-repositories.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"repositories": [
|
||||
{
|
||||
"target_repo": "openclaw/clawhub",
|
||||
"display_name": "ClawHub",
|
||||
"checkout_dir": "clawhub",
|
||||
"community_url": "https://clawhub.ai/",
|
||||
"prompt_note": "Use the ClawHub source tree and current main branch. Review every issue and PR with the same evidence standard, but only propose auto-close for pull requests that are certainly implemented on main. Keep everything else open.",
|
||||
"apply_close_rules": {
|
||||
"issue": [],
|
||||
"pull_request": ["implemented_on_main"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"target_repo": "openclaw/clawsweeper",
|
||||
"display_name": "ClawSweeper",
|
||||
"checkout_dir": "clawsweeper",
|
||||
"prompt_note": "Use the ClawSweeper source tree and current main branch. Review bot automation, workflow, and documentation changes conservatively. Only propose auto-close for pull requests that are certainly implemented on main; keep issues open for maintainer triage.",
|
||||
"apply_close_rules": {
|
||||
"issue": [],
|
||||
"pull_request": ["implemented_on_main"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"target_repo": "openclaw/fs-safe",
|
||||
"display_name": "fs-safe",
|
||||
"checkout_dir": "fs-safe",
|
||||
"prompt_note": "Use the fs-safe source tree and current main branch. Review filesystem-safety, path-handling, and package changes conservatively. Only propose auto-close for pull requests that are certainly implemented on main; keep issues open for maintainer triage.",
|
||||
"apply_close_rules": {
|
||||
"issue": [],
|
||||
"pull_request": ["implemented_on_main"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"openclaw_fallback": {
|
||||
"owner": "openclaw",
|
||||
"deny_repositories": ["openclaw/clawsweeper-state", "openclaw/.github"],
|
||||
"allow_repo_name_pattern": "^[A-Za-z0-9_.-]+$",
|
||||
"prompt_note": "Use the {target_repo} source tree and current main branch. This repository is using the generic OpenClaw onboarding profile, so keep review conservative: issues are review/comment-only, and pull requests may be auto-closed only when the exact change is certainly already implemented on main.",
|
||||
"apply_close_rules": {
|
||||
"issue": [],
|
||||
"pull_request": ["implemented_on_main"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,9 @@ Important source files:
|
||||
|
||||
- `src/clawsweeper.ts`: item selection, cadence, planning, review, dashboard,
|
||||
and status JSON
|
||||
- `config/target-repositories.json`: configured non-core target repositories
|
||||
and the conservative `openclaw/*` exact-review fallback
|
||||
- `docs/target-repositories.md`: target onboarding and rollout checklist
|
||||
- `src/repair/workflow-utils.ts`: GitHub Actions output shaping for plans
|
||||
- `results/sweep-status/<repo-slug>.json`: generated state consumed by the
|
||||
dashboard
|
||||
@ -75,6 +78,14 @@ group, so targeted maintainer checks do not wait behind broad normal backfill.
|
||||
- issues are review/comment-only; PRs may auto-close only when already
|
||||
implemented on `main`
|
||||
|
||||
Other `openclaw/*` repositories:
|
||||
|
||||
- exact event/manual review: supported through the generic conservative
|
||||
fallback after the target dispatcher and GitHub App installation are present
|
||||
- scheduled review/apply/audit: not enabled automatically
|
||||
- issues are review/comment-only; PRs may auto-close only when already
|
||||
implemented on `main`
|
||||
|
||||
Manual `workflow_dispatch` can override `target_repo`, `item_number`,
|
||||
`item_numbers`, `batch_size`, `shard_count`, `hot_intake`, and apply inputs.
|
||||
Exact item dispatches use a dedicated concurrency group and exact planner
|
||||
|
||||
@ -22,6 +22,13 @@ For issue and PR dispatch, copy this workflow into each target repository as
|
||||
`.github/workflows/clawsweeper-dispatch.yml`, or merge these triggers and the
|
||||
`Dispatch exact ClawSweeper review` step into an existing combined dispatcher:
|
||||
|
||||
Target repositories no longer need a TypeScript profile before exact event
|
||||
review can run. Any installed `openclaw/*` repository that is not denied in
|
||||
`config/target-repositories.json` uses the conservative generic profile:
|
||||
issues stay open, and PRs can auto-close only when already implemented on
|
||||
`main`. Add a config entry only when the repo should appear in the dashboard or
|
||||
needs repo-specific review guidance.
|
||||
|
||||
```yaml
|
||||
name: ClawSweeper Dispatch
|
||||
|
||||
|
||||
64
docs/target-repositories.md
Normal file
64
docs/target-repositories.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Target Repositories
|
||||
|
||||
Read when enabling ClawSweeper for another OpenClaw repository, changing
|
||||
`config/target-repositories.json`, or debugging `Unsupported target repo`
|
||||
failures.
|
||||
|
||||
ClawSweeper has two target-repository paths:
|
||||
|
||||
- configured dashboard targets in `config/target-repositories.json`
|
||||
- a conservative generic fallback for exact event/manual reviews of
|
||||
`openclaw/*` repositories
|
||||
|
||||
`openclaw/openclaw` remains a built-in profile because it has broader
|
||||
auto-close policy. Other configured targets default to safer repo-local rules:
|
||||
issues are review/comment-only, and PRs may auto-close only when the same
|
||||
change is certainly already implemented on `main`.
|
||||
|
||||
## Generic OpenClaw Fallback
|
||||
|
||||
The fallback lets a newly installed OpenClaw repo dispatch to ClawSweeper
|
||||
without a TypeScript change. It is intentionally narrow:
|
||||
|
||||
- owner must be `openclaw`
|
||||
- repo name must match `allow_repo_name_pattern`
|
||||
- denied repositories are rejected
|
||||
- issues cannot be auto-closed
|
||||
- PRs can auto-close only for `implemented_on_main`
|
||||
- scheduled dashboard/backfill rows are not added automatically
|
||||
|
||||
This is enough for event-driven review after the target repo has the dispatcher
|
||||
workflow and GitHub App installation. It is not a blanket scheduled rollout.
|
||||
|
||||
## Add One Repository
|
||||
|
||||
1. Install the ClawSweeper GitHub App on the target repository.
|
||||
2. Add or merge the target dispatcher from
|
||||
[`docs/target-dispatcher.md`](target-dispatcher.md).
|
||||
3. Ensure the target repo can read the org or repo
|
||||
`CLAWSWEEPER_APP_PRIVATE_KEY` secret.
|
||||
4. Open, edit, or comment on a target issue/PR and confirm a dispatcher run
|
||||
appears in the target repo.
|
||||
5. Confirm the receiver run appears in
|
||||
`https://github.com/openclaw/clawsweeper/actions`.
|
||||
6. Confirm the target item gets one durable ClawSweeper review comment.
|
||||
|
||||
For a repo that should appear in the README dashboard or scheduled queues, add
|
||||
it to `config/target-repositories.json` with an explicit prompt note and
|
||||
close-policy block. Keep the default policy unless the repo has a documented
|
||||
reason to allow broader issue closes.
|
||||
|
||||
## Add Many Repositories
|
||||
|
||||
Batch rollout should be incremental:
|
||||
|
||||
- install the app and dispatcher on a small group first
|
||||
- leave scheduled backfill off
|
||||
- verify event review/comment sync on one issue or PR per repo
|
||||
- add config entries for repos that should show in the dashboard
|
||||
- enable scheduled backfill/apply only after repo-specific safety rules exist
|
||||
|
||||
If a target dispatch reaches ClawSweeper but receiver token creation fails, the
|
||||
App is usually not installed on that target repo. If the target workflow skips
|
||||
before dispatch, the target repo usually cannot access
|
||||
`CLAWSWEEPER_APP_PRIVATE_KEY`.
|
||||
@ -1,3 +1,7 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export type RepositoryItemKind = "issue" | "pull_request";
|
||||
export type RepositoryCloseReason =
|
||||
| "implemented_on_main"
|
||||
@ -20,6 +24,30 @@ export interface RepositoryProfile {
|
||||
applyCloseRules: Partial<Record<RepositoryItemKind, readonly RepositoryCloseReason[]>>;
|
||||
}
|
||||
|
||||
interface TargetRepositoryConfig {
|
||||
schemaVersion: 1;
|
||||
repositories: readonly ConfiguredRepositoryProfile[];
|
||||
openclawFallback?: OpenClawFallbackConfig;
|
||||
}
|
||||
|
||||
interface ConfiguredRepositoryProfile {
|
||||
targetRepo: string;
|
||||
displayName: string;
|
||||
checkoutDir: string;
|
||||
docsUrl?: string;
|
||||
communityUrl?: string;
|
||||
promptNote: string;
|
||||
applyCloseRules: Partial<Record<RepositoryItemKind, readonly RepositoryCloseReason[]>>;
|
||||
}
|
||||
|
||||
interface OpenClawFallbackConfig {
|
||||
owner: string;
|
||||
denyRepositories: readonly string[];
|
||||
allowRepoNamePattern: RegExp;
|
||||
promptNote: string;
|
||||
applyCloseRules: Partial<Record<RepositoryItemKind, readonly RepositoryCloseReason[]>>;
|
||||
}
|
||||
|
||||
const OPENCLAW_CLOSE_REASONS: readonly RepositoryCloseReason[] = [
|
||||
"implemented_on_main",
|
||||
"cannot_reproduce",
|
||||
@ -30,60 +58,32 @@ const OPENCLAW_CLOSE_REASONS: readonly RepositoryCloseReason[] = [
|
||||
"stale_insufficient_info",
|
||||
];
|
||||
|
||||
const ALL_CLOSE_REASONS: readonly RepositoryCloseReason[] = [...OPENCLAW_CLOSE_REASONS, "none"];
|
||||
const CLOSE_REASON_SET = new Set<RepositoryCloseReason>(ALL_CLOSE_REASONS);
|
||||
const ITEM_KIND_SET = new Set<RepositoryItemKind>(["issue", "pull_request"]);
|
||||
|
||||
export const DEFAULT_TARGET_REPO = "openclaw/openclaw";
|
||||
|
||||
export const REPOSITORY_PROFILES: readonly RepositoryProfile[] = [
|
||||
{
|
||||
targetRepo: DEFAULT_TARGET_REPO,
|
||||
slug: "openclaw-openclaw",
|
||||
displayName: "OpenClaw",
|
||||
checkoutDir: "openclaw",
|
||||
docsUrl: "https://docs.openclaw.ai",
|
||||
communityUrl: "https://clawhub.ai/",
|
||||
promptNote:
|
||||
"Use the OpenClaw source tree, docs, changelog, and current main branch. Close proposals may use the normal OpenClaw stale/duplicate/not-in-repo/implemented-on-main policy when evidence is strong.",
|
||||
applyCloseRules: {
|
||||
issue: OPENCLAW_CLOSE_REASONS,
|
||||
pull_request: OPENCLAW_CLOSE_REASONS.filter((reason) => reason !== "stale_insufficient_info"),
|
||||
},
|
||||
},
|
||||
{
|
||||
targetRepo: "openclaw/clawhub",
|
||||
slug: "openclaw-clawhub",
|
||||
displayName: "ClawHub",
|
||||
checkoutDir: "clawhub",
|
||||
communityUrl: "https://clawhub.ai/",
|
||||
promptNote:
|
||||
"Use the ClawHub source tree and current main branch. Review every issue and PR with the same evidence standard, but only propose auto-close for pull requests that are certainly implemented on main. Keep everything else open.",
|
||||
applyCloseRules: {
|
||||
issue: [],
|
||||
pull_request: ["implemented_on_main"],
|
||||
},
|
||||
},
|
||||
{
|
||||
targetRepo: "openclaw/clawsweeper",
|
||||
slug: "openclaw-clawsweeper",
|
||||
displayName: "ClawSweeper",
|
||||
checkoutDir: "clawsweeper",
|
||||
promptNote:
|
||||
"Use the ClawSweeper source tree and current main branch. Review bot automation, workflow, and documentation changes conservatively. Only propose auto-close for pull requests that are certainly implemented on main; keep issues open for maintainer triage.",
|
||||
applyCloseRules: {
|
||||
issue: [],
|
||||
pull_request: ["implemented_on_main"],
|
||||
},
|
||||
},
|
||||
{
|
||||
targetRepo: "openclaw/fs-safe",
|
||||
slug: "openclaw-fs-safe",
|
||||
displayName: "fs-safe",
|
||||
checkoutDir: "fs-safe",
|
||||
promptNote:
|
||||
"Use the fs-safe source tree and current main branch. Review filesystem-safety, path-handling, and package changes conservatively. Only propose auto-close for pull requests that are certainly implemented on main; keep issues open for maintainer triage.",
|
||||
applyCloseRules: {
|
||||
issue: [],
|
||||
pull_request: ["implemented_on_main"],
|
||||
},
|
||||
const CORE_OPENCLAW_PROFILE: RepositoryProfile = {
|
||||
targetRepo: DEFAULT_TARGET_REPO,
|
||||
slug: "openclaw-openclaw",
|
||||
displayName: "OpenClaw",
|
||||
checkoutDir: "openclaw",
|
||||
docsUrl: "https://docs.openclaw.ai",
|
||||
communityUrl: "https://clawhub.ai/",
|
||||
promptNote:
|
||||
"Use the OpenClaw source tree, docs, changelog, and current main branch. Close proposals may use the normal OpenClaw stale/duplicate/not-in-repo/implemented-on-main policy when evidence is strong.",
|
||||
applyCloseRules: {
|
||||
issue: OPENCLAW_CLOSE_REASONS,
|
||||
pull_request: OPENCLAW_CLOSE_REASONS.filter((reason) => reason !== "stale_insufficient_info"),
|
||||
},
|
||||
};
|
||||
|
||||
const TARGET_REPOSITORY_CONFIG = readTargetRepositoryConfig();
|
||||
|
||||
export const REPOSITORY_PROFILES: RepositoryProfile[] = [
|
||||
CORE_OPENCLAW_PROFILE,
|
||||
...TARGET_REPOSITORY_CONFIG.repositories.map(configuredRepositoryProfile),
|
||||
];
|
||||
|
||||
export function repositoryProfileFor(targetRepo: string): RepositoryProfile {
|
||||
@ -91,12 +91,14 @@ export function repositoryProfileFor(targetRepo: string): RepositoryProfile {
|
||||
const profile = REPOSITORY_PROFILES.find(
|
||||
(candidate) => normalizeRepo(candidate.targetRepo) === normalized,
|
||||
);
|
||||
if (!profile) {
|
||||
throw new Error(
|
||||
`Unsupported target repo: ${targetRepo}. Known repos: ${REPOSITORY_PROFILES.map((candidate) => candidate.targetRepo).join(", ")}`,
|
||||
);
|
||||
}
|
||||
return profile;
|
||||
if (profile) return profile;
|
||||
|
||||
const fallback = fallbackRepositoryProfile(normalized);
|
||||
if (fallback) return fallback;
|
||||
|
||||
throw new Error(
|
||||
`Unsupported target repo: ${targetRepo}. Known repos: ${REPOSITORY_PROFILES.map((candidate) => candidate.targetRepo).join(", ")}. Generic fallback: ${fallbackDescription()}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function repositoryProfileForSlug(slug: string): RepositoryProfile | undefined {
|
||||
@ -114,3 +116,181 @@ export function isAutoCloseAllowed(
|
||||
): boolean {
|
||||
return Boolean(profile.applyCloseRules[kind]?.includes(reason));
|
||||
}
|
||||
|
||||
function configuredRepositoryProfile(profile: ConfiguredRepositoryProfile): RepositoryProfile {
|
||||
const targetRepo = normalizeRepo(profile.targetRepo);
|
||||
const result: RepositoryProfile = {
|
||||
targetRepo,
|
||||
slug: slugForRepo(targetRepo),
|
||||
displayName: profile.displayName,
|
||||
checkoutDir: profile.checkoutDir,
|
||||
promptNote: profile.promptNote,
|
||||
applyCloseRules: profile.applyCloseRules,
|
||||
};
|
||||
if (profile.docsUrl) result.docsUrl = profile.docsUrl;
|
||||
if (profile.communityUrl) result.communityUrl = profile.communityUrl;
|
||||
return result;
|
||||
}
|
||||
|
||||
function fallbackRepositoryProfile(normalizedTargetRepo: string): RepositoryProfile | undefined {
|
||||
const fallback = TARGET_REPOSITORY_CONFIG.openclawFallback;
|
||||
if (!fallback) return undefined;
|
||||
|
||||
const [owner, repoName] = normalizedTargetRepo.split("/");
|
||||
if (!owner || !repoName || owner !== fallback.owner) return undefined;
|
||||
if (fallback.denyRepositories.includes(normalizedTargetRepo)) return undefined;
|
||||
if (!fallback.allowRepoNamePattern.test(repoName)) return undefined;
|
||||
|
||||
return {
|
||||
targetRepo: normalizedTargetRepo,
|
||||
slug: slugForRepo(normalizedTargetRepo),
|
||||
displayName: repoName,
|
||||
checkoutDir: repoName,
|
||||
promptNote: fallback.promptNote
|
||||
.replaceAll("{target_repo}", normalizedTargetRepo)
|
||||
.replaceAll("{repo_name}", repoName),
|
||||
applyCloseRules: fallback.applyCloseRules,
|
||||
};
|
||||
}
|
||||
|
||||
function fallbackDescription(): string {
|
||||
const fallback = TARGET_REPOSITORY_CONFIG.openclawFallback;
|
||||
if (!fallback) return "disabled";
|
||||
const denied =
|
||||
fallback.denyRepositories.length === 0 ? "" : ` except ${fallback.denyRepositories.join(", ")}`;
|
||||
return `${fallback.owner}/*${denied}`;
|
||||
}
|
||||
|
||||
function slugForRepo(targetRepo: string): string {
|
||||
return targetRepo.replace(/[^A-Za-z0-9_.-]+/g, "-");
|
||||
}
|
||||
|
||||
function readTargetRepositoryConfig(
|
||||
filePath = join(repoRoot(), "config", "target-repositories.json"),
|
||||
): TargetRepositoryConfig {
|
||||
if (!existsSync(filePath)) return { schemaVersion: 1, repositories: [] };
|
||||
const parsed = JSON.parse(readFileSync(filePath, "utf8")) as unknown;
|
||||
return validateTargetRepositoryConfig(parsed);
|
||||
}
|
||||
|
||||
function validateTargetRepositoryConfig(value: unknown): TargetRepositoryConfig {
|
||||
const config = record(value, "target repository config");
|
||||
const schemaVersion = numberValue(config.schema_version, "schema_version");
|
||||
if (schemaVersion !== 1)
|
||||
throw new Error(`Unsupported target repository config schema: ${schemaVersion}`);
|
||||
const repositories = arrayValue(config.repositories, "repositories").map((entry, index) =>
|
||||
validateConfiguredRepositoryProfile(entry, `repositories[${index}]`),
|
||||
);
|
||||
const result: TargetRepositoryConfig = { schemaVersion: 1, repositories };
|
||||
if (config.openclaw_fallback !== undefined) {
|
||||
result.openclawFallback = validateOpenClawFallbackConfig(config.openclaw_fallback);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateConfiguredRepositoryProfile(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): ConfiguredRepositoryProfile {
|
||||
const profile = record(value, label);
|
||||
const result: ConfiguredRepositoryProfile = {
|
||||
targetRepo: repoValue(profile.target_repo, `${label}.target_repo`),
|
||||
displayName: stringValue(profile.display_name, `${label}.display_name`),
|
||||
checkoutDir: pathSegmentValue(profile.checkout_dir, `${label}.checkout_dir`),
|
||||
promptNote: stringValue(profile.prompt_note, `${label}.prompt_note`),
|
||||
applyCloseRules: closeRulesValue(profile.apply_close_rules, `${label}.apply_close_rules`),
|
||||
};
|
||||
if (profile.docs_url !== undefined) {
|
||||
result.docsUrl = stringValue(profile.docs_url, `${label}.docs_url`);
|
||||
}
|
||||
if (profile.community_url !== undefined) {
|
||||
result.communityUrl = stringValue(profile.community_url, `${label}.community_url`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateOpenClawFallbackConfig(value: unknown): OpenClawFallbackConfig {
|
||||
const fallback = record(value, "openclaw_fallback");
|
||||
const pattern = stringValue(
|
||||
fallback.allow_repo_name_pattern,
|
||||
"openclaw_fallback.allow_repo_name_pattern",
|
||||
);
|
||||
return {
|
||||
owner: stringValue(fallback.owner, "openclaw_fallback.owner").toLowerCase(),
|
||||
denyRepositories: arrayValue(
|
||||
fallback.deny_repositories,
|
||||
"openclaw_fallback.deny_repositories",
|
||||
).map((entry, index) =>
|
||||
normalizeRepo(repoValue(entry, `openclaw_fallback.deny_repositories[${index}]`)),
|
||||
),
|
||||
allowRepoNamePattern: new RegExp(pattern),
|
||||
promptNote: stringValue(fallback.prompt_note, "openclaw_fallback.prompt_note"),
|
||||
applyCloseRules: closeRulesValue(
|
||||
fallback.apply_close_rules,
|
||||
"openclaw_fallback.apply_close_rules",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function closeRulesValue(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): Partial<Record<RepositoryItemKind, readonly RepositoryCloseReason[]>> {
|
||||
const rules = record(value, label);
|
||||
const result: Partial<Record<RepositoryItemKind, RepositoryCloseReason[]>> = {};
|
||||
for (const [kind, reasons] of Object.entries(rules)) {
|
||||
if (!ITEM_KIND_SET.has(kind as RepositoryItemKind)) {
|
||||
throw new Error(`${label}.${kind} has unsupported item kind`);
|
||||
}
|
||||
result[kind as RepositoryItemKind] = arrayValue(reasons, `${label}.${kind}`).map(
|
||||
(reason, index) => closeReasonValue(reason, `${label}.${kind}[${index}]`),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function closeReasonValue(value: unknown, label: string): RepositoryCloseReason {
|
||||
const reason = stringValue(value, label) as RepositoryCloseReason;
|
||||
if (!CLOSE_REASON_SET.has(reason))
|
||||
throw new Error(`${label} has unsupported close reason: ${reason}`);
|
||||
return reason;
|
||||
}
|
||||
|
||||
function repoValue(value: unknown, label: string): string {
|
||||
const repo = normalizeRepo(stringValue(value, label));
|
||||
if (!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/.test(repo)) throw new Error(`${label} must be owner/repo`);
|
||||
return repo;
|
||||
}
|
||||
|
||||
function pathSegmentValue(value: unknown, label: string): string {
|
||||
const segment = stringValue(value, label);
|
||||
if (!/^[A-Za-z0-9_.-]+$/.test(segment)) throw new Error(`${label} must be a safe path segment`);
|
||||
return segment;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, label: string): string {
|
||||
if (typeof value !== "string" || value.trim() === "")
|
||||
throw new Error(`${label} must be a string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown, label: string): number {
|
||||
if (typeof value !== "number") throw new Error(`${label} must be a number`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function arrayValue(value: unknown, label: string): unknown[] {
|
||||
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`${label} must be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function repoRoot(): string {
|
||||
return dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
}
|
||||
|
||||
@ -20,6 +20,32 @@ test("repositoryProfileFor supports fs-safe event reviews", () => {
|
||||
assert.deepEqual(profile.applyCloseRules.pull_request, ["implemented_on_main"]);
|
||||
});
|
||||
|
||||
test("generic OpenClaw fallback supports conservative event-only onboarding", () => {
|
||||
const profile = repositoryProfileFor("OpenClaw/example-tool");
|
||||
|
||||
assert.equal(profile.targetRepo, "openclaw/example-tool");
|
||||
assert.equal(profile.slug, "openclaw-example-tool");
|
||||
assert.equal(profile.displayName, "example-tool");
|
||||
assert.equal(profile.checkoutDir, "example-tool");
|
||||
assert.match(profile.promptNote, /generic OpenClaw onboarding profile/);
|
||||
assert.deepEqual(profile.applyCloseRules.issue, []);
|
||||
assert.deepEqual(profile.applyCloseRules.pull_request, ["implemented_on_main"]);
|
||||
});
|
||||
|
||||
test("generic OpenClaw fallback keeps denied repositories unsupported", () => {
|
||||
assert.throws(
|
||||
() => repositoryProfileFor("openclaw/clawsweeper-state"),
|
||||
/Unsupported target repo: openclaw\/clawsweeper-state/,
|
||||
);
|
||||
});
|
||||
|
||||
test("generic fallback does not support repositories outside OpenClaw", () => {
|
||||
assert.throws(
|
||||
() => repositoryProfileFor("other-org/example-tool"),
|
||||
/Unsupported target repo: other-org\/example-tool/,
|
||||
);
|
||||
});
|
||||
|
||||
test("profile lookup normalizes candidate target repos as well as input", () => {
|
||||
const mixedCaseProfile = {
|
||||
...REPOSITORY_PROFILES[0],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user