Pular para o conteúdo

LabReserve

Sistema web para consultar disponibilidade e reservar laboratórios e equipamentos, com testes automatizados.

Problema

Reservar laboratórios e equipamentos sem um sistema central leva a conflitos de horário e controle manual. O projeto também testou como construir uma aplicação funcional com agentes de IA seguindo boas práticas de engenharia.

Decisões

  • TypeScript de ponta a ponta, com React na interface e SQLite como banco simples, sem servidor à parte.
  • Plano antes do código: escopo quebrado em histórias pequenas com critérios de aceitação.
  • Test-first: cada mudança de comportamento começa com um teste falhando.
  • Gate de validação com typecheck, lint, build, testes e E2E antes de declarar algo pronto.

Stack

  • TypeScript
  • React
  • SQLite

Resultado

Sistema local com consulta de disponibilidade, reservas com detecção de conflito de horário, filtros, cancelamento e histórico de eventos.

Aprendizado

Usar testes, plano e revisão para manter o controle sobre código produzido com agentes de IA.

Como usei IA

Construído com agentes de IA. O CLAUDE.md fixa a arquitetura, as regras de negócio que precisam continuar valendo, o trabalho test-first e o gate completo (typecheck, lint, build, testes e E2E). O AGENT_ISSUES.md é o quadro persistente de handoff entre sessões. A entrega da interface passou por uma autoauditoria e por duas revisões independentes e cegas, em paralelo, registradas em docs/verification/; foi aprovada na sexta rodada.

Evidências

  • ILUSTRAÇÃO · ILLUSTRATION docs/ARCHITECTURE.md web/ React + Vite, HTTP client server/ node:http, routes, validation application/ LabReserveService, contracts domain/ TimeInterval, Reservation no HTTP, React or SQLite persistence/ SQLite (node:sqlite) implements contracts ──▶ depende de · depends on - -▶ implementa · implements
    IlustraçãoArquitetura: monólito modular com dependências apontando para dentro, conforme docs/ARCHITECTURE.md do repositório.
  • ILUSTRAÇÃO · ILLUSTRATION src/application/lab-reserve-service.ts POST /api/reservations TimeInterval.create(startAt, endAt) ISO 8601 + timezone, end > start 400 INVALID_TIME_RANGE BEGIN … COMMIT (ROLLBACK on any error) findResource(resourceId) 404 RESOURCE_NOT_FOUND findConflict(resourceId, start, end) status = 'CONFIRMED' (cancelled don't block) newStart < existingEnd && newEnd > existingStart 409 RESERVATION_CONFLICT 10:00–11:00 vs 11:00–12:00 equal limits: allowed insertReservation(reservation) status: 'CONFIRMED' insertEvent(event) type: 'RESERVATION_CREATED' 201 { "data": reservation } ──▶ segue para · proceeds to borda rosada = erro · rose border = error
    IlustraçãoFluxo de criação de reserva: validação do intervalo, checagem de conflito e gravação atômica da reserva e do evento.
  •   overlaps(other: TimeInterval): boolean {
        return this.startAt < other.endAt && this.endAt > other.startAt;
      }

    Regra de sobreposição no domínio: limites iguais não conflitam. Ver no repositório

  •   createReservation(input: CreateReservationInput): Reservation {
        const interval = TimeInterval.create(input.startAt, input.endAt);
        return this.repository.transaction(() => {
          if (!this.repository.findResource(input.resourceId)) {
            throw new ApplicationError('RESOURCE_NOT_FOUND', 'Recurso não encontrado.');
          }
          const conflict = this.repository.findConflict(input.resourceId, interval.startAt, interval.endAt);
          if (conflict) {
            throw new ApplicationError(
              'RESERVATION_CONFLICT',
              'O recurso já está reservado nesse período.',
              { conflictingReservationId: conflict.id },
            );
          }
          const createdAt = this.clock.now().toISOString();
          const reservation = createConfirmedReservation({
            id: this.ids.generate(), resourceId: input.resourceId,
            startAt: interval.startAt, endAt: interval.endAt, createdAt,
          });
          const event: ReservationEvent = {
            id: this.ids.generate(), reservationId: reservation.id,
            type: 'RESERVATION_CREATED', occurredAt: createdAt,
          };
          this.repository.insertReservation(reservation);
          this.repository.insertEvent(event);
          return reservation;
        });
      }

    Caso de uso de criação: conflito e gravação da reserva e do evento na mesma transação. Ver no repositório

  • test('detecta sobreposição estrita e permite intervalos consecutivos', () => {
      const existing = TimeInterval.create('2026-09-16T10:00:00Z', '2026-09-16T11:00:00Z');
    
      assert.equal(
        existing.overlaps(TimeInterval.create('2026-09-16T10:30:00Z', '2026-09-16T11:30:00Z')),
        true,
      );
      assert.equal(
        existing.overlaps(TimeInterval.create('2026-09-16T11:00:00Z', '2026-09-16T12:00:00Z')),
        false,
      );
    });

    Teste de domínio que fixa a regra: sobreposição parcial conflita, horário consecutivo não. Ver no repositório

  • $ npm test
    ✔ responde 413 legível para corpos realmente grandes sem resetar a conexão (386.123ms)
    ✔ lista recursos e cria reserva pelo contrato HTTP (55.2194ms)
    ✔ lista, cancela e mostra histórico com erros estruturados (77.333ms)
    ✔ responde 404 para o histórico de uma reserva inexistente (10.1546ms)
    ✔ protege respostas e rejeita corpos que não sejam JSON (89.2057ms)
    ✔ registra falhas inesperadas sem expor seus detalhes (8.7134ms)
    ✔ mantém a resposta interna segura quando o logger falha (12.5998ms)
    ✔ informa disponibilidade, histórico global e erros de domínio pelo contrato (42.4548ms)
    ✔ cancela uma reserva confirmada uma única vez (4.9019ms)
    ✔ normaliza um intervalo ISO 8601 com offset para UTC (3.9276ms)
    ✔ rejeita um intervalo cujo término seja igual ao início (1.2695ms)
    ✔ rejeita uma data que não esteja em ISO 8601 com fuso explícito (0.5249ms)
    ✔ rejeita datas ISO 8601 sintaticamente válidas, mas inexistentes (0.415ms)
    ✔ detecta sobreposição estrita e permite intervalos consecutivos (0.5614ms)
    ✔ o projeto declara scripts reproduzíveis sem versões flutuantes (3.5348ms)
    ✔ cancelamento gera evento, libera intervalo e suporta filtros (9.6265ms)
    ✔ cria reserva e evento atomicamente, recusa conflito e aceita horário consecutivo (6.282ms)
    ✔ reverte a reserva quando a gravação do evento falha (1.5119ms)
    ✔ um banco novo recebe laboratório e equipamento determinísticos (5.8135ms)
    ℹ tests 19
    ℹ suites 0
    ℹ pass 19
    ℹ fail 0
    ℹ cancelled 0
    ℹ skipped 0
    ℹ todo 0
    ℹ duration_ms 1194.8207
    Saída real dos testes de domínio, integração e API (node:test), executados em 30/09/2026 no commit 204de08, Node 24.20.0.
  • $ npm run test:e2e
    Running 3 tests using 2 workers
    ok 1 tests\e2e\reservations.spec.ts:73:1 › consulta período com data vazia sem deixar carregamento pendente (3.0s)
    ok 2 tests\e2e\reservations.spec.ts:31:1 › cria, rejeita conflito, permite período consecutivo, cancela e mostra histórico (4.1s)
    ok 3 tests\e2e\reservations.spec.ts:85:1 › aplica filtros de reserva (3.1s)
    3 passed (12.5s)
    Saída real dos cenários E2E com Playwright (Chromium), na mesma execução.
  • Código-fonte, plano de implementação e registros de verificação no GitHub