Skip to content

Commit c239b47

Browse files
accesswatchGitHub Workshop Botclaude
authored
fix(learning-room): seed peer simulation issue, branch, and PR during progression (#238)
Challenges 1-13 tell students to comment on, review, or compare with the seeded 'Peer Simulation: Welcome Link Needs Context' issue and the 'Peer Simulation: Improve contribution guidance' pull request. Those artifacts were created by scripts/classroom/Seed-PeerSimulation.ps1, which was deleted in the GitHub Classroom removal (c567eb7) without moving the behavior into hybrid provisioning, so every provisioned room was missing them (support issue Community-Access/support#58). The learning room progression script now seeds all peer simulation artifacts (labels, both issues, the peer-simulation/review-pr branch with docs/samples/peer-review-practice.md, and the PR) idempotently on every progression run, so new rooms get them at Challenge 1 seed time and existing rooms self-repair on their next challenge close. Seeding failures log a warning instead of failing progression. The student-progression workflow gains pull-requests: write for the PR creation. Co-authored-by: GitHub Workshop Bot <workshop@example.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 01ae447 commit c239b47

3 files changed

Lines changed: 370 additions & 6 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Regression tests for peer simulation seeding in the student-facing
2+
// challenge progression script. Challenges 1-13 reference a seeded
3+
// "Peer Simulation: Welcome Link Needs Context" issue and a
4+
// "Peer Simulation: Improve contribution guidance" pull request. These were
5+
// originally created by scripts/classroom/Seed-PeerSimulation.ps1, which was
6+
// deleted with GitHub Classroom, leaving provisioned rooms without the
7+
// artifacts (support issue Community-Access/support#58, 2026-07-04). The
8+
// progression script must seed them idempotently on every run.
9+
const test = require('node:test');
10+
const assert = require('node:assert/strict');
11+
const path = require('node:path');
12+
13+
process.env.GITHUB_TOKEN = 'test-token';
14+
process.env.GITHUB_REPOSITORY = 'org/room';
15+
process.env.START_CHALLENGE = '';
16+
process.env.GITHUB_EVENT_NAME = '';
17+
process.env.CLOSED_ISSUE_TITLE = '';
18+
19+
// The script resolves its issue-template directory from the working
20+
// directory, so requiring it must happen from inside learning-room/.
21+
process.chdir(path.join(__dirname, '..', '..', '..', 'learning-room'));
22+
23+
const {
24+
ensurePeerSimulationArtifacts,
25+
ensureLabel,
26+
ensurePeerSimulationBranch,
27+
ensurePeerSimulationPullRequest
28+
} = require('../../../learning-room/.github/scripts/challenge-progression');
29+
30+
function fakeRequest(overrides = {}) {
31+
const calls = [];
32+
const request = async (route, options = {}) => {
33+
const method = options.method || 'GET';
34+
calls.push({ method, route, body: options.body });
35+
for (const [pattern, handler] of Object.entries(overrides)) {
36+
const [overrideMethod, routePrefix] = pattern.split(' ');
37+
if (method === overrideMethod && route.startsWith(routePrefix)) {
38+
return handler({ route, options });
39+
}
40+
}
41+
// Defaults that describe an empty, freshly provisioned room.
42+
if (method === 'GET' && route.startsWith('/repos/org/room/issues?')) {
43+
return [];
44+
}
45+
if (method === 'GET' && route.startsWith('/repos/org/room/pulls?')) {
46+
return [];
47+
}
48+
if (method === 'GET' && route.startsWith('/repos/org/room/git/ref/heads/peer-simulation')) {
49+
throw new Error('GET failed: 404 Not Found');
50+
}
51+
if (method === 'GET' && route.startsWith('/repos/org/room/git/ref/heads/main')) {
52+
return { object: { sha: 'abc123' } };
53+
}
54+
if (method === 'GET' && route.startsWith('/repos/org/room/contents/')) {
55+
throw new Error('GET failed: 404 Not Found');
56+
}
57+
if (method === 'POST' && route === '/repos/org/room/issues') {
58+
return { number: 42, html_url: 'https://example.test/issue/42' };
59+
}
60+
if (method === 'POST' && route === '/repos/org/room/pulls') {
61+
return { number: 7, html_url: 'https://example.test/pull/7' };
62+
}
63+
return null;
64+
};
65+
return { calls, request };
66+
}
67+
68+
test('seeds labels, both issues, branch, file, and PR in an empty room', async () => {
69+
const { calls, request } = fakeRequest();
70+
71+
await ensurePeerSimulationArtifacts(request);
72+
73+
const posts = calls.filter((c) => c.method === 'POST');
74+
const labelPosts = posts.filter((c) => c.route === '/repos/org/room/labels');
75+
assert.equal(labelPosts.length, 4);
76+
77+
const issuePosts = posts.filter((c) => c.route === '/repos/org/room/issues');
78+
assert.deepEqual(
79+
issuePosts.map((c) => c.body.title),
80+
[
81+
'Peer Simulation: Welcome Link Needs Context',
82+
'Peer Simulation: Review Request for Contribution Guidance'
83+
]
84+
);
85+
assert.ok(issuePosts[0].body.labels.includes('peer-simulation'));
86+
87+
const refPosts = posts.filter((c) => c.route === '/repos/org/room/git/refs');
88+
assert.equal(refPosts.length, 1);
89+
assert.equal(refPosts[0].body.ref, 'refs/heads/peer-simulation/review-pr');
90+
assert.equal(refPosts[0].body.sha, 'abc123');
91+
92+
const filePuts = calls.filter((c) => c.method === 'PUT');
93+
assert.equal(filePuts.length, 1);
94+
assert.ok(filePuts[0].route.includes('docs/samples/peer-review-practice.md'));
95+
assert.equal(filePuts[0].body.branch, 'peer-simulation/review-pr');
96+
97+
const prPosts = posts.filter((c) => c.route === '/repos/org/room/pulls');
98+
assert.equal(prPosts.length, 1);
99+
assert.equal(prPosts[0].body.title, 'Peer Simulation: Improve contribution guidance');
100+
assert.equal(prPosts[0].body.head, 'peer-simulation/review-pr');
101+
assert.equal(prPosts[0].body.base, 'main');
102+
assert.match(prPosts[0].body.body, /#42/);
103+
});
104+
105+
test('is idempotent: creates nothing when the artifacts already exist', async () => {
106+
const { calls, request } = fakeRequest({
107+
'GET /repos/org/room/issues?': () => [
108+
{ number: 5, title: 'Peer Simulation: Welcome Link Needs Context' },
109+
{ number: 6, title: 'Peer Simulation: Review Request for Contribution Guidance' }
110+
],
111+
'GET /repos/org/room/git/ref/heads/peer-simulation': () => ({ object: { sha: 'abc123' } }),
112+
'GET /repos/org/room/contents/': () => ({ sha: 'filesha' }),
113+
'GET /repos/org/room/pulls?': () => [{ number: 7 }]
114+
});
115+
116+
await ensurePeerSimulationArtifacts(request);
117+
118+
const writes = calls.filter(
119+
(c) => (c.method === 'POST' || c.method === 'PUT') && c.route !== '/repos/org/room/labels'
120+
);
121+
assert.deepEqual(writes, []);
122+
});
123+
124+
test('never throws: progression survives a seeding failure', async () => {
125+
const request = async () => {
126+
throw new Error('boom: no network');
127+
};
128+
129+
await assert.doesNotReject(ensurePeerSimulationArtifacts(request));
130+
});
131+
132+
test('ensureLabel tolerates a label that already exists', async () => {
133+
const request = async () => {
134+
throw new Error('POST /repos/org/room/labels failed: 422 already_exists');
135+
};
136+
137+
await assert.doesNotReject(
138+
ensureLabel({ name: 'peer-simulation', color: '5319e7', description: 'x' }, request)
139+
);
140+
});
141+
142+
test('branch seeding skips the commit when the practice file is present', async () => {
143+
const { calls, request } = fakeRequest({
144+
'GET /repos/org/room/git/ref/heads/peer-simulation': () => ({ object: { sha: 'abc123' } }),
145+
'GET /repos/org/room/contents/': () => ({ sha: 'filesha' })
146+
});
147+
148+
await ensurePeerSimulationBranch(request);
149+
150+
assert.deepEqual(calls.filter((c) => c.method !== 'GET'), []);
151+
});
152+
153+
test('PR seeding skips creation when a PR for the branch exists in any state', async () => {
154+
const { calls, request } = fakeRequest({
155+
'GET /repos/org/room/pulls?': () => [{ number: 7, state: 'closed' }]
156+
});
157+
158+
await ensurePeerSimulationPullRequest(6, request);
159+
160+
assert.deepEqual(calls.filter((c) => c.method === 'POST'), []);
161+
});

0 commit comments

Comments
 (0)