Semana 1 — ¿Qué es la ingeniería de software?
Objetivos
- Distinguir la ingeniería de software de la programación: el tiempo, la escala y las decisiones con costos.
- Explicar con ejemplos la ley de Hyrum y por qué un cambio “inofensivo” puede romper a quien depende de un sistema.
- Describir qué hacen bien y qué hacen mal las herramientas de IA que escriben código, y qué evidencia hay sobre su efecto real en la productividad.
- Trabajar con Git en equipo: ramas, commits con mensajes útiles, pull requests y conflictos de merge.
Sesión A — Presentación del curso · ¿Qué es la ingeniería de software?
Lecturas:
- Software Engineering at Google (SWEG), cap. 1, What Is Software Engineering?.
Lecturas complementarias:
- SWEG, cap. 2, How to Work Well on Teams (el mito del genio y el bus factor).
Temas:
- Presentación: el programa, el proyecto con un cliente real, la política de uso de IA con registro y defensa oral, el entorno del curso.
- Ingeniería de software como programación integrada en el tiempo. La pregunta que separa un script de un sistema: ¿cuánto tiempo va a vivir este código? Un script que corre una vez y el sistema de reservas de un complejo deportivo que va a cambiar durante años necesitan prácticas distintas.
- Sostenibilidad: un proyecto es sostenible si puede reaccionar a los cambios que valen la pena (una ley nueva, una vulnerabilidad en una dependencia, un pedido del cliente) a un costo razonable.
- La ley de Hyrum: con suficientes usuarios, todo comportamiento observable de un sistema termina siendo usado por alguien. Ejemplo con Kallpa: si la API devuelve las canchas siempre ordenadas por nombre sin haberlo prometido, algún cliente HTTP va a depender de ese orden.
- Escala: políticas que escalan y políticas que no (que cada equipo actualice su copia de una dependencia a mano frente a que lo haga quien cambia la dependencia). Shifting left: encontrar un error al escribir el código cuesta menos que encontrarlo en revisión, en pruebas o en producción.
- Decisiones con costos: todo es un trade-off (tiempo de las personas, tiempo de máquina, riesgo, deuda). Una decisión se toma con datos y se revisa cuando cambian.
- Error común: medir un proyecto de software por cuánto código se escribió. Menos código que resuelve lo mismo es mejor: cada línea es algo que mantener.
Sesión B — La IA en el ciclo de desarrollo: qué hace bien, qué hace mal y cómo usarla con responsabilidad
Lecturas:
- CMU News, CMU Students Put AI Coding Tools to the Test (2025): una nota sobre un curso donde los estudiantes no escriben código a mano, y por qué se les pide aplicar prácticas de ingeniería a lo que produce la herramienta.
- METR, Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity (2025): el resumen del estudio y su nota de actualización.
Lecturas complementarias:
- DORA, State of AI-assisted Software Development 2025.
- El artículo completo del estudio de METR en arXiv.
Temas:
- Qué es una herramienta de IA para programar: un modelo de lenguaje que predice texto, con acceso (según la herramienta) a los archivos del proyecto, a la terminal y a otras herramientas. Autocompletado, chat, agente que edita archivos y corre comandos.
- Qué hace bien: código repetitivo, primeros borradores, explicar código ajeno, traducir entre lenguajes, proponer pruebas. Qué hace mal: código que compila, pasa sus propias pruebas y está mal; inventar funciones o paquetes que no existen; seguir instrucciones escondidas en lo que lee; no saber lo que el cliente quiso decir.
- Evidencia, no anécdotas. En el estudio de METR de 2025, desarrolladores experimentados en sus propios proyectos tardaron 19 % más con IA, aunque antes esperaban ir 24 % más rápido y después creían haber ido 20 % más rápido. METR advierte que las herramientas cambiaron y que esos números ya no describen las actuales; la lección que sigue vigente es que la sensación de productividad no es una medición. El reporte de DORA de 2025 describe la IA como un amplificador de las fortalezas y debilidades que el equipo ya tiene.
- La responsabilidad no se delega: quien hace commit responde por el código. Por eso el curso pide un registro de uso de IA y una defensa oral, y por eso las prácticas del curso (especificaciones, pruebas, revisión, CI) importan más con IA, no menos.
- La política del curso, en concreto: qué va en el registro (qué se pidió, a qué herramienta, qué se aceptó, qué se rechazó y por qué), cuándo se cita, y que los exámenes son sin IA.
- Error común: aceptar código generado porque “pasa las pruebas” cuando las pruebas también las generó la herramienta. Unas pruebas que nadie leyó no verifican nada.
Laboratorio — Entorno y Git en equipo: ramas, pull requests y conflictos sobre Kallpa
Guía: labs/README.md. Referencias: Pro Git en español, cap. 3 (ramificaciones); GitHub, About pull requests; SWEG, cap. 16, Version Control and Branch Management.
- Instalar Docker y Git, crear la cuenta de GitHub y abrir la shell del curso con
./env/run.sh. Comprobarnode --version(26),tsc --versionygit --version. Configurargit config --global user.nameyuser.emailen la terminal de su computadora, no en el contenedor (labs/README.md). - Crear un repositorio propio a partir de Kallpa (la carpeta
kallpa/), siguiendo el paso 1 de P0, y corrermake checkymake start. Abrirhttp://localhost:3000en el navegador y hacer una reserva. - En parejas, sobre el mismo repositorio: cada integrante crea una rama con un cambio pequeño en el mismo archivo (por ejemplo, el texto de un mensaje de error), hace commit, sube la rama y abre un pull request. La segunda persona en hacer merge encuentra un conflicto: resolverlo, entender las marcas
<<<<<<<,=======,>>>>>>>, y terminar el merge. - Revisar el pull request de la otra persona en GitHub: un comentario en una línea, una sugerencia de cambio y la aprobación.
- Leer
git log --oneline --graphdel repositorio y explicar qué muestra. - Leer el enunciado de P0 y elegir el issue según la regla del enunciado.
Entregas
- P0 publicado: Primer aporte: un issue de Kallpa resuelto en una rama, con pruebas y un pull request revisado (
projects/README.md). Entrega en semana 3.
Preguntas de repaso
- Den un ejemplo de código de un curso anterior que no necesitaba prácticas de ingeniería de software y otro, de su vida diaria, que sí. ¿Qué pregunta los separa?
- Expliquen la ley de Hyrum con un ejemplo de Kallpa distinto del de clase. ¿Qué podría hacer el equipo de Kallpa para que nadie dependa de ese comportamiento?
- ¿Por qué “encontrar el error antes” es más barato? Den un ejemplo de un error que se encuentre al escribir el código, otro en revisión y otro en producción.
- En el estudio de METR, ¿qué diferencia hubo entre lo que los desarrolladores creían y lo que se midió? ¿Qué conclusión se puede sacar y cuál no?
- Una herramienta les genera una función y sus pruebas, y todas pasan. ¿Qué tendrían que hacer antes de hacer commit? ¿Qué escribirían en el registro de uso de IA?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (10)Pruebas (5)Compilación (3)
Ejemplos
Los programas que usan los apuntes y la clase.
// ¿En qué orden devuelve Kallpa las canchas? El README no lo promete; el
// código las ordena por nombre (ORDER BY name en court-repository.ts).
// node courts-order.ts
import { CourtRepository } from '../../kallpa/src/store/court-repository.ts';
import { openDatabase } from '../../kallpa/src/store/database.ts';
import { seedCatalog } from '../../kallpa/src/store/seed.ts';
const db = openDatabase(':memory:');
seedCatalog(db);
const courts = new CourtRepository(db);
const show = () => courts.list().map((court) => `${court.id} ${court.name}`).join(' | ');
console.log(`Hoy: ${show()}`);
courts.insert({ name: 'Área techada', sport: 'basquet' });
console.log(`Con una cancha nueva: ${show()}`);
db.close();// Las mismas reglas que Kallpa: las recomendadas de JavaScript y de
// typescript-eslint.
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import { defineConfig } from 'eslint/config';
export default defineConfig(
{ ignores: ['build/'] },
js.configs.recommended,
tseslint.configs.recommended,
);// Dos clientes HTTP de Kallpa escritos por otras personas: el bot de WhatsApp
// de una liga de fulbito y un script de la administración. Cada uno tiene una
// versión que depende de lo que Kallpa promete y otra que depende de lo que
// Kallpa hace hoy sin haberlo prometido (ley de Hyrum).
export type CourtJson = { id: number; name: string; sport: string };
export type ErrorJson = { error: { code: string; message: string } };
// "La segunda cancha de la lista es Fulbito 1": cierto hoy, porque Kallpa
// ordena por nombre y 'Básquet' va antes que 'Fulbito 1'.
export function fulbitoOneFragile(courts: CourtJson[]): number {
return courts[1].id;
}
// Busca por lo que la API sí da: el nombre de cada cancha.
export function fulbitoOne(courts: CourtJson[]): number | undefined {
return courts.find((court) => court.name === 'Fulbito 1')?.id;
}
// Compara el texto del mensaje, que está escrito para personas.
export function isPastHourFragile(body: ErrorJson): boolean {
return body.error.message === 'Esa hora ya pasó.';
}
// Compara el código, que el README de Kallpa promete estable.
export function isPastHour(body: ErrorJson): boolean {
return body.error.code === 'IN_THE_PAST';
}#!/usr/bin/env bash
# Laboratorio de la semana 1, paso a paso y sin GitHub: dos estudiantes, Ana y
# Bruno, cambian el mismo mensaje de error de Kallpa en dos ramas. El pull
# request de Ana se fusiona primero; Bruno trae main a su rama, encuentra el
# conflicto, lo resuelve y su pull request se fusiona después.
#
# ./team-git.sh build/equipo arma todo dentro de build/equipo
#
# Un repositorio "bare" (sin carpeta de trabajo) hace de GitHub: es a donde
# los dos suben y de donde traen los cambios. Los merges que en GitHub hace el
# botón "Merge pull request" se hacen aquí con `git merge --no-ff`, que es lo
# que ese botón hace por omisión. Los nombres, correos y fechas de los commits
# son fijos, así que los hashes salen siempre iguales.
set -euo pipefail
DIR="${1:?Uso: ./team-git.sh <carpeta>}"
KALLPA="${KALLPA:-$(cd "$(dirname "$0")/../../kallpa" && pwd)}"
FILE=src/domain/booking.ts
OLD="'Esa hora ya pasó.'"
rm -rf "$DIR"
mkdir -p "$DIR"
DIR="$(cd "$DIR" && pwd)"
export GIT_CONFIG_GLOBAL="$DIR/gitconfig" # no usar el ~/.gitconfig de nadie
git config --global init.defaultBranch main
git config --global advice.detachedHead false
git config --global safe.directory '*'
# Quién hace cada paso y a qué hora (la hora de Lima del laboratorio).
as() {
local who="$1" when="$2"
export GIT_AUTHOR_NAME="$who" GIT_COMMITTER_NAME="$who"
WHO="$(echo "${who%% *}" | tr '[:upper:]' '[:lower:]')"
export GIT_AUTHOR_EMAIL="$WHO@uni.example" GIT_COMMITTER_EMAIL="$WHO@uni.example"
export GIT_AUTHOR_DATE="2026-03-25T$when:00-05:00" GIT_COMMITTER_DATE="2026-03-25T$when:00-05:00"
cd "$DIR/$WHO"
}
# Muestra el comando y su salida, como en la terminal.
run() {
local shown="" arg
for arg in "$@"; do
case "$arg" in *" "*) arg="\"$arg\"" ;; esac
shown="$shown $arg"
done
printf '%s$%s\n' "$WHO" "$shown"
"$@" 2>&1
}
step() { printf '\n## %s\n' "$*"; }
step "GitHub: un repositorio vacío"
git init --quiet --bare "$DIR/github/kallpa.git"
step "Ana sube su copia de Kallpa"
mkdir -p "$DIR/ana"
cp -r "$KALLPA" "$DIR/ana/kallpa"
rm -rf "$DIR/ana/kallpa/node_modules" "$DIR/ana/kallpa"/data/*.db*
as "Ana Quispe" 10:00
cd kallpa
run git init --quiet
run git add -A
run git commit --quiet -m "Versión inicial de Kallpa"
run git remote add origin "$DIR/github/kallpa.git"
run git push --quiet -u origin main
step "Bruno clona el repositorio"
mkdir -p "$DIR/bruno"
as "Bruno Rojas" 10:05
run git clone --quiet "$DIR/github/kallpa.git" kallpa
cd kallpa
run git log --oneline --decorate
step "Ana: una rama y un commit"
as "Ana Quispe" 10:10
cd kallpa
run git switch -c mensaje-hora-pasada
sed -i "s/$OLD/'Esa hora ya pasó: elijan una hora libre.'/" "$FILE"
run git diff
run git commit -am "Sugiere elegir otra hora cuando la hora ya pasó"
run git push -u origin mensaje-hora-pasada
step "Bruno: otra rama, la misma línea"
as "Bruno Rojas" 10:12
cd kallpa
run git switch -c mensaje-hora-iniciada
sed -i "s/$OLD/'No se puede reservar una hora que ya empezó.'/" "$FILE"
run git commit -am "Aclara que tampoco se reserva una hora ya empezada"
run git push -u origin mensaje-hora-iniciada
step "Se fusiona el pull request de Ana"
as "Ana Quispe" 10:30
cd kallpa
run git switch main
run git merge --no-ff mensaje-hora-pasada -m "Merge pull request #1 from ana/mensaje-hora-pasada"
run git push origin main
run git cat-file -p HEAD
step "Bruno trae main a su rama: conflicto"
as "Bruno Rojas" 10:40
cd kallpa
run git fetch origin
run git log --oneline --graph --decorate --all
run git merge origin/main || true
run git status
run sed -n 64,70p "$FILE"
step "Bruno resuelve el conflicto"
sed -i "/^<<<<<<< /,/^>>>>>>> /c\\ return { code: 'IN_THE_PAST', message: 'Esa hora ya empezó: elijan una hora libre.' };" "$FILE"
run sed -n 64,66p "$FILE"
run git add "$FILE"
run git commit --no-edit
run git push origin mensaje-hora-iniciada
step "Se fusiona el pull request de Bruno"
as "Ana Quispe" 10:50
cd kallpa
run git switch main
run git fetch --quiet origin
run git merge --no-ff origin/mensaje-hora-iniciada -m "Merge pull request #2 from ana/mensaje-hora-iniciada"
run git push origin main
step "La historia de main"
run git log --oneline --graph --decorate// La versión corregida: lee los soles y los céntimos como enteros, sin pasar
// por un número decimal, y rechaza lo que no tiene la forma de un monto.
const AMOUNT = /^(?:S\/ ?)?(\d{1,7})(?:\.(\d{2}))?$/;
export function parseSoles(text: string): number | null {
const match = AMOUNT.exec(text.trim());
if (match === null) return null;
const soles = Number(match[1]);
const cents = match[2] === undefined ? 0 : Number(match[2]);
return soles * 100 + cents;
}// Ilustración escrita para la clase: un cambio como los que suele proponer un
// agente cuando se le pide "una función que convierta a céntimos los montos
// que escribe la administración (por ejemplo, 'S/ 112.50')". Compila, pasa
// las pruebas que vienen con él (parse-soles.test.ts) y está mal: ver
// roundtrip.check.ts y el apunte de la semana 1.
export function parseSoles(text: string): number {
const amount = parseFloat(text.replace('S/', '').trim());
return Math.floor(amount * 100);
}// Revisa parse-soles.ts contra la regla de ida y vuelta, para todos los montos
// de S/ 0.00 a S/ 1000.00. Falla: `node --test agent-change/roundtrip.check.ts`.
// (No se llama *.test.ts para que `make test` no lo corra como prueba; lo
// corre agent-change.test.ts, que comprueba que falla.)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { formatSoles } from '../../../kallpa/src/domain/pricing.ts';
import { parseSoles } from './parse-soles.ts';
import { roundTripFailures } from './roundtrip.ts';
test('todo monto que muestra Kallpa se vuelve a leer igual', () => {
const failures = roundTripFailures(parseSoles, 100_000);
const shown = failures.slice(0, 3).map((cents) => `'${formatSoles(cents)}' -> ${parseSoles(formatSoles(cents))}`);
assert.equal(failures.length, 0, `${failures.length} montos no vuelven: ${shown.join(', ')}...`);
});import { formatSoles } from '../../../kallpa/src/domain/pricing.ts';
// Una prueba que sale de la regla, no de la implementación: todo monto que
// Kallpa muestra con formatSoles tiene que volver a leerse igual. Devuelve
// los montos (en céntimos, de 0 a maxCents) que no vuelven.
export function roundTripFailures(parse: (text: string) => number | null, maxCents: number): number[] {
const failures: number[] = [];
for (let cents = 0; cents <= maxCents; cents++) {
if (parse(formatSoles(cents)) !== cents) failures.push(cents);
}
return failures;
}// Un error de tipeo que Node no ve y tsc sí: la propiedad se llama `hours`,
// no `hour`. Node quita los tipos y ejecuta; tsc revisa los tipos antes.
// node price-typo.ts imprime NaN
// tsc -p . marca el error en la línea 7
import { quotePriceCents } from '../../../kallpa/src/domain/pricing.ts';
const cents = quotePriceCents({ sport: 'voley', date: '2026-03-10', startHour: 9, hour: 2, plan: 'regular' });
console.log(`Total: ${cents} céntimos`);{
"compilerOptions": {
"target": "es2024",
"lib": ["es2024"],
"module": "nodenext",
"moduleResolution": "nodenext",
"types": ["node"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"files": ["price-typo.ts"]
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Comprueba lo que el apunte dice del cambio ilustrativo de agent-change/:
// pasa sus propias pruebas, falla la de ida y vuelta, y la versión corregida
// pasa las dos.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { parseSoles } from './agent-change/parse-soles-fixed.ts';
import { roundTripFailures } from './agent-change/roundtrip.ts';
const here = new URL('.', import.meta.url).pathname;
// Sin NODE_TEST_CONTEXT, el node --test hijo escribe su reporte normal.
const env = { ...process.env };
delete env.NODE_TEST_CONTEXT;
const nodeTest = (file: string) => spawnSync('node', ['--test', file], { cwd: here, encoding: 'utf8', env });
test('con decimales, 0.1 + 0.2 no da 0.3 y 0.29 * 100 no da 29', () => {
assert.notEqual(0.1 + 0.2, 0.3);
assert.equal(0.29 * 100, 28.999999999999996);
});
test('el cambio pasa las pruebas que trae', () => {
const run = nodeTest('agent-change/parse-soles.test.ts');
assert.equal(run.status, 0);
assert.match(run.stdout, /ℹ pass 4\nℹ fail 0/);
});
test('el cambio falla la prueba de ida y vuelta en 4586 montos', () => {
const run = nodeTest('agent-change/roundtrip.check.ts');
assert.equal(run.status, 1);
assert.match(run.stdout, /4586 montos no vuelven: 'S\/ 0\.29' -> 28, 'S\/ 0\.57' -> 56, 'S\/ 0\.58' -> 57/);
});
test('la versión corregida pasa la ida y vuelta de S/ 0.00 a S/ 1000.00', () => {
assert.deepEqual(roundTripFailures(parseSoles, 100_000), []);
});
test('la versión corregida pasa los ejemplos del cambio y rechaza lo que no es un monto', () => {
assert.equal(parseSoles('S/ 112.50'), 11250);
assert.equal(parseSoles('S/ 90'), 9000);
assert.equal(parseSoles('S/ 62.50'), 6250);
assert.equal(parseSoles('81.00'), 8100);
assert.equal(parseSoles('S/ 0.29'), 29);
for (const text of ['', 'S/', 'noventa', '12.5', '12.345', '-5.00', 'S/ 1e3']) {
assert.equal(parseSoles(text), null, text);
}
});// Comprueba lo que el apunte dice sobre la ley de Hyrum con la Kallpa del
// curso: el orden de GET /api/courts y los errores de POST /api/bookings.
import { after, before, test } from 'node:test';
import assert from 'node:assert/strict';
import { getJson, postJson, startTestServer, type TestServer } from '../../kallpa/test/helpers.ts';
import { CourtRepository } from '../../kallpa/src/store/court-repository.ts';
import { fulbitoOne, fulbitoOneFragile, isPastHour, isPastHourFragile, type CourtJson } from './hyrum-clients.ts';
let server: TestServer;
before(async () => {
server = await startTestServer();
});
after(() => server.close());
test('Kallpa devuelve las canchas ordenadas por nombre, no por id', async () => {
const { body } = await getJson(`${server.url}/api/courts`);
assert.deepEqual(body.courts.map((court: CourtJson) => court.id), [4, 1, 2, 3]);
});
test('hoy los dos clientes encuentran Fulbito 1', async () => {
const { body } = await getJson(`${server.url}/api/courts`);
assert.equal(fulbitoOneFragile(body.courts), 1);
assert.equal(fulbitoOne(body.courts), 1);
});
test('si Kallpa ordenara por id, el cliente frágil reservaría otra cancha', async () => {
const { body } = await getJson(`${server.url}/api/courts`);
const byId = [...body.courts].sort((a: CourtJson, b: CourtJson) => a.id - b.id);
assert.equal(fulbitoOneFragile(byId), 2);
assert.equal(fulbitoOne(byId), 1);
});
test('el orden por nombre de SQLite no es el alfabético del español', () => {
new CourtRepository(server.db).insert({ name: 'Área techada', sport: 'basquet' });
const names = new CourtRepository(server.db).list().map((court) => court.name);
assert.deepEqual(names, ['Básquet', 'Fulbito 1', 'Fulbito 2', 'Vóley', 'Área techada']);
});
test('una hora que ya pasó responde 422 con el código IN_THE_PAST', async () => {
// El reloj de las pruebas marca el lunes 9 de marzo de 2026 a las 10:00.
const response = await postJson(`${server.url}/api/bookings`, {
courtId: 1, memberId: 2, date: '2026-03-09', startHour: 9, hours: 1, teamName: 'Real Barranco',
});
assert.equal(response.status, 422);
assert.deepEqual(response.body, { error: { code: 'IN_THE_PAST', message: 'Esa hora ya pasó.' } });
assert.equal(isPastHourFragile(response.body), true);
assert.equal(isPastHour(response.body), true);
});
test('con el mensaje nuevo del laboratorio, solo sigue funcionando el cliente que compara el código', () => {
const body = { error: { code: 'IN_THE_PAST', message: 'Esa hora ya empezó: elijan una hora libre.' } };
assert.equal(isPastHourFragile(body), false);
assert.equal(isPastHour(body), true);
});// El mismo error de tipeo, visto por Node (NaN, sin aviso) y por tsc (error).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
const dir = new URL('./shift-left/', import.meta.url).pathname;
test('Node ejecuta price-typo.ts sin quejarse e imprime NaN', () => {
const run = spawnSync('node', ['price-typo.ts'], { cwd: dir, encoding: 'utf8' });
assert.equal(run.status, 0);
assert.equal(run.stdout, 'Total: NaN céntimos\n');
});
test('tsc encuentra el error antes de ejecutar', () => {
const run = spawnSync('tsc', ['-p', '.'], { cwd: dir, encoding: 'utf8' });
assert.notEqual(run.status, 0);
assert.match(run.stdout, /price-typo\.ts\(7,83\): error TS2561: .*'hour' does not exist in type 'PriceQuote'\. Did you mean to write 'hours'\?/);
});// Corre team-git.sh en una carpeta temporal y comprueba lo que el apunte
// muestra de su salida: el conflicto, su resolución y la historia final.
import { after, before, test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const script = new URL('./team-git.sh', import.meta.url).pathname;
let dir: string;
let output: string;
const git = (repo: string, ...args: string[]) =>
spawnSync('git', args, { cwd: join(dir, repo), encoding: 'utf8', env: { ...process.env, GIT_CONFIG_GLOBAL: join(dir, 'gitconfig') } }).stdout.trim();
before(() => {
dir = mkdtempSync(join(tmpdir(), 'equipo-'));
const run = spawnSync('bash', [script, dir], { encoding: 'utf8' });
assert.equal(run.status, 0, run.stderr);
output = run.stdout;
});
after(() => rmSync(dir, { recursive: true, force: true }));
test('el merge de Bruno choca con el de Ana en booking.ts', () => {
assert.match(output, /CONFLICT \(content\): Merge conflict in src\/domain\/booking\.ts/);
assert.match(output, /both modified: {3}src\/domain\/booking\.ts/);
assert.match(output, /<<<<<<< HEAD\n.*'No se puede reservar una hora que ya empezó\.'.*\n=======\n.*'Esa hora ya pasó: elijan una hora libre\.'.*\n>>>>>>> origin\/main/);
});
test('main termina con el mensaje que eligió Bruno al resolver, sin marcas', () => {
const file = readFileSync(join(dir, 'ana/kallpa/src/domain/booking.ts'), 'utf8');
assert.match(file, /message: 'Esa hora ya empezó: elijan una hora libre\.'/);
assert.doesNotMatch(file, /^(<<<<<<<|=======|>>>>>>>)/m);
});
test('la historia de main tiene tres merges de dos padres y dos commits de cambio', () => {
const lines = git('ana/kallpa', 'log', '--format=%p|%s', 'main').split('\n');
assert.equal(lines.length, 6);
const merges = lines.filter((line) => line.split('|')[0].includes(' '));
assert.deepEqual(merges.map((line) => line.split('|')[1]), [
'Merge pull request #2 from ana/mensaje-hora-iniciada',
"Merge remote-tracking branch 'origin/main' into mensaje-hora-iniciada",
'Merge pull request #1 from ana/mensaje-hora-pasada',
]);
});
test('el GitHub de mentira y las dos copias quedan en el mismo commit', () => {
const head = git('ana/kallpa', 'rev-parse', 'main');
assert.equal(git('github/kallpa.git', 'rev-parse', 'main'), head);
git('bruno/kallpa', 'fetch', '--quiet', 'origin');
assert.equal(git('bruno/kallpa', 'rev-parse', 'origin/main'), head);
});
test('las pruebas de Kallpa siguen pasando: comparan códigos, no mensajes', () => {
// Sin NODE_TEST_CONTEXT, el node --test hijo escribe su reporte normal.
const env = { ...process.env };
delete env.NODE_TEST_CONTEXT;
const run = spawnSync('node', ['--test', 'test/**/*.test.ts'], { cwd: join(dir, 'ana/kallpa'), encoding: 'utf8', env });
assert.equal(run.status, 0, run.stdout);
assert.match(run.stdout, /ℹ fail 0/);
});
test('con nombres y fechas fijos, los hashes se repiten en otra corrida', () => {
const again = mkdtempSync(join(tmpdir(), 'equipo-'));
try {
const run = spawnSync('bash', [script, again], { encoding: 'utf8' });
assert.equal(run.status, 0, run.stderr);
const graph = (text: string) => text.slice(text.indexOf('## La historia de main'));
assert.equal(graph(run.stdout), graph(output));
} finally {
rmSync(again, { recursive: true, force: true });
}
});// Las pruebas que vienen con el cambio de parse-soles.ts (también escritas
// para la clase). Todas pasan.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseSoles } from './parse-soles.ts';
test('convierte una tarifa de fulbito en hora punta', () => {
assert.equal(parseSoles('S/ 112.50'), 11250);
});
test('convierte un monto sin céntimos', () => {
assert.equal(parseSoles('S/ 90'), 9000);
});
test('convierte una tarifa de vóley en hora punta', () => {
assert.equal(parseSoles('S/ 62.50'), 6250);
});
test('acepta el monto sin el símbolo de soles', () => {
assert.equal(parseSoles('81.00'), 8100);
});Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 1: ingeniería de software, la ley de Hyrum, un cambio que pasa sus
# propias pruebas y Git en equipo. Se usa dentro del contenedor del curso.
#
# make arma en build/equipo el laboratorio de Git de ejemplo (Ana y
# Bruno, dos ramas, un conflicto y dos pull requests) y guarda
# lo que muestra la terminal en build/team-git.txt
# make test revisa los tipos (tsc), corre ESLint y las pruebas
# (*.test.ts), que comprueban lo que dice el apunte
# make clean borra build/
#
# Los ejemplos usan la Kallpa del curso (../../kallpa) sin modificarla.
# shift-left/price-typo.ts tiene un error de tipos a propósito: queda fuera de
# tsconfig.json y lo revisa shift-left.test.ts.
.PHONY: all test clean
all: build/team-git.txt
build/team-git.txt: team-git.sh
mkdir -p build
./team-git.sh build/equipo > $@
@echo "Listo: la salida está en $@"
test:
tsc -p .
eslint .
node --test "*.test.ts"
clean:
rm -rf build{
"name": "week-01",
"private": true,
"type": "module"
}{
"compilerOptions": {
"target": "es2024",
"lib": ["es2024"],
"module": "nodenext",
"moduleResolution": "nodenext",
"types": ["node"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["*.ts", "agent-change/*.ts"]
}