Code Comprehension Part 2: Tests & Cross-File Flow
Part 2 of the code comprehension tutorial. Return a few days after Part 1, then practice testing hypotheses with Node.js behavior tests and mapping interactions across React and backend files in a roughly one-hour session.
Tests as Specification Beacons
Why this matters
If you just finished the basics tutorial today, stop here and come back in two or three days. That pause forces retrieval, which is exactly what makes the skill stick. When you return, this step starts with one of the strongest professional beacons: tests that describe the behavior before you read the implementation.
🎯 You will learn to
- Use Node.js tests to form a behavior hypothesis
- Verify one expectation by reading the smallest relevant helper
- Decide when a test is a trustworthy beacon and when it needs implementation evidence
Retrieval Warm-Up
Before opening the code, recall the Part 1 routine without looking:
Orient → Predict → Beacon-hunt → Descend selectively → Verify → Summarize
Then name one switch trigger. For example: if a behavior depends on a risky branch, stop scanning and trace one input through that branch.
Tests First
Students often read tests only after code fails. For comprehension, read them before production code:
- Test names tell you the intended behavior.
- Setup data tells you the important objects.
- Assertions tell you the invariant or postcondition.
- Failing cases tell you where the implementation is risky.
Contract-to-Mechanism Map
After reading tests, write a tiny contract map before implementation:
| Contract clue | Mechanism to verify |
|---|---|
| “returns only sessions that fit” | A filter or guard using minutes |
| “prefers a matching topic” | A ranking or scoring rule using topic |
| “low energy gives solo sessions a boost” | A branch involving energy and mode |
This keeps your attention on the behavior the tests actually promise. You can always trace more later, but first you should know what you are trying to prove.
In this step, read sessionPlanner.test.js before sessionPlanner.js. Your first hypothesis should be about behavior, not implementation:
“This planner probably returns short study sessions that fit the time box, prefers matching topics, and adjusts for energy level.”
Then inspect only the helper that can confirm each part.
One syntax note: .sort((left, right) => scoreSession(right, request) - scoreSession(left, request))
puts higher-scoring sessions first. You do not need to master every
sorting detail here; just connect that expression to the ranking behavior
named by the tests.
const SESSIONS = [
{ id: 1, title: "Trace one Python helper", topic: "python", minutes: 10, mode: "solo" },
{ id: 2, title: "Pair-read a React component", topic: "react", minutes: 18, mode: "pair" },
{ id: 3, title: "Map an Express route", topic: "node", minutes: 16, mode: "solo" },
{ id: 4, title: "Sketch a full review plan", topic: "review", minutes: 30, mode: "pair" },
];
function scoreSession(session, request) {
let score = 0;
if (session.topic === request.topic) {
score += 5;
}
if (session.minutes <= request.minutes) {
score += 2;
}
if (request.energy === "low" && session.mode === "solo") {
score += 2;
}
if (session.minutes > request.minutes) {
score -= 10;
}
return score;
}
function pickSessionPlan(request) {
return SESSIONS
.filter(session => session.minutes <= request.minutes)
.sort((left, right) => scoreSession(right, request) - scoreSession(left, request))
.slice(0, 2)
.map(session => session.title);
}
module.exports = { pickSessionPlan, scoreSession };
const test = require("node:test");
const assert = require("node:assert/strict");
const { pickSessionPlan, scoreSession } = require("./sessionPlanner");
test("returns the only session that fits a 12-minute request", () => {
const plan = pickSessionPlan({ topic: "python", minutes: 12, energy: "high" });
assert.deepEqual(plan, ["Trace one Python helper"]);
});
test("places a matching topic first when it fits", () => {
const plan = pickSessionPlan({ topic: "node", minutes: 18, energy: "high" });
assert.equal(plan[0], "Map an Express route");
});
test("scoreSession gives solo sessions a low-energy bonus", () => {
const soloScore = scoreSession(
{ title: "Solo", topic: "python", minutes: 10, mode: "solo" },
{ topic: "react", minutes: 20, energy: "low" }
);
const pairScore = scoreSession(
{ title: "Pair", topic: "python", minutes: 10, mode: "pair" },
{ topic: "react", minutes: 20, energy: "low" }
);
assert.equal(soloScore, pairScore + 2);
});
Step 1 — Knowledge Check
Min. score: 80%
1. What behavior hypothesis should the tests give you before reading sessionPlanner.js?
The tests give a specification-layer summary: fit the time, rank matching topics, and adjust scoring for low energy.
2. Which implementation region verifies the hypothesis that unavailable long sessions are excluded?
The filtering predicate is the shortest verification path for the minutes invariant.
3. A low-energy request should favor solo sessions. What is the smallest useful trace?
Targeted tracing means choosing the one comparison that exercises the uncertain branch.
4. Which cues make the tests useful beacons rather than just grading tools? (select all that apply)
Tests help comprehension when their names, setup, and assertions reveal intent.
5. What behavior is not fully specified by these tests?
Tests are powerful specification beacons, but they are not complete specifications. Strong readers also notice what a test suite leaves open.
Node Hypothesis Sprint
Why this matters
Backend code often spreads behavior across route handlers, services, and data modules. Reading everything from top to bottom burns attention on details before you know what matters. This sprint asks you to read tests as beacons, choose a route, and trace only the path needed to answer the questions. Let’s see how far you can make it.
🎯 You will learn to
- Apply tests-as-specification reading to a multi-file Node.js example
- Trace request data from a route handler into a service helper
- Identify when a 404 behavior is implemented in the route rather than the data layer
Reading Goal
You have 10 minutes. Read in this order:
__tests__/sessionRoutes.test.jsroutes/sessionRoutes.js- Only the service helper needed for the current question
- Write a one-line contract-to-mechanism note: expected behavior → code region that enforces it.
fakeResponse() is only a small test double for Express’s res object.
Read its status() and json() methods only enough to know what the
route sent; do not treat it as production behavior.
Use reading-notes.md to write your reading path. Core target: answer
the route and service questions. Stretch target: locate where a field
leak would begin.
Do not edit the code. Your job is to answer with evidence.
# Node Hypothesis Sprint Notes
Contract hypothesis:
Top 3 beacons:
1.
2.
3.
What I can ignore for this question:
Smallest trace:
Final EiPE summary:
const sessions = [
{ id: 1, title: "Python filter drill", topic: "python", minutes: 12, nextStep: "Trace one hidden item" },
{ id: 2, title: "React callback map", topic: "react", minutes: 18, nextStep: "Follow the callback prop" },
{ id: 3, title: "Node route map", topic: "node", minutes: 16, nextStep: "Read tests before routes" },
];
module.exports = { sessions };
const { sessions } = require("../data/sessions");
function findSessions(topic) {
if (!topic || topic === "all") {
return [...sessions];
}
return sessions.filter(session => session.topic === topic);
}
function findSessionById(id) {
return sessions.find(session => session.id === id);
}
function toSummary(session) {
return {
id: session.id,
title: session.title,
minutes: session.minutes,
};
}
function withNextStep(session) {
return {
...toSummary(session),
topic: session.topic,
nextStep: session.nextStep,
};
}
module.exports = { findSessions, findSessionById, toSummary, withNextStep };
const {
findSessions,
findSessionById,
toSummary,
withNextStep,
} = require("../services/sessionService");
function listSessions(req, res) {
const topic = req.query.topic || "all";
const summaries = findSessions(topic).map(toSummary);
return res.json(summaries);
}
function sessionDetails(req, res) {
const id = Number(req.params.id);
const session = findSessionById(id);
if (!session) {
return res.status(404).json({ error: "session not found" });
}
return res.json(withNextStep(session));
}
module.exports = { listSessions, sessionDetails };
const test = require("node:test");
const assert = require("node:assert/strict");
const { listSessions, sessionDetails } = require("../routes/sessionRoutes");
function fakeResponse() {
return {
statusCode: 200,
body: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(payload) {
this.body = payload;
return this;
},
};
}
test("GET /sessions?topic=react returns only React summaries", () => {
const res = fakeResponse();
listSessions({ query: { topic: "react" } }, res);
assert.equal(res.statusCode, 200);
assert.deepEqual(res.body, [
{ id: 2, title: "React callback map", minutes: 18 },
]);
});
test("GET /sessions/:id includes the next recommended step", () => {
const res = fakeResponse();
sessionDetails({ params: { id: "3" } }, res);
assert.equal(res.statusCode, 200);
assert.equal(res.body.nextStep, "Read tests before routes");
});
test("GET /sessions/:id returns 404 for a missing session", () => {
const res = fakeResponse();
sessionDetails({ params: { id: "99" } }, res);
assert.equal(res.statusCode, 404);
assert.deepEqual(res.body, { error: "session not found" });
});
Step 2 — Knowledge Check
Min. score: 80%
1. For GET /sessions?topic=react, what is the request-data path?
The query value is read at the route boundary, passed to the service, and only then mapped into response summaries.
2. Where is the missing-session 404 response implemented?
The route owns HTTP response shape. The service owns data lookup.
3. Which helper should you inspect to answer why nextStep appears in the details response but not the list response?
listSessions() maps through toSummary(), while sessionDetails() returns withNextStep(session).
4. Which files are central implementation evidence for the missing-session 404 behavior? (select all that apply)
Attention control means separating specification evidence, central implementation evidence, and supporting data.
5. A bug report says the list endpoint is leaking nextStep. Where should the search start?
Concept location starts from the user-visible concept, then follows the route boundary to the helper that shapes the response.
Cross-File Interaction Maps
Why this matters
Large code rarely keeps cause and effect in one place. A click in one React file may update state in another file and change rendering in a third. Cross-file comprehension means turning scattered code into a small interaction map.
🎯 You will learn to
- Map event flow across parent and child React components
- Identify which file owns state and which files only receive props
- Verify a cross-file hypothesis by following one callback path
The Interaction Map
Use a four-column map. This one is partly worked; fill the blanks in your own notes before checking the code.
| Trigger | File / component | Evidence | Effect |
|---|---|---|---|
| User types in search box | LessonSearch |
onChange={event => onQueryChange(...)} |
Calls parent callback |
| Parent callback runs | _____ | onQueryChange={setQuery} |
Updates query state |
| Derived list recalculates | App |
_____ | Chooses visible lessons |
| List renders | LessonList |
lessons.map(...) |
_____ |
The point is not to memorize React syntax. The point is to name the connection between separated locations.
Predict Before Reading
Before opening every file, predict which file owns:
- the search text,
- the selected lesson,
- the display of matching lessons.
Then verify by following the callback props.
.lesson-shell {
max-width: 60rem;
margin: 0 auto;
padding: 1.5rem;
}
.lesson-card {
border: 1px solid #ccd6e0;
border-radius: 6px;
padding: 1rem;
margin-block: 0.75rem;
}
const LESSONS = [
{ id: 1, title: "Python filters", tag: "python", minutes: 12 },
{ id: 2, title: "React callbacks", tag: "react", minutes: 18 },
{ id: 3, title: "Node route tests", tag: "node", minutes: 16 },
];
function LessonSearch({ query, onQueryChange }) {
return (
<label>
Search lessons
<input
value={query}
onChange={event => onQueryChange(event.target.value)}
placeholder="python, react, node..."
/>
</label>
);
}
function LessonList({ lessons, selectedId, onSelectLesson }) {
return (
<section aria-label="Matching lessons">
{lessons.map(lesson => (
<article className="lesson-card" key={lesson.id}>
<h2 className="h5">{lesson.title}</h2>
<p>{lesson.minutes} minutes</p>
<button type="button" onClick={() => onSelectLesson(lesson.id)}>
{selectedId === lesson.id ? "Selected" : "Select"}
</button>
</article>
))}
</section>
);
}
function App() {
const [query, setQuery] = React.useState("");
const [selectedId, setSelectedId] = React.useState(null);
const normalizedQuery = query.trim().toLowerCase();
const visibleLessons = LESSONS.filter(lesson =>
lesson.title.toLowerCase().includes(normalizedQuery) ||
lesson.tag.includes(normalizedQuery)
);
const selectedLesson = LESSONS.find(lesson => lesson.id === selectedId);
return (
<main className="lesson-shell">
<h1>Lesson Finder</h1>
<LessonSearch query={query} onQueryChange={setQuery} />
{selectedLesson && (
<p role="status">Selected: {selectedLesson.title}</p>
)}
<LessonList
lessons={visibleLessons}
selectedId={selectedId}
onSelectLesson={setSelectedId}
/>
</main>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
Step 3 — Knowledge Check
Min. score: 80%
1. A user types node in the search input. What is the correct interaction path?
The search behavior crosses files through a callback prop and derived state in the parent.
2. Which file owns selectedId?
In React, state ownership is usually marked by useState; child components receive values and callback props.
3. Which evidence proves that LessonList is display-and-event wiring, not the owner of lesson filtering?
LessonList renders whatever list it receives; filtering happens before the prop crosses into the component.
4. Which cross-file links should appear in an accurate interaction map? (select all that apply)
Cross-file maps should include both upward callback flow and downward data flow.
5. When should you switch from beacon reading to a small bottom-up trace in this React example?
A callback crossing a file boundary is a good switch point: predict from the prop name, then trace one event path.
Backend Interaction Sprint
Why this matters
This final sprint combines the whole routine: orient from tests, predict the design, find beacons, trace one risky branch, and map files. It is also a concept-location task: you start with a domain idea such as “coupon” or “out of stock” and locate the files that realize it. The code is bigger than before, but the reading job is still bounded. Let’s see how far you can make it.
🎯 You will learn to
- Map request data through route, service, data, and discount helpers
- Verify one error path without tracing the entire backend
- Evaluate when a helper name is a reliable beacon for responsibility
Reading Goal
You have 15 minutes. Read __tests__/checkoutRoute.test.js first. Then answer:
- What happens when an item is out of stock?
- Where is the coupon applied?
- Which helper computes the subtotal?
- Which files participate in a successful quote?
- Which files are central, supporting, or incidental to the concept you are investigating?
Use reading-notes.md to write a narrow interaction map. Core target:
stock, subtotal, and coupon flow. Stretch target: diagnose the weak
TEA3 coupon beacon and identify the incidental receipt formatter.
Do not edit the code. Use a narrow interaction map.
# Backend Interaction Sprint Notes
Contract hypothesis:
Top 3 beacons:
1.
2.
3.
What I can ignore for this question:
Smallest trace:
Central / supporting / incidental files:
Final EiPE summary:
const items = [
{ sku: "mochi", name: "Mango Mochi", price: 4, stock: 8 },
{ sku: "ramen", name: "Late-Night Ramen", price: 7, stock: 0 },
{ sku: "tea", name: "Jasmine Tea", price: 3, stock: 15 },
];
module.exports = { items };
const { items } = require("../data/items");
function findItem(sku) {
return items.find(item => item.sku === sku);
}
function checkStock(lineItems) {
const missing = [];
for (const lineItem of lineItems) {
const item = findItem(lineItem.sku);
if (!item || item.stock < lineItem.quantity) {
missing.push(lineItem.sku);
}
}
return { ok: missing.length === 0, missing };
}
function subtotalFor(lineItems) {
return lineItems.reduce((total, lineItem) => {
const item = findItem(lineItem.sku);
return total + item.price * lineItem.quantity;
}, 0);
}
module.exports = { checkStock, subtotalFor };
function discountFor(coupon, subtotal) {
if (coupon === "STUDY10" && subtotal >= 10) {
return Math.round(subtotal * 0.10);
}
if (coupon === "TEA3") {
return 3;
}
return 0;
}
module.exports = { discountFor };
function formatReceiptLine(name, quantity, total) {
return `${quantity}x ${name}: $${total}`;
}
module.exports = { formatReceiptLine };
const { checkStock, subtotalFor } = require("../services/stockService");
const { discountFor } = require("../services/discountService");
function quoteCheckout(req, res) {
const lineItems = req.body?.items ?? [];
const stockReport = checkStock(lineItems);
if (!stockReport.ok) {
return res.status(409).json({
error: "out of stock",
missing: stockReport.missing,
});
}
const subtotal = subtotalFor(lineItems);
const discount = discountFor(req.body?.coupon, subtotal);
return res.json({
subtotal,
discount,
total: subtotal - discount,
});
}
module.exports = { quoteCheckout };
const test = require("node:test");
const assert = require("node:assert/strict");
const { quoteCheckout } = require("../routes/checkoutRoute");
function fakeResponse() {
return {
statusCode: 200,
body: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(payload) {
this.body = payload;
return this;
},
};
}
test("successful quote applies STUDY10 to an eligible subtotal", () => {
const res = fakeResponse();
quoteCheckout({
body: {
coupon: "STUDY10",
items: [
{ sku: "mochi", quantity: 2 },
{ sku: "tea", quantity: 1 },
],
},
}, res);
assert.equal(res.statusCode, 200);
assert.deepEqual(res.body, { subtotal: 11, discount: 1, total: 10 });
});
test("out-of-stock items return 409 and skip totals", () => {
const res = fakeResponse();
quoteCheckout({
body: {
coupon: "STUDY10",
items: [{ sku: "ramen", quantity: 1 }],
},
}, res);
assert.equal(res.statusCode, 409);
assert.deepEqual(res.body, { error: "out of stock", missing: ["ramen"] });
});
Step 4 — Knowledge Check
Min. score: 80%
1. Which helper computes subtotal: 11 for two mochi and one tea?
subtotalFor() reduces line items by looking up each item price and multiplying by quantity.
2. For the out-of-stock request, what is the shortest correct trace?
The relevant branch is route → stock check → early 409 response. A targeted trace stops before discount and subtotal helpers.
3. Where is the STUDY10 coupon applied in a successful quote?
The route passes the coupon and subtotal to the discount helper. The helper owns coupon rules.
4. Which files are part of the runtime flow for a successful checkout quote? (select all that apply)
A successful quote is a runtime flow: route coordination, stock and price helpers, discount rules, and catalog data.
5. When should a reader switch from top-down beacon reading to bottom-up tracing in this example?
Top-down reading is efficient until the evidence gets risky, surprising, or incomplete. Then a short bottom-up trace repairs or confirms the model.
6. A bug report says TEA3 can make tiny orders too cheap. Which beacon could mislead a fast reader, and what evidence corrects it?
Concept location starts from domain language, then false-beacon diagnosis names both the tempting inference and the exact branch that corrects it.
React Concept Location Sprint
Why this matters
Real comprehension tasks often begin with a domain idea, not a file name: “Where is pinning implemented?” or “Where does completion change?” Concept location is the skill of turning that idea into a focused search path across files. This sprint is larger than the Part 1 React examples, but the goal is still bounded. Let’s see how far you can make it.
🎯 You will learn to
- Locate a domain concept across React state, props, and child events
- Rank files as central, supporting, or incidental evidence
- Verify one cross-file interaction with a targeted trace
Reading Goal
You have 10 minutes. Your concept is pinning a lesson.
Read in this order:
App.jsxfor state ownership and derived values.LessonCard.jsxfor the click event.- Only then inspect display-only helpers if a question needs them.
Write a central/supporting/incidental map in reading-notes.md.
| Category | What it means |
|---|---|
| Central | Code that owns or changes the concept |
| Supporting | Code that provides data or displays the concept |
| Incidental | Nearby code that does not participate in the concept |
Stretch target: find one weak beacon. A CSS class like pinned-card can confirm presentation, but it is not enough to prove where pinning state lives.
# React Concept Location Notes
Concept:
Central files:
Supporting files:
Incidental files:
Strongest beacons:
1.
2.
3.
Smallest trace:
Final EiPE summary:
.board-shell {
max-width: 62rem;
margin: 0 auto;
padding: 1.5rem;
}
.pinned-card {
border: 2px solid #2774ae;
padding: 1rem;
border-radius: 6px;
}
const LESSONS = [
{ id: 1, title: "Python plan roles", topic: "python", done: true },
{ id: 2, title: "React callback descent", topic: "react", done: false },
{ id: 3, title: "Node tests as beacons", topic: "node", done: false },
{ id: 4, title: "Review risk map", topic: "review", done: true },
];
function LessonFilter({ view, onViewChange }) {
return (
<label>
View
<select value={view} onChange={event => onViewChange(event.target.value)}>
<option value="all">All</option>
<option value="open">Open</option>
<option value="done">Done</option>
</select>
</label>
);
}
function LessonCard({ lesson, isPinned, onPinLesson, onCompleteLesson }) {
return (
<article className={isPinned ? "pinned-card" : ""}>
<h2 className="h5">{lesson.title}</h2>
<p>Topic: {lesson.topic}</p>
<p>{lesson.done ? "Done" : "Still open"}</p>
<button type="button" onClick={() => onPinLesson(lesson.id)}>
{isPinned ? "Pinned" : "Pin"}
</button>
<button type="button" onClick={() => onCompleteLesson(lesson.id)}>
Mark done
</button>
</article>
);
}
function ProgressSummary({ lessons }) {
const doneCount = lessons.filter(lesson => lesson.done).length;
return <p>{doneCount} of {lessons.length} lessons complete.</p>;
}
function App() {
const [view, setView] = React.useState("all");
const [pinnedId, setPinnedId] = React.useState(2);
const [lessons, setLessons] = React.useState(LESSONS);
function handleCompleteLesson(id) {
setLessons(currentLessons => currentLessons.map(lesson =>
lesson.id === id ? { ...lesson, done: true } : lesson
));
}
const visibleLessons = lessons.filter(lesson => {
if (view === "done") {
return lesson.done;
}
if (view === "open") {
return !lesson.done;
}
return true;
});
const pinnedLesson = lessons.find(lesson => lesson.id === pinnedId);
return (
<main className="board-shell">
<h1>Lesson Board</h1>
<LessonFilter view={view} onViewChange={setView} />
<ProgressSummary lessons={lessons} />
{pinnedLesson && <p role="status">Pinned: {pinnedLesson.title}</p>}
<section aria-label="Visible lessons">
{visibleLessons.map(lesson => (
<LessonCard
key={lesson.id}
lesson={lesson}
isPinned={lesson.id === pinnedId}
onPinLesson={setPinnedId}
onCompleteLesson={handleCompleteLesson}
/>
))}
</section>
</main>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
Step 5 — Knowledge Check
Min. score: 80%1. Where is the pinning state owned?
State ownership is the central evidence for a React concept-location task.
2. A user clicks Pin on a lesson. What is the correct interaction path?
The concept crosses files through a callback prop, then returns to App as derived state and props.
3. Which file is incidental to the pinning concept, even though it is nearby?
Concept-location skill includes knowing what to ignore. Progress counting is real behavior, but it does not implement pinning.
4. Which beacons should appear in a central/supporting map for pinning? (select all that apply)
A good map distinguishes central state and callbacks from supporting presentation.
5. Which is the best false-beacon diagnosis for this sprint?
Robust top-down reading uses weak cues for orientation, then verifies ownership through state and callback evidence.
Architecture-Code Alignment Review
Why this matters
Advanced comprehension means reading code against an intended architecture, not just finding the line that produces an output. A route, service, data store, and policy helper each have different responsibilities. When those responsibilities stay visible, the code gives future readers trustworthy architectural beacons.
🎯 You will learn to
- Map a Node.js behavior from route to service to data and policy helpers
- Separate specification evidence from runtime implementation evidence
- Evaluate whether files preserve their intended responsibilities
Mini Review Packet
Requirement:
When a student completes a lesson, store the completion, return the completed lesson id, and award a three-day streak badge when the student’s completion days contain a three-day run.
Intended architecture:
| Layer | Responsibility |
|---|---|
| Route | Read HTTP request fields and choose HTTP status/response shape |
| Progress service | Coordinate completion behavior |
| Store | Save and list completion records |
| Badge service | Decide whether the records earn a badge |
| Email preview | Nearby formatting helper, not part of completion behavior |
Read __tests__/progressRoutes.test.js first, then map the requirement to code. Your final answer should name central, supporting, and incidental files.
const completions = [];
function listCompletions(userId) {
return completions
.filter(record => record.userId === userId)
.map(record => ({ ...record }));
}
function saveCompletion(record) {
const alreadySaved = completions.some(existing =>
existing.userId === record.userId &&
existing.lessonId === record.lessonId &&
existing.day === record.day
);
if (!alreadySaved) {
completions.push({ ...record });
}
return { ...record };
}
function resetCompletions() {
completions.length = 0;
}
module.exports = { listCompletions, saveCompletion, resetCompletions };
function streakBadgeFor(records) {
const days = [...new Set(records.map(record => record.day))].sort((a, b) => a - b);
let current = 0;
let previous = null;
for (const day of days) {
current = previous === null || day === previous + 1 ? current + 1 : 1;
if (current >= 3) {
return "three-day-streak";
}
previous = day;
}
return null;
}
module.exports = { streakBadgeFor };
const { listCompletions, saveCompletion } = require("../data/progressStore");
const { streakBadgeFor } = require("./badgeService");
function completeLesson({ userId, lessonId, day }) {
saveCompletion({ userId, lessonId, day });
const records = listCompletions(userId);
return {
completed: true,
lessonId,
badge: streakBadgeFor(records),
};
}
module.exports = { completeLesson };
function previewCompletionEmail(studentName, lessonTitle) {
return `${studentName} completed ${lessonTitle}.`;
}
module.exports = { previewCompletionEmail };
const { completeLesson } = require("../services/progressService");
function completeLessonRoute(req, res) {
const { userId, lessonId, day } = req.body;
if (!userId || !lessonId || !day) {
return res.status(400).json({ error: "missing completion fields" });
}
const summary = completeLesson({ userId, lessonId, day });
return res.status(201).json(summary);
}
module.exports = { completeLessonRoute };
const { beforeEach, test } = require("node:test");
const assert = require("node:assert/strict");
const { listCompletions, resetCompletions } = require("../data/progressStore");
const { completeLessonRoute } = require("../routes/progressRoutes");
function fakeResponse() {
return {
statusCode: 200,
body: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(payload) {
this.body = payload;
return this;
},
};
}
beforeEach(() => {
resetCompletions();
});
test("POST /progress/completions stores a completion and returns 201", () => {
const res = fakeResponse();
completeLessonRoute({ body: { userId: "u1", lessonId: "react-flow", day: 1 } }, res);
assert.equal(res.statusCode, 201);
assert.deepEqual(res.body, {
completed: true,
lessonId: "react-flow",
badge: null,
});
assert.deepEqual(listCompletions("u1"), [
{ userId: "u1", lessonId: "react-flow", day: 1 },
]);
});
test("third consecutive completion returns a streak badge", () => {
completeLessonRoute({ body: { userId: "u2", lessonId: "python-plan", day: 1 } }, fakeResponse());
completeLessonRoute({ body: { userId: "u2", lessonId: "react-flow", day: 2 } }, fakeResponse());
const res = fakeResponse();
completeLessonRoute({ body: { userId: "u2", lessonId: "node-route", day: 3 } }, res);
assert.equal(res.statusCode, 201);
assert.equal(res.body.badge, "three-day-streak");
});
test("missing fields return 400 without storing a completion", () => {
const res = fakeResponse();
completeLessonRoute({ body: { userId: "u3", day: 4 } }, res);
assert.equal(res.statusCode, 400);
assert.deepEqual(res.body, { error: "missing completion fields" });
assert.deepEqual(listCompletions("u3"), []);
});
Step 6 — Knowledge Check
Min. score: 80%1. Which first-pass hypothesis comes from the tests?
The tests provide the specification layer: completion storage, status shape, and streak badge behavior.
2. Where is the missing-field 400 behavior implemented?
The route owns HTTP boundary behavior, including input checks and status/response shape.
3. Which files are central runtime evidence for awarding the streak badge? (select all that apply)
Central runtime files are the ones the successful completion path actually calls.
4. Which responsibility map best matches the implementation?
Architecture-code alignment means the code vocabulary preserves the intended responsibility vocabulary.
5. Which false-beacon diagnosis is strongest?
A plausible nearby file can be a weak contextual beacon. Strong readers verify relevance through imports, calls, and tests.