Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/scripts/extract-workflow-js.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Extract a named, inlined JavaScript block from a GitHub workflow YAML and
// load it as a module.
//
// Workflows sometimes inline JS into a `script:` literal (e.g. so a reusable
// workflow needs no checkout when called cross-repo). To keep that JS unit-
// testable, wrap the block in sentinel comments that NAME it:
//
// // ----- extract-js:<block>:begin -----
// ...the functions...
// // ----- extract-js:<block>:end -----
//
// `load({workflow, block})` reads that block, dedents it (the inverse of the
// indent it carries inside the `script:` literal, so the result is byte-
// identical to standalone source), appends `module.exports` for every top-level
// declaration, writes a temp file, and requires it. Tests then exercise exactly
// what the workflow runs. Nothing here is specific to any one workflow or block.
"use strict";

const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");

// Repo root, so callers can pass a repo-relative workflow path.
const REPO_ROOT = path.join(__dirname, "..", "..");

function markerPair(block) {
if (!block || !/^[\w.-]+$/.test(block))
throw new Error(`invalid block name: ${JSON.stringify(block)}`);
return { begin: `extract-js:${block}:begin`, end: `extract-js:${block}:end` };
}

// The dedented JavaScript source of the named block within `yamlText`.
function extractSource(yamlText, block) {
const { begin, end } = markerPair(block);
const bi = yamlText.indexOf(begin);
const ei = yamlText.indexOf(end);
if (bi < 0 || ei < 0 || ei < bi)
throw new Error(`extract-js markers for block "${block}" not found`);
const between = yamlText.slice(yamlText.indexOf("\n", bi) + 1, yamlText.lastIndexOf("\n", ei) + 1);
const lines = between.split("\n");
let min = Infinity;
for (const l of lines) {
if (l.trim() === "") continue;
const indent = l.length - l.replace(/^\s+/, "").length;
if (indent < min) min = indent;
}
if (!isFinite(min)) min = 0;
return lines.map((l) => l.slice(min)).join("\n").replace(/\s+$/, "") + "\n";
}

function resolveWorkflow(workflow) {
if (!workflow) throw new Error("workflow path is required");
return path.isAbsolute(workflow) ? workflow : path.join(REPO_ROOT, workflow);
}

// `extractSource` + a `module.exports` of every top-level function/const,
// ready to write to a .js file.
function buildModule({ workflow, block }) {
const src = extractSource(fs.readFileSync(resolveWorkflow(workflow), "utf8"), block);
const names = new Set();
const re = /^(?:async\s+)?function\s+([A-Za-z_$][\w$]*)|^const\s+([A-Za-z_$][\w$]*)\s*=/gm;
let m;
while ((m = re.exec(src))) names.add(m[1] || m[2]);
return `"use strict";\n${src}\nmodule.exports = { ${[...names].join(", ")} };\n`;
}

// Build, write to a unique temp file, and require it.
function load({ workflow, block }) {
const code = buildModule({ workflow, block });
const tmp = path.join(os.tmpdir(), `wf-extract-${block}-${process.pid}-${Date.now()}.js`);
fs.writeFileSync(tmp, code);
try {
return require(tmp);
} finally {
fs.unlinkSync(tmp);
}
}

module.exports = { extractSource, buildModule, load };

// CLI: `node extract-workflow-js.js <workflow-yaml> <block> [out.js]`
if (require.main === module) {
const [workflow, block, out] = process.argv.slice(2);
if (!workflow || !block) {
console.error("usage: extract-workflow-js.js <workflow-yaml> <block> [out.js]");
process.exit(2);
}
const code = buildModule({ workflow, block });
if (out) {
fs.writeFileSync(out, code);
console.log(`wrote ${out}`);
} else {
process.stdout.write(code);
}
}
91 changes: 91 additions & 0 deletions .github/scripts/pr-assign.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Unit tests for the assignee/reviewer selection (the JS port of
// select_assignee_and_reviewers), run against the copy INLINED in
// pr-board-sync.yml (the single source of truth), extracted at run time.
// No deps; run with: node .github/scripts/pr-assign.test.js
// Mirrors TestSelectAssigneeAndReviewers in the slang-skills test_pr_sweep.py.
"use strict";

const assert = require("node:assert");
const { selectAssigneeAndReviewers } = require("./extract-workflow-js.js").load({
workflow: ".github/workflows/pr-board-sync.yml",
block: "assignment",
});

const OWNERS = new Set(["owner1", "owner2"]);
const COLLAB = new Set(["owner1", "owner2", "dev1", "dev2"]); // owners are also collaborators
const MAINT = "maintainer";

function select(issue, committers, extra) {
return selectAssigneeAndReviewers({
issueAssignees: issue, committersBySignal: committers,
owners: OWNERS, collaborators: COLLAB, author: "author", maintainer: MAINT,
...(extra || {}),
});
}

const tests = [];
const test = (name, fn) => tests.push([name, fn]);

test("issue assignee wins", () => {
const { assignee, reviewers } = select(["owner2"], ["owner1", "dev1"]);
assert.strictEqual(assignee, "owner2");
assert.ok(reviewers.includes("owner2"));
});

test("commit-signal owner when no issue", () => {
const { assignee } = select([], ["owner1", "owner2"]);
assert.strictEqual(assignee, "owner1");
});

test("maintainer fallback", () => {
const { assignee, reviewers } = select([], ["dev1"]);
assert.strictEqual(assignee, "maintainer");
assert.deepStrictEqual(reviewers, ["maintainer", "dev1"]);
});

test("extra reviewer is top collaborator-not-owner", () => {
const { assignee, reviewers } = select([], ["dev2", "owner1", "dev1"]);
assert.strictEqual(assignee, "owner1");
assert.deepStrictEqual(reviewers, ["owner1", "dev2"]);
});

test("no collaborator committer means only assignee", () => {
const { reviewers } = select([], ["owner1", "owner2"]);
assert.deepStrictEqual(reviewers, ["owner1"]);
});

test("author never requested as reviewer", () => {
const { assignee, reviewers } = selectAssigneeAndReviewers({
issueAssignees: [], committersBySignal: ["dev1"], owners: OWNERS,
collaborators: COLLAB, author: "maintainer", maintainer: "maintainer",
});
assert.strictEqual(assignee, "maintainer");
assert.deepStrictEqual(reviewers, ["dev1"]); // author(maintainer) excluded
});

test("real existing reviewer blocks adding", () => {
const { assignee, reviewers } = select([], ["dev2", "owner1"], {
existingReviewers: ["dave"], ignoredReviewers: new Set(["bmillsNV"]),
});
assert.strictEqual(assignee, "owner1");
assert.deepStrictEqual(reviewers, []);
});

test("ignored and bot reviewers do not count as existing", () => {
const { assignee, reviewers } = select([], ["dev2", "owner1"], {
existingReviewers: ["bmillsNV", "copilot[bot]"],
botAuthors: ["nv-slang-bot"], ignoredReviewers: new Set(["bmillsNV"]),
});
assert.strictEqual(assignee, "owner1");
assert.deepStrictEqual(reviewers, ["owner1", "dev2"]);
});

(async () => {
let passed = 0, failed = 0;
for (const [name, fn] of tests) {
try { await fn(); passed++; }
catch (e) { failed++; console.error(`FAIL: ${name}\n ${(e && e.stack) || e}`); }
}
console.log(`${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
})();
185 changes: 185 additions & 0 deletions .github/scripts/pr-signal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Unit tests for the committer-signal ranking (the JS port of pr_signal.py),
// run against the copy INLINED in pr-board-sync.yml (the single source of
// truth), extracted at run time. No deps; run with:
// node .github/scripts/pr-signal.test.js
// Mirrors the pure-helper and cache tests in the slang-skills test_pr_sweep.py.
"use strict";

const assert = require("node:assert");
const s = require("./extract-workflow-js.js").load({
workflow: ".github/workflows/pr-board-sync.yml",
block: "assignment",
});

const tests = [];
const test = (name, fn) => tests.push([name, fn]);
const approx = (a, b) => assert.ok(Math.abs(a - b) < 1e-9, `${a} !~= ${b}`);

// --- classification --------------------------------------------------------
test("classify_is_bot variants", () => {
const bots = ["nv-slang-bot", "slang-coworker-nanoclaw"];
assert.ok(s.classifyIsBot("nv-slang-bot", bots));
assert.ok(s.classifyIsBot("nv-slang-bot[bot]", bots));
assert.ok(s.classifyIsBot("app/nv-slang-bot", bots));
assert.ok(s.classifyIsBot("slang-coworker-nanoclaw[bot]", bots));
assert.ok(!s.classifyIsBot("some-human", bots));
});

test("is_bot_login", () => {
const bots = ["nv-slang-bot"];
assert.ok(s.isBotLogin("github-actions[bot]", bots));
assert.ok(s.isBotLogin("nv-slang-bot", bots));
assert.ok(s.isBotLogin("", bots));
assert.ok(!s.isBotLogin("alice", bots));
});

// --- attribution -----------------------------------------------------------
const BOTS = ["nv-slang-bot"];
test("attribute: human author credited", () =>
assert.strictEqual(s.attributeCommit("alice", null, "author", BOTS), "alice"));
test("attribute: author-self uses approver", () =>
assert.strictEqual(s.attributeCommit("author", "carol", "author", BOTS), "carol"));
test("attribute: author-self no approver dropped", () =>
assert.strictEqual(s.attributeCommit("author", null, "author", BOTS), null));
test("attribute: author-self approver-is-author dropped", () =>
assert.strictEqual(s.attributeCommit("author", "author", "author", BOTS), null));
test("attribute: bot author uses approver", () =>
assert.strictEqual(s.attributeCommit("nv-slang-bot", "carol", "author", BOTS), "carol"));
test("attribute: unmapped author uses approver", () =>
assert.strictEqual(s.attributeCommit("", "carol", "author", BOTS), "carol"));
test("attribute: bot author no approver dropped", () =>
assert.strictEqual(s.attributeCommit("nv-slang-bot", null, "author", BOTS), null));
test("attribute: bot author approver-is-pr-author dropped", () =>
assert.strictEqual(s.attributeCommit("nv-slang-bot", "author", "author", BOTS), null));
test("attribute: bot author approver-is-bot dropped", () =>
assert.strictEqual(s.attributeCommit("nv-slang-bot", "nv-slang-bot", "author", BOTS), null));

// --- weighting -------------------------------------------------------------
const TABLE = [["source/slang/**", 3.0], ["source/**", 2.0], ["tests/**", 1.0]];
test("multiplier most-specific wins", () => {
assert.strictEqual(s.matchFileMultiplier("source/slang/x.cpp", TABLE, 1.0), 3.0);
assert.strictEqual(s.matchFileMultiplier("source/core/x.cpp", TABLE, 1.0), 2.0);
assert.strictEqual(s.matchFileMultiplier("tests/x.slang", TABLE, 1.0), 1.0);
assert.strictEqual(s.matchFileMultiplier("README.md", TABLE, 1.0), 1.0);
});

test("per_file_signals", () => {
const sig = s.perFileSignals({ "source/slang/a.cpp": 10.0, "tests/b.slang": 10.0 }, TABLE, 1.0);
assert.deepStrictEqual(sig, { "source/slang/a.cpp": 30.0, "tests/b.slang": 10.0 });
});

test("top_k_weights normalizes", () => {
const w = s.topKWeights({ a: 30.0, b: 10.0 }, 10);
approx(w.a, 0.75); approx(w.b, 0.25);
});
test("top_k_weights limits", () => {
const w = s.topKWeights({ a: 3.0, b: 2.0, c: 1.0 }, 2);
assert.deepStrictEqual(new Set(Object.keys(w)), new Set(["a", "b"]));
});
test("top_k_weights empty when no signal", () =>
assert.deepStrictEqual(s.topKWeights({ a: 0.0 }, 5), {}));

test("overall_signal sums weight*loc", () => {
const overall = s.overallSignal(
{ f1: 0.75, f2: 0.25 }, { f1: { alice: 40.0, bob: 10.0 }, f2: { bob: 20.0 } });
approx(overall.alice, 30.0);
approx(overall.bob, 0.75 * 10 + 0.25 * 20);
});

test("rank_logins descending", () =>
assert.deepStrictEqual(s.rankLogins({ alice: 30.0, bob: 12.5 }), ["alice", "bob"]));

// --- tiebreak bookkeeping --------------------------------------------------
test("needs_tiebreak close", () =>
assert.ok(s.needsTiebreak({ a: 10.0, b: 8.0 }, ["a", "b"], 1.5)));
test("no tiebreak clear winner", () =>
assert.ok(!s.needsTiebreak({ a: 20.0, b: 8.0 }, ["a", "b"], 1.5)));
test("no tiebreak single candidate", () =>
assert.ok(!s.needsTiebreak({ a: 5.0 }, ["a"], 1.5)));
test("no tiebreak second zero", () =>
assert.ok(!s.needsTiebreak({ a: 5.0, b: 0.0 }, ["a", "b"], 1.5)));

test("merge_refined reorders top-n keeps tail", () =>
assert.deepStrictEqual(
s.mergeRefined(["a", "b", "c", "d"], 2, { a: 1.0, b: 5.0 }), ["b", "a", "c", "d"]));
test("merge_refined all finalists", () =>
assert.deepStrictEqual(s.mergeRefined(["a", "b"], 2, { a: 1.0, b: 2.0 }), ["b", "a"]));

// --- cache behavior (fake gh) ----------------------------------------------
function countingGh() {
const gh = { graphqlCalls: 0, commitCalls: 0 };
gh.graphql = async (query) => {
gh.graphqlCalls++;
const target = {};
let i = 0;
while (query.includes(`f${i}:`)) i++;
for (let j = 0; j < Math.max(i, 1); j++) {
target[`f${j}`] = { nodes: [{
oid: `sha${j}`, additions: 10, deletions: 2,
author: { user: { login: "alice" } },
associatedPullRequests: { nodes: [] },
}] };
}
return { repository: { defaultBranchRef: { target } } };
};
gh.commitFiles = async () => {
gh.commitCalls++;
return [{ filename: "a.cpp", additions: 5, deletions: 1 }];
};
return gh;
}

test("file history cached across calls", async () => {
const gh = countingGh();
const cache = new Map();
await s.fetchFileHistories(gh, "o/r", ["a.cpp", "b.cpp"], "S", 5, cache);
assert.strictEqual(gh.graphqlCalls, 1);
await s.fetchFileHistories(gh, "o/r", ["a.cpp", "b.cpp"], "S", 5, cache);
assert.strictEqual(gh.graphqlCalls, 1); // fully cached
await s.fetchFileHistories(gh, "o/r", ["a.cpp", "c.cpp"], "S", 5, cache);
assert.strictEqual(gh.graphqlCalls, 2); // only the uncached path
});

test("file history keyed by repo", async () => {
const gh = countingGh();
const cache = new Map();
await s.fetchFileHistories(gh, "o/r1", ["a.cpp"], "S", 5, cache);
await s.fetchFileHistories(gh, "o/r2", ["a.cpp"], "S", 5, cache);
assert.strictEqual(gh.graphqlCalls, 2); // same path, different repo -> not shared
});

test("commit file loc cached by repo+sha", async () => {
const gh = countingGh();
const cache = new Map();
assert.strictEqual(await s.commitFileLoc(gh, "o/r", "deadbeef", "a.cpp", cache), 5.0);
await s.commitFileLoc(gh, "o/r", "deadbeef", "a.cpp", cache); // cached
assert.strictEqual(gh.commitCalls, 1);
await s.commitFileLoc(gh, "o/r", "cafef00d", "a.cpp", cache); // new sha
assert.strictEqual(gh.commitCalls, 2);
});

// --- end-to-end ranking (fake gh) ------------------------------------------
test("compute_committer_ranking returns ranked logins", async () => {
const gh = countingGh();
const ranked = await s.computeCommitterRanking(gh, "o/r", "author", {
locByFile: { "source/slang/a.cpp": 10.0 },
fileMultipliers: TABLE,
defaultMultiplier: 1.0,
topFiles: 10,
commits: 5,
horizonDays: 180,
botAuthors: BOTS,
since: "S",
});
assert.deepStrictEqual(ranked, ["alice"]); // only alice authored the history
});

(async () => {
let passed = 0, failed = 0;
for (const [name, fn] of tests) {
try { await fn(); passed++; }
catch (e) { failed++; console.error(`FAIL: ${name}\n ${(e && e.stack) || e}`); }
}
console.log(`${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
})();
Loading
Loading