Skip to content

Commit 6b22445

Browse files
committed
Use the workflow
This doesn't use the published version, but instead compiles the current version which is, itself, a form of QA
1 parent ce29611 commit 6b22445

8 files changed

Lines changed: 154 additions & 54 deletions

File tree

.github/workflows/qa.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: QA Instructions
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize]
6+
7+
permissions:
8+
pull-requests: write
9+
models: read
10+
11+
jobs:
12+
qa-instructions:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v6
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v6
20+
with:
21+
node-version-file: ".node-version"
22+
cache: "npm"
23+
24+
- name: Install dependencies
25+
run: npm ci
26+
27+
- name: Build
28+
run: npm run build
29+
30+
- name: Generate QA Instructions
31+
uses: ./
32+
with:
33+
github-token: ${{ secrets.GITHUB_TOKEN }}

src/constants.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,29 @@ export const DEFAULT_GITHUB_MODELS_MODEL = "openai/gpt-4o";
99
export const GITHUB_MODELS_BASE_URL =
1010
"https://models.github.ai/inference/chat/completions";
1111

12-
export const MAX_DIFF_CHARS = 80_000;
13-
export const MAX_CHANGED_FILES_CHARS = 60_000;
14-
export const MAX_FILE_CHARS = 10_000;
15-
export const MAX_FILE_TREE_CHARS = 20_000;
16-
export const MAX_TOTAL_CHARS = 180_000;
12+
export interface ContextLimits {
13+
maxDiffChars: number;
14+
maxChangedFilesChars: number;
15+
maxFileChars: number;
16+
maxFileTreeChars: number;
17+
maxTotalChars: number;
18+
}
19+
20+
export const ANTHROPIC_CONTEXT_LIMITS: ContextLimits = {
21+
maxDiffChars: 80_000,
22+
maxChangedFilesChars: 60_000,
23+
maxFileChars: 10_000,
24+
maxFileTreeChars: 20_000,
25+
maxTotalChars: 180_000,
26+
};
27+
28+
export const GITHUB_MODELS_CONTEXT_LIMITS: ContextLimits = {
29+
maxDiffChars: 16_000,
30+
maxChangedFilesChars: 12_000,
31+
maxFileChars: 5_000,
32+
maxFileTreeChars: 5_000,
33+
maxTotalChars: 40_000,
34+
};
1735

1836
export const SYSTEM_PROMPT = `You are an expert QA engineer reviewing a pull request. Your job is to generate clear, actionable QA testing instructions that a human tester can follow.
1937

src/context-builder.test.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { describe, it, expect } from "vitest";
22
import { buildPromptContext } from "./context-builder.js";
33
import type { PrData } from "./types.js";
4-
import { MAX_DIFF_CHARS, MAX_FILE_CHARS } from "./constants.js";
4+
import { ANTHROPIC_CONTEXT_LIMITS } from "./constants.js";
5+
6+
const limits = ANTHROPIC_CONTEXT_LIMITS;
57

68
function makePrData(overrides: Partial<PrData> = {}): PrData {
79
return {
@@ -16,7 +18,7 @@ function makePrData(overrides: Partial<PrData> = {}): PrData {
1618

1719
describe("buildPromptContext", () => {
1820
it("includes PR title and description", () => {
19-
const result = buildPromptContext(makePrData(), "");
21+
const result = buildPromptContext(makePrData(), "", limits);
2022
expect(result).toContain("**Title:** Test PR");
2123
expect(result).toContain("Test body");
2224
});
@@ -25,7 +27,7 @@ describe("buildPromptContext", () => {
2527
const data = makePrData({
2628
metadata: { title: "Test", body: "", headSha: "abc" },
2729
});
28-
const result = buildPromptContext(data, "");
30+
const result = buildPromptContext(data, "", limits);
2931
expect(result).not.toContain("**Description:**");
3032
});
3133

@@ -36,25 +38,29 @@ describe("buildPromptContext", () => {
3638
{ sha: "1234567abcdef", message: "Fix bug" },
3739
],
3840
});
39-
const result = buildPromptContext(data, "");
41+
const result = buildPromptContext(data, "", limits);
4042
expect(result).toContain("## Commits");
4143
expect(result).toContain("- abcdef1 Initial commit");
4244
expect(result).toContain("- 1234567 Fix bug");
4345
});
4446

4547
it("includes diff content", () => {
4648
const data = makePrData({ diff: "+added line\n-removed line" });
47-
const result = buildPromptContext(data, "");
49+
const result = buildPromptContext(data, "", limits);
4850
expect(result).toContain("## Diff");
4951
expect(result).toContain("+added line\n-removed line");
5052
});
5153

5254
it("truncates long diffs at line boundaries", () => {
5355
const lines = Array.from({ length: 10000 }, (_, i) => `+line ${i}`);
5456
const longDiff = lines.join("\n");
55-
expect(longDiff.length).toBeGreaterThan(MAX_DIFF_CHARS);
57+
expect(longDiff.length).toBeGreaterThan(limits.maxDiffChars);
5658

57-
const result = buildPromptContext(makePrData({ diff: longDiff }), "");
59+
const result = buildPromptContext(
60+
makePrData({ diff: longDiff }),
61+
"",
62+
limits,
63+
);
5864
expect(result).toContain("[Content truncated]");
5965
});
6066

@@ -65,15 +71,15 @@ describe("buildPromptContext", () => {
6571
{ filename: "big.ts", content: "x".repeat(500) },
6672
],
6773
});
68-
const result = buildPromptContext(data, "");
74+
const result = buildPromptContext(data, "", limits);
6975
expect(result).toContain("## Changed File Contents");
7076
// Big file should appear before small file
7177
const bigIdx = result.indexOf("big.ts");
7278
const smallIdx = result.indexOf("small.ts");
7379
expect(bigIdx).toBeLessThan(smallIdx);
7480
});
7581

76-
it("truncates individual files exceeding MAX_FILE_CHARS", () => {
82+
it("truncates individual files exceeding maxFileChars", () => {
7783
const data = makePrData({
7884
changedFiles: [
7985
{
@@ -84,21 +90,22 @@ describe("buildPromptContext", () => {
8490
},
8591
],
8692
});
87-
// The content is larger than MAX_FILE_CHARS
88-
expect(data.changedFiles[0].content.length).toBeGreaterThan(MAX_FILE_CHARS);
93+
expect(data.changedFiles[0].content.length).toBeGreaterThan(
94+
limits.maxFileChars,
95+
);
8996

90-
const result = buildPromptContext(data, "");
97+
const result = buildPromptContext(data, "", limits);
9198
expect(result).toContain("[Content truncated]");
9299
});
93100

94-
it("stops adding files when total exceeds MAX_CHANGED_FILES_CHARS", () => {
101+
it("stops adding files when total exceeds maxChangedFilesChars", () => {
95102
const files = Array.from({ length: 20 }, (_, i) => ({
96103
filename: `file${i}.ts`,
97-
content: "x".repeat(MAX_FILE_CHARS - 100),
104+
content: "x".repeat(limits.maxFileChars - 100),
98105
}));
99106
const data = makePrData({ changedFiles: files });
100107

101-
const result = buildPromptContext(data, "");
108+
const result = buildPromptContext(data, "", limits);
102109

103110
// Not all 20 files should be included
104111
const includedCount = (result.match(/### file\d+\.ts/g) || []).length;
@@ -110,7 +117,7 @@ describe("buildPromptContext", () => {
110117
const data = makePrData({
111118
fileTree: ["src/index.ts", "src/utils.ts", "package.json"],
112119
});
113-
const result = buildPromptContext(data, "");
120+
const result = buildPromptContext(data, "", limits);
114121
expect(result).toContain("## Repository File Tree");
115122
expect(result).toContain("src/index.ts");
116123
});
@@ -119,26 +126,25 @@ describe("buildPromptContext", () => {
119126
const result = buildPromptContext(
120127
makePrData(),
121128
"Focus on accessibility testing",
129+
limits,
122130
);
123131
expect(result).toContain("## Additional Instructions");
124132
expect(result).toContain("Focus on accessibility testing");
125133
});
126134

127135
it("omits additional instructions when custom prompt is empty", () => {
128-
const result = buildPromptContext(makePrData(), "");
136+
const result = buildPromptContext(makePrData(), "", limits);
129137
expect(result).not.toContain("## Additional Instructions");
130138
});
131139

132140
it("respects overall character cap", () => {
133-
// Create data that would exceed MAX_TOTAL_CHARS
141+
// Create data that would exceed maxTotalChars
134142
const data = makePrData({
135143
diff: "x".repeat(80_000),
136144
changedFiles: [{ filename: "big.ts", content: "y".repeat(60_000) }],
137145
fileTree: Array.from({ length: 5000 }, (_, i) => `path/${i}.ts`),
138146
});
139-
const result = buildPromptContext(data, "");
140-
// The result includes truncation markers, so it will be slightly over
141-
// but the core content should be capped
147+
const result = buildPromptContext(data, "", limits);
142148
expect(result).toContain("[Content truncated]");
143149
});
144150
});

src/context-builder.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import type { PrData } from "./types.js";
2-
import {
3-
MAX_DIFF_CHARS,
4-
MAX_CHANGED_FILES_CHARS,
5-
MAX_FILE_CHARS,
6-
MAX_FILE_TREE_CHARS,
7-
MAX_TOTAL_CHARS,
8-
} from "./constants.js";
2+
import type { ContextLimits } from "./constants.js";
93

104
function truncateAtLineBreak(text: string, maxChars: number): string {
115
if (text.length <= maxChars) return text;
@@ -15,7 +9,11 @@ function truncateAtLineBreak(text: string, maxChars: number): string {
159
return truncated.slice(0, cutPoint) + "\n\n[Content truncated]";
1610
}
1711

18-
export function buildPromptContext(data: PrData, customPrompt: string): string {
12+
export function buildPromptContext(
13+
data: PrData,
14+
customPrompt: string,
15+
limits: ContextLimits,
16+
): string {
1917
const sections: string[] = [];
2018

2119
// Pr title + description + commits (always included in full)
@@ -31,13 +29,13 @@ export function buildPromptContext(data: PrData, customPrompt: string): string {
3129
sections.push(`## Commits\n\n${commitLines}`);
3230
}
3331

34-
// Diff (up to MAX_DIFF_CHARS)
32+
// Diff (up to maxDiffChars)
3533
if (data.diff) {
36-
const truncatedDiff = truncateAtLineBreak(data.diff, MAX_DIFF_CHARS);
34+
const truncatedDiff = truncateAtLineBreak(data.diff, limits.maxDiffChars);
3735
sections.push(`## Diff\n\n\`\`\`diff\n${truncatedDiff}\n\`\`\``);
3836
}
3937

40-
// Changed file contents (up to MAX_CHANGED_FILES_CHARS total, MAX_FILE_CHARS per file)
38+
// Changed file contents (up to maxChangedFilesChars total, maxFileChars per file)
4139
// Sort by content length descending (most-changed files first)
4240
if (data.changedFiles.length > 0) {
4341
const sorted = [...data.changedFiles].sort(
@@ -47,10 +45,10 @@ export function buildPromptContext(data: PrData, customPrompt: string): string {
4745
let totalFileChars = 0;
4846

4947
for (const file of sorted) {
50-
if (totalFileChars >= MAX_CHANGED_FILES_CHARS) break;
48+
if (totalFileChars >= limits.maxChangedFilesChars) break;
5149

52-
const remaining = MAX_CHANGED_FILES_CHARS - totalFileChars;
53-
const perFileLimit = Math.min(MAX_FILE_CHARS, remaining);
50+
const remaining = limits.maxChangedFilesChars - totalFileChars;
51+
const perFileLimit = Math.min(limits.maxFileChars, remaining);
5452
const content = truncateAtLineBreak(file.content, perFileLimit);
5553

5654
fileBlocks.push(`### ${file.filename}\n\n\`\`\`\n${content}\n\`\`\``);
@@ -60,10 +58,13 @@ export function buildPromptContext(data: PrData, customPrompt: string): string {
6058
sections.push(`## Changed File Contents\n\n${fileBlocks.join("\n\n")}`);
6159
}
6260

63-
// File tree (lowest priority, up to MAX_FILE_TREE_CHARS)
61+
// File tree (lowest priority, up to maxFileTreeChars)
6462
if (data.fileTree.length > 0) {
6563
const treeText = data.fileTree.join("\n");
66-
const truncatedTree = truncateAtLineBreak(treeText, MAX_FILE_TREE_CHARS);
64+
const truncatedTree = truncateAtLineBreak(
65+
treeText,
66+
limits.maxFileTreeChars,
67+
);
6768
sections.push(
6869
`## Repository File Tree\n\n\`\`\`\n${truncatedTree}\n\`\`\``,
6970
);
@@ -77,8 +78,8 @@ export function buildPromptContext(data: PrData, customPrompt: string): string {
7778
let result = sections.join("\n\n");
7879

7980
// Overall cap
80-
if (result.length > MAX_TOTAL_CHARS) {
81-
result = truncateAtLineBreak(result, MAX_TOTAL_CHARS);
81+
if (result.length > limits.maxTotalChars) {
82+
result = truncateAtLineBreak(result, limits.maxTotalChars);
8283
}
8384

8485
return result;

src/index.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as ghModule from "./github.js";
2020
import * as contextBuilder from "./context-builder.js";
2121
import * as providerFactory from "./provider-factory.js";
2222
import * as comment from "./comment.js";
23+
import { GITHUB_MODELS_CONTEXT_LIMITS } from "./constants.js";
2324

2425
const mockGenerateQAInstructions = vi.fn();
2526

@@ -46,6 +47,9 @@ describe("run", () => {
4647
vi.mocked(providerFactory.createProvider).mockReturnValue({
4748
generateQAInstructions: mockGenerateQAInstructions,
4849
});
50+
vi.mocked(providerFactory.getContextLimits).mockReturnValue(
51+
GITHUB_MODELS_CONTEXT_LIMITS,
52+
);
4953

5054
vi.mocked(ghModule.getPrMetadata).mockResolvedValue({
5155
title: "Test PR",
@@ -75,6 +79,9 @@ describe("run", () => {
7579
githubToken: "fake-token",
7680
octokit: "mock-octokit",
7781
});
82+
expect(providerFactory.getContextLimits).toHaveBeenCalledWith(
83+
"github-models",
84+
);
7885
expect(ghModule.getPrMetadata).toHaveBeenCalledWith(
7986
"mock-octokit",
8087
"test-owner",
@@ -105,6 +112,7 @@ describe("run", () => {
105112
commits: [{ sha: "abc123", message: "commit" }],
106113
},
107114
"",
115+
GITHUB_MODELS_CONTEXT_LIMITS,
108116
);
109117
expect(mockGenerateQAInstructions).toHaveBeenCalledWith(
110118
expect.any(String),
@@ -179,11 +187,7 @@ describe("run", () => {
179187
contextAny.payload = originalPayload;
180188
});
181189

182-
it("calls setFailed when createProvider throws", async () => {
183-
vi.mocked(providerFactory.createProvider).mockImplementation(() => {
184-
throw new Error('Invalid provider "bad"');
185-
});
186-
190+
it("fails for invalid provider", async () => {
187191
vi.mocked(core.getInput).mockImplementation((name: string) => {
188192
const inputs: Record<string, string> = {
189193
"github-token": "fake-token",
@@ -200,6 +204,7 @@ describe("run", () => {
200204
expect(core.setFailed).toHaveBeenCalledWith(
201205
expect.stringContaining('Invalid provider "bad"'),
202206
);
207+
expect(providerFactory.createProvider).not.toHaveBeenCalled();
203208
});
204209

205210
it("calls setFailed when an error occurs", async () => {

0 commit comments

Comments
 (0)