mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-12 23:04:14 +00:00
* Add Copilot app for Beginners to the Learning Hub * Fix course fragment anchors and extend page coverage * Apply suggestion from @aaronpowell --------- Co-authored-by: Aaron Powell <me@aaron-powell.com>
152 lines
7.4 KiB
JavaScript
152 lines
7.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
|
|
import {
|
|
DOCS_DIR, IMAGES_DIR, adaptMarkdown, approvedPages, planImport,
|
|
} from './app-for-beginners-sync.mjs';
|
|
|
|
const sha = 'a'.repeat(40);
|
|
const nextSha = 'b'.repeat(40);
|
|
const date = '2026-10-04';
|
|
const directories = [
|
|
'00-setup', '01-tour-the-app', '02-sessions-worktrees-context', '03-development-workflows',
|
|
'04-skills-custom-agents', '05-mcp-plugins', '06-canvases', '07-automations',
|
|
];
|
|
|
|
function fixture() {
|
|
const source = new Map([
|
|
['README.md', Buffer.from('# GitHub Copilot app for Beginners\n\n[Start](00-setup/)\n')],
|
|
['LICENSE', Buffer.from('MIT License\nCopyright GitHub, Inc.\n')],
|
|
['assets/assignment.webp', Buffer.from([0, 1, 2, 255])],
|
|
['GLOSSARY.md', Buffer.from('## Worktree\n')],
|
|
['samples/book-app-web/README.md', Buffer.from('# Sample\n')],
|
|
['.github/scripts/setup-training-scenarios.js', Buffer.from('// source only\n')],
|
|
]);
|
|
for (const directory of directories) {
|
|
source.set(`${directory}/README.md`, Buffer.from(
|
|
'\n\n[Home](../README.md)\n',
|
|
));
|
|
}
|
|
return source;
|
|
}
|
|
|
|
function plan(source, local = new Map(), commit = sha, importDate = date) {
|
|
return planImport({
|
|
sha: commit, date: importDate, paths: new Set(source.keys()),
|
|
readSource: async (filename) => {
|
|
assert.ok(source.has(filename), `Missing source: ${filename}`);
|
|
return source.get(filename);
|
|
},
|
|
readLocal: async (filename) => local.get(filename),
|
|
});
|
|
}
|
|
|
|
function applyToMap(result, previous = new Map()) {
|
|
const local = new Map(previous);
|
|
for (const filename of result.changed) {
|
|
if (result.outputs.has(filename)) local.set(filename, result.outputs.get(filename));
|
|
else local.delete(filename);
|
|
}
|
|
return local;
|
|
}
|
|
|
|
test('imports only nine pages and referenced binary assets; repeat and unrelated commits are noops', async () => {
|
|
const source = fixture();
|
|
const initial = await plan(source);
|
|
assert.equal(initial.pages, 9);
|
|
assert.equal(initial.assets, 1);
|
|
assert.deepEqual(initial.outputs.get(`${IMAGES_DIR}/overview/assignment.webp`), source.get('assets/assignment.webp'));
|
|
assert.ok(![...initial.outputs.keys()].some((filename) => filename.includes('samples/')));
|
|
const local = applyToMap(initial);
|
|
assert.deepEqual((await plan(source, local, sha, '2026-10-05')).changed, []);
|
|
source.set('samples/book-app-web/src/App.tsx', Buffer.from('// unrelated\n'));
|
|
assert.deepEqual((await plan(source, local, nextSha)).changed, []);
|
|
assert.equal(JSON.parse(local.get(`${DOCS_DIR}/source.json`)).sourceCommit, sha);
|
|
});
|
|
|
|
test('cache loss and pending PR do not advance the main baseline or miss older changes', async () => {
|
|
const source = fixture();
|
|
const main = applyToMap(await plan(source));
|
|
source.set('00-setup/README.md', Buffer.from('A visible typo fix.\n'));
|
|
const pendingPlan = await plan(source, main, nextSha, '2026-11-04');
|
|
assert.equal(pendingPlan.baseline, sha);
|
|
assert.ok(pendingPlan.changed.includes(`${DOCS_DIR}/00-setup.md`));
|
|
const pending = applyToMap(pendingPlan, main);
|
|
assert.deepEqual((await plan(source, pending, nextSha)).changed, []);
|
|
assert.equal(JSON.parse(main.get(`${DOCS_DIR}/source.json`)).sourceCommit, sha);
|
|
assert.ok((await plan(source, main, nextSha)).changed.length > 0);
|
|
source.set('01-tour-the-app/README.md', Buffer.from('A later change while the PR is open.\n'));
|
|
assert.ok((await plan(source, pending, 'c'.repeat(40))).changed.includes(`${DOCS_DIR}/01-tour-the-app.md`));
|
|
});
|
|
|
|
test('asset-only updates change bytes and the dates of pages that use them', async () => {
|
|
const source = fixture();
|
|
const local = applyToMap(await plan(source));
|
|
source.set('assets/assignment.webp', Buffer.from([0, 3, 4, 255]));
|
|
const result = await plan(source, local, nextSha, '2026-10-05');
|
|
assert.ok(result.changed.includes(`${IMAGES_DIR}/overview/assignment.webp`));
|
|
assert.match(result.outputs.get(`${DOCS_DIR}/06-canvases.md`).toString(), /lastUpdated: 2026-10-05/);
|
|
});
|
|
|
|
test('renames keep chapter routes stable; deletions prune only manifest-owned files; additions stay upstream', async () => {
|
|
const source = fixture();
|
|
const local = applyToMap(await plan(source));
|
|
source.set('01-renamed/README.md', source.get('01-tour-the-app/README.md'));
|
|
source.delete('01-tour-the-app/README.md');
|
|
source.delete('07-automations/README.md');
|
|
source.set('08-extra/README.md', Buffer.from('New out-of-scope chapter\n'));
|
|
const result = await plan(source, local, nextSha);
|
|
assert.equal(result.pages, 8);
|
|
assert.ok(result.outputs.has(`${DOCS_DIR}/01-tour-the-app.md`));
|
|
assert.ok(result.deleted.includes(`${DOCS_DIR}/07-automations.md`));
|
|
assert.ok(!result.outputs.has(`${DOCS_DIR}/08-extra.md`));
|
|
assert.throws(() => approvedPages(new Set(['00-a/README.md', '00-b/README.md'])), /Ambiguous/);
|
|
});
|
|
|
|
test('rewrites links and images without changing prose, code, prompts, or inline sample paths', () => {
|
|
const paths = new Set(fixture().keys());
|
|
const pages = approvedPages(paths);
|
|
const markdown = [
|
|
'[Chapter](../00-setup/README.md#seed-the-repository)',
|
|
'[Glossary](../GLOSSARY.md#worktree)',
|
|
'[`setup` script](../.github/scripts/setup-training-scenarios.js)',
|
|
'[Sample](../samples/book-app-web/)',
|
|
'[](../LICENSE)',
|
|
'[home]: ../README.md',
|
|
'<img src="../assets/assignment.webp" alt="Assignment" width="800" />',
|
|
'<details>\n<summary>Optional</summary>\n\n## Nested heading\n\n</details>',
|
|
'> [!NOTE]\n> Keep this note.',
|
|
'Use `samples/book-app-web` and `@samples/book-app-web`.',
|
|
'```text\n[do not rewrite](../README.md)\ncd samples/book-app-web\n```',
|
|
].join('\n');
|
|
const { body } = adaptMarkdown(markdown, '06-canvases/README.md', pages, paths);
|
|
assert.match(body, /\/learning-hub\/app-for-beginners\/00-setup\/#seed-the-repository/);
|
|
assert.match(body, /\/blob\/main\/GLOSSARY.md#worktree/);
|
|
assert.match(body, /\/blob\/main\/.github\/scripts\/setup-training-scenarios.js/);
|
|
assert.match(body, /\/tree\/main\/samples\/book-app-web/);
|
|
assert.match(body, /\]\(https:\/\/github.com\/github\/copilot-app-for-beginners\/blob\/main\/LICENSE\)/);
|
|
assert.match(body, /\[home\]: \/learning-hub\/app-for-beginners\//);
|
|
assert.match(body, /\/images\/learning-hub\/copilot-app-for-beginners\/overview\/assignment.webp/);
|
|
for (const unchanged of [
|
|
'<details>\n<summary>Optional</summary>\n\n## Nested heading\n\n</details>',
|
|
'> [!NOTE]\n> Keep this note.',
|
|
'Use `samples/book-app-web` and `@samples/book-app-web`.',
|
|
'```text\n[do not rewrite](../README.md)\ncd samples/book-app-web\n```',
|
|
]) assert.ok(body.includes(unchanged));
|
|
});
|
|
|
|
test('source failures, invalid commits, missing links and unsafe deletions remain errors', async () => {
|
|
const source = fixture();
|
|
await assert.rejects(plan(source, new Map(), 'main'), /full source commit/);
|
|
const local = applyToMap(await plan(source));
|
|
source.set('README.md', Buffer.from('[Broken](missing.md)\n'));
|
|
await assert.rejects(plan(source, local), /Missing source link/);
|
|
source.set('README.md', Buffer.from('[Outside](../../other)\n'));
|
|
await assert.rejects(plan(source, local), /leaves the repository/);
|
|
const valid = fixture();
|
|
const manifest = JSON.parse(local.get(`${DOCS_DIR}/source.json`));
|
|
manifest.files.push({ local: 'CODEOWNERS' });
|
|
local.set(`${DOCS_DIR}/source.json`, Buffer.from(JSON.stringify(manifest)));
|
|
await assert.rejects(plan(valid, local), /Unsafe deletion/);
|
|
});
|