Skip to content

Commit 3270e84

Browse files
that-github-userunknownclaude
authored
Add CI workflows, CodeQL scanning, and convergence tests (#9)
* Add CI workflows, CodeQL scanning, and convergence tests - GitHub Actions CI: build + typecheck + test on Node 22/24 - CodeQL security scanning on push/PR + weekly schedule - 9 unit tests for convergence analysis and recommendation scoring - Update test script to use tsx for direct TS execution Closes #5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove continue-on-error from test step — tests should fail the build --------- Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 016de77 commit 3270e84

File tree

4 files changed

+198
-1
lines changed

4 files changed

+198
-1
lines changed

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build:
14+
name: Build & Test
15+
runs-on: ubuntu-latest
16+
strategy:
17+
matrix:
18+
node-version: [22, 24]
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Use Node.js ${{ matrix.node-version }}
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: ${{ matrix.node-version }}
27+
cache: npm
28+
29+
- name: Install dependencies
30+
run: npm ci
31+
32+
- name: Build
33+
run: npm run build
34+
35+
- name: Check types
36+
run: npx tsc --noEmit
37+
38+
- name: Test
39+
run: npm test

.github/workflows/codeql.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
- cron: "0 6 * * 1"
10+
11+
permissions:
12+
actions: read
13+
contents: read
14+
security-events: write
15+
16+
jobs:
17+
analyze:
18+
name: Analyze
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- name: Initialize CodeQL
25+
uses: github/codeql-action/init@v3
26+
with:
27+
languages: javascript-typescript
28+
29+
- name: Autobuild
30+
uses: github/codeql-action/autobuild@v3
31+
32+
- name: Perform CodeQL Analysis
33+
uses: github/codeql-action/analyze@v3

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"scripts": {
1010
"build": "tsc",
1111
"dev": "tsx src/cli.ts",
12-
"test": "node --test dist/**/*.test.js"
12+
"test": "tsx --test src/**/*.test.ts"
1313
},
1414
"keywords": ["ai", "coding", "ensemble", "claude", "claude-code", "consensus"],
1515
"author": "",

src/scoring/convergence.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { analyzeConvergence, recommend } from "./convergence.js";
4+
import type { AgentResult } from "../types.js";
5+
6+
function makeAgent(overrides: Partial<AgentResult> & { id: number }): AgentResult {
7+
return {
8+
worktree: `/tmp/agent-${overrides.id}`,
9+
status: "success",
10+
exitCode: 0,
11+
duration: 5000,
12+
output: "",
13+
diff: "some diff",
14+
filesChanged: ["src/index.ts"],
15+
linesAdded: 10,
16+
linesRemoved: 5,
17+
...overrides,
18+
};
19+
}
20+
21+
describe("analyzeConvergence", () => {
22+
it("returns empty for no agents", () => {
23+
const result = analyzeConvergence([]);
24+
assert.deepEqual(result, []);
25+
});
26+
27+
it("returns empty when all agents failed", () => {
28+
const agents = [
29+
makeAgent({ id: 1, status: "error", diff: "" }),
30+
makeAgent({ id: 2, status: "timeout", diff: "" }),
31+
];
32+
const result = analyzeConvergence(agents);
33+
assert.deepEqual(result, []);
34+
});
35+
36+
it("groups agents that changed the same files", () => {
37+
const agents = [
38+
makeAgent({ id: 1, filesChanged: ["a.ts", "b.ts"] }),
39+
makeAgent({ id: 2, filesChanged: ["a.ts", "b.ts"] }),
40+
makeAgent({ id: 3, filesChanged: ["c.ts"] }),
41+
];
42+
const groups = analyzeConvergence(agents);
43+
44+
assert.equal(groups.length, 2);
45+
assert.deepEqual(groups[0]!.agents, [1, 2]);
46+
assert.ok(groups[0]!.similarity > groups[1]!.similarity);
47+
assert.deepEqual(groups[1]!.agents, [3]);
48+
});
49+
50+
it("handles file order differences", () => {
51+
const agents = [
52+
makeAgent({ id: 1, filesChanged: ["b.ts", "a.ts"] }),
53+
makeAgent({ id: 2, filesChanged: ["a.ts", "b.ts"] }),
54+
];
55+
const groups = analyzeConvergence(agents);
56+
57+
assert.equal(groups.length, 1);
58+
assert.deepEqual(groups[0]!.agents, [1, 2]);
59+
assert.equal(groups[0]!.similarity, 1);
60+
});
61+
62+
it("labels strong consensus at 80%+", () => {
63+
const agents = [
64+
makeAgent({ id: 1, filesChanged: ["a.ts"] }),
65+
makeAgent({ id: 2, filesChanged: ["a.ts"] }),
66+
makeAgent({ id: 3, filesChanged: ["a.ts"] }),
67+
makeAgent({ id: 4, filesChanged: ["a.ts"] }),
68+
makeAgent({ id: 5, filesChanged: ["b.ts"] }),
69+
];
70+
const groups = analyzeConvergence(agents);
71+
72+
assert.ok(groups[0]!.description.includes("Strong consensus"));
73+
assert.ok(groups[1]!.description.includes("Divergent"));
74+
});
75+
});
76+
77+
describe("recommend", () => {
78+
it("returns null for no completed agents", () => {
79+
const agents = [makeAgent({ id: 1, status: "error", diff: "" })];
80+
assert.equal(recommend(agents, [], []), null);
81+
});
82+
83+
it("prefers agents that pass tests", () => {
84+
const agents = [
85+
makeAgent({ id: 1, linesAdded: 20, linesRemoved: 10 }),
86+
makeAgent({ id: 2, linesAdded: 5, linesRemoved: 2 }),
87+
];
88+
const tests = [
89+
{ agentId: 1, passed: true },
90+
{ agentId: 2, passed: false },
91+
];
92+
const convergence = analyzeConvergence(agents);
93+
94+
assert.equal(recommend(agents, tests, convergence), 1);
95+
});
96+
97+
it("prefers agents in larger convergence group when tests are equal", () => {
98+
const agents = [
99+
makeAgent({ id: 1, filesChanged: ["a.ts"], linesAdded: 10, linesRemoved: 5 }),
100+
makeAgent({ id: 2, filesChanged: ["a.ts"], linesAdded: 10, linesRemoved: 5 }),
101+
makeAgent({ id: 3, filesChanged: ["b.ts"], linesAdded: 10, linesRemoved: 5 }),
102+
];
103+
const tests = [
104+
{ agentId: 1, passed: true },
105+
{ agentId: 2, passed: true },
106+
{ agentId: 3, passed: true },
107+
];
108+
const convergence = analyzeConvergence(agents);
109+
const rec = recommend(agents, tests, convergence);
110+
111+
// Should pick agent 1 or 2 (in the majority group), not 3
112+
assert.ok(rec === 1 || rec === 2);
113+
});
114+
115+
it("prefers smaller diffs as tiebreaker", () => {
116+
const agents = [
117+
makeAgent({ id: 1, filesChanged: ["a.ts"], linesAdded: 50, linesRemoved: 20 }),
118+
makeAgent({ id: 2, filesChanged: ["a.ts"], linesAdded: 5, linesRemoved: 2 }),
119+
];
120+
const convergence = analyzeConvergence(agents);
121+
122+
// No test results — convergence is equal, so diff size decides
123+
assert.equal(recommend(agents, [], convergence), 2);
124+
});
125+
});

0 commit comments

Comments
 (0)