125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
extractConfiguredReposFromYaml,
|
|
fetchIssues,
|
|
mergeStringLists,
|
|
parseStringArraySetting,
|
|
} from "./index.ts";
|
|
|
|
function makeIssue(number: number, title: string, repo: string) {
|
|
return {
|
|
number,
|
|
title,
|
|
labels: [],
|
|
state: "open",
|
|
assignees: [],
|
|
createdAt: `2026-03-${String(number).padStart(2, "0")}T00:00:00Z`,
|
|
url: `https://github.com/${repo}/issues/${number}`,
|
|
};
|
|
}
|
|
|
|
test("extractConfiguredReposFromYaml reads every project repo", () => {
|
|
const rawYaml = `
|
|
port: 3000
|
|
projects:
|
|
app:
|
|
repo: acme/app
|
|
path: ~/code/app
|
|
docs:
|
|
repo: "acme/docs" # keep quoted repos working
|
|
path: ~/code/docs
|
|
notifiers:
|
|
openclaw:
|
|
plugin: openclaw
|
|
`;
|
|
|
|
assert.deepEqual(extractConfiguredReposFromYaml(rawYaml), ["acme/app", "acme/docs"]);
|
|
});
|
|
|
|
test("fetchIssues queries every configured repo when repo is omitted", () => {
|
|
const ghCalls: string[] = [];
|
|
const result = fetchIssues(
|
|
{ aoCwd: "/tmp/work" },
|
|
{},
|
|
{
|
|
getConfiguredRepos: () => ["acme/app", "acme/docs"],
|
|
runGh: (_config, args) => {
|
|
const repoIndex = args.indexOf("-R");
|
|
const repo = repoIndex >= 0 ? args[repoIndex + 1] : "default";
|
|
ghCalls.push(repo);
|
|
|
|
if (repo === "acme/app") {
|
|
return { ok: true, output: JSON.stringify([makeIssue(1, "App bug", repo)]) };
|
|
}
|
|
if (repo === "acme/docs") {
|
|
return { ok: true, output: JSON.stringify([makeIssue(2, "Docs bug", repo)]) };
|
|
}
|
|
|
|
return { ok: false, error: `unexpected repo: ${repo}` };
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(ghCalls, ["acme/app", "acme/docs"]);
|
|
assert.equal(result.ok, true);
|
|
if (!result.ok) return;
|
|
|
|
assert.deepEqual(result.scannedRepos, ["acme/app", "acme/docs"]);
|
|
assert.equal(result.warnings.length, 0);
|
|
assert.deepEqual(
|
|
result.issues.map((issue) => issue.repository),
|
|
["acme/docs", "acme/app"],
|
|
);
|
|
});
|
|
|
|
test("fetchIssues surfaces GitHub failures instead of reporting an empty board", () => {
|
|
const result = fetchIssues(
|
|
{ aoCwd: "/tmp/work" },
|
|
{},
|
|
{
|
|
getConfiguredRepos: () => ["acme/app"],
|
|
runGh: () => ({ ok: false, error: "gh auth token missing" }),
|
|
},
|
|
);
|
|
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.match(result.error, /gh auth token missing/);
|
|
});
|
|
|
|
test("fetchIssues keeps partial failures visible when at least one repo succeeds", () => {
|
|
const result = fetchIssues(
|
|
{ aoCwd: "/tmp/work" },
|
|
{},
|
|
{
|
|
getConfiguredRepos: () => ["acme/app", "acme/docs"],
|
|
runGh: (_config, args) => {
|
|
const repoIndex = args.indexOf("-R");
|
|
const repo = repoIndex >= 0 ? args[repoIndex + 1] : "default";
|
|
if (repo === "acme/app") {
|
|
return { ok: true, output: JSON.stringify([makeIssue(3, "App bug", repo)]) };
|
|
}
|
|
return { ok: false, error: "gh not authenticated for docs repo" };
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(result.ok, true);
|
|
if (!result.ok) return;
|
|
assert.equal(result.issues.length, 1);
|
|
assert.deepEqual(result.warnings, ["acme/docs: gh not authenticated for docs repo"]);
|
|
});
|
|
|
|
test("allowlist helpers preserve existing entries while adding AO requirements", () => {
|
|
assert.deepEqual(mergeStringLists(["custom:tools", "group:plugins"], ["group:plugins"]), [
|
|
"custom:tools",
|
|
"group:plugins",
|
|
]);
|
|
assert.deepEqual(parseStringArraySetting('["group:plugins","custom:tools"]'), [
|
|
"group:plugins",
|
|
"custom:tools",
|
|
]);
|
|
assert.deepEqual(parseStringArraySetting("null"), []);
|
|
});
|