test(api): verify regressions over HTTP and document UI readiness

This commit is contained in:
syntaxbullet
2026-09-04 09:52:44 +02:00
parent f520d73bbc
commit 1acf016169
2 changed files with 51 additions and 9 deletions

View File

@@ -99,7 +99,7 @@ Omitted habit or task schedules default to daily. Both rules must match for a ta
Weekdays use Sunday=0 through Saturday=6. Weekday lists must be nonempty and unique. Week intervals use Monday-start weeks, with no occurrence before the anchor date. Day intervals support 13,650 days; week intervals support 1520 weeks. Neither lateness nor corrections shift recurrence.
A task habit with no matching tasks is not due, including when its parent schedule and task rules do not intersect. Inspect projected future days or the calendar to see the resulting schedule.
A task habit with no matching tasks is not due, including when its parent schedule and task rules do not intersect. Habit configuration responses include `warnings`: `no_tasks` for an empty task habit, or `task_never_due` with a `taskId` when the parent and task recurrence rules can never coincide. These are informational and do not prevent saving. The check uses the exact repeating schedules, including anchored day/week intervals. Inspect projected future days or the calendar to see the resulting schedule.
## Daily reset and optional carryover
@@ -131,15 +131,15 @@ Every successful write returns the updated individual day. Important response fi
- `due`, `future`, `status`: `not_due`, `future`, `empty`, `partial`, or `complete`.
- `value`, `target`, `unit`, `ratio`, `complete`: exact progress and capped completion ratio. Not-due ratios are null.
- `loggedCount`, `carriedFrom`: explicit and inherited count information.
- `tasks`: due occurrences with `id`, `taskId`, `name`, `done`, `expiredAt` and `updatedAt`.
- `tasks`: due occurrences with `id`, `taskId`, `name`, `done`, `expiredAt`, `closedAt` and `updatedAt`. Occurrences follow the current dated task-definition order.
Backfills use that date's method, target, schedule and task list. They automatically affect every combined chart that selects the habit. Reducing a completed habit below its requirement removes its completed contribution.
## History, expiry and timezones
Revisions and prior day snapshots are retained. Changing a target, method, task list or schedule takes effect on the current tracking date. Same-method edits retain today's explicit count/manual state; surviving task IDs retain their occurrence state across task edits. Changing methods starts a fresh current-day record, retaining the superseded record for audit. Configuration responses include `effectiveDate` and `revisionId` so clients can explain this behavior.
Revisions and prior day snapshots are retained. Changing a target, method, task list or schedule takes effect on the current tracking date. Same-method edits retain today's explicit count/manual state; surviving task IDs retain their occurrence state across task edits. Temporarily archiving the habit or disabling its schedule preserves same-day task states for restoration. Changing methods starts a fresh current-day record, retaining the superseded record for audit. Configuration responses include `effectiveDate` and `revisionId` so clients can explain this behavior.
Incomplete task occurrences expire at the preserved local midnight. Expiry is reconciled lazily before authenticated tracking requests, including after downtime, and stores the original deadline rather than the time the API was accessed. No scheduler process is required. A historical correction changes `done` without clearing the original `expiredAt`. Tasks completed at their original deadline have no expiry record. Old occurrences never become overdue tasks on a later date.
Incomplete task occurrences expire at the preserved local midnight. Expiry is reconciled lazily before authenticated tracking requests, including after downtime, and stores the original deadline rather than the time the API was accessed. No scheduler process is required. The `closedAt` timestamp finalizes the original outcome at the deadline exactly once, including tasks that were completed. A historical correction changes `done` without clearing or inventing the original `expiredAt`. Tasks completed at their original deadline have no expiry record. Old occurrences never become overdue tasks on a later date.
Timezone updates first reconcile existing dates under the old timezone. Existing snapshots and deadlines remain fixed; subsequent newly opened dates use the new timezone. The tracking date never moves backward when traveling west across a date boundary. `/today.date` is authoritative during such a transition, and the new timezone catches up naturally. DST boundaries use actual local midnight, including 23-hour and 25-hour days. UTC millisecond timestamps and local date strings serve different purposes and should not be interchanged.
@@ -169,7 +169,7 @@ The API intentionally stores only product-level appearance preferences:
| `notDueColor` | `#f5f5f5` | Nothing scheduled |
| `futureColor` | `#dbeafe` | Upcoming date |
Positive colors are shades of the main color. `auto` uses the dated count target or number of due tasks for individual charts and four positive shades for combined charts. Thus an eight-count habit has eight positive levels plus empty. Explicit shade counts offer a simpler display without changing underlying progress. Manual habits always remain binary. Layout, borders, spacing, typography, callbacks and animation are not persisted settings. PUT replaces individual settings; the `settings` object in a chart PATCH is also a replacement, with defaults for omitted settings fields.
Positive colors are shades of the main color; very light main colors use darker intermediate shades so white and near-white choices remain distinct. `auto` uses the dated count target or number of due tasks for individual charts and four positive shades for combined charts. Thus an eight-count habit has eight positive levels plus empty. Explicit shade counts offer a simpler display without changing underlying progress. Manual habits always remain binary. Layout, borders, spacing, typography, callbacks and animation are not persisted settings. PUT replaces individual settings; the `settings` object in a chart PATCH is also a replacement, with defaults for omitted settings fields.
Calendar responses include `kind`, `from`, `to`, `today`, `timezone`, `settings`, `days`, and a numeric `data` summary. Summary values are positive shade levels, 0 for due/empty, -1 for not due, and -2 for future. Exact counts always live in `days`.
@@ -196,4 +196,35 @@ Errors are JSON objects with an `error` string:
| 422 | Invalid fields, unsupported configuration, invalid dates/range, or a progress body inconsistent with the dated method |
| 500 | Unexpected failure; internal details are not exposed |
Schemas reject unknown body properties. Failed domain transactions roll back their partial writes. Progress audit events record before/after values; they are not overwritten by corrections.
Schemas reject unknown body properties. PATCH preserves all omitted top-level fields and rejects an empty patch. Request bodies are fully read before taking the dated configuration snapshot, so overlapping mutations in the single Bun server do not reuse stale configuration. A request streaming across midnight uses the date at which its write is processed. Explicit progress writes remain absolute totals; simultaneous edits of the same field use the last processed value. Failed domain transactions roll back their partial writes. Progress audit events record before/after values; they are not overwritten by corrections.
## API readiness review
The API regression suite and production smoke checks cover the known defects found
before UI work: omitted PATCH fields, same-day task restoration, immutable expiry,
correction audit values, overlapping configuration writes, streaming requests across
midnight/timezone changes, stable task ordering, and pale calendar colors. Migration
0003 reconstructs deadline outcomes for existing active occurrences using the first
post-deadline correction's before-state when present.
| PRD acceptance criteria | Evidence |
| --- | --- |
| 1: exclusive completion method | `security.test.ts`, `calendar.test.ts` |
| 23: count target and binary manual progress | `api.test.ts`, `calendar.test.ts` |
| 45: derived task progress and no-due exclusion | `api.test.ts` |
| 69: equal-weight combined ratios, neutral and full states | `api.test.ts`, `shared/calendar.test.ts` |
| 10: fixed daily, day-interval, weekday and week-interval recurrence | `calendar.test.ts`, `api.test.ts` |
| 11: occurrence expiry and fresh next occurrence | `api.test.ts`, `regressions.test.ts`, `migrations.test.ts` |
| 12: historical corrections without shifting recurrence | `api.test.ts`, `carryover.test.ts`, `regressions.test.ts` |
| 13: preserved historical requirements | `api.test.ts`, `resilience.test.ts`, `regressions.test.ts` |
| 14: exact values and status for inspection | `api.test.ts`, `shared/calendar.test.ts` |
Habit test filenames above live in `src/habits`, migration tests in `src/db`.
The `test:smoke` command independently checks built production HTTP behavior and
persistence after restart. Line coverage is a supplementary metric; these behavior
checks are the acceptance evidence. Actual calendar rendering, interaction,
responsive layout, and accessibility remain UI-layer work. Live Discord consent
continues to require the configured provider and a real user login; tests stub its
network calls. Optional account export, reminders, streaks and other features
outside the PRD are not prerequisites for the UI layer.

View File

@@ -40,11 +40,22 @@ try {
await boot();
const date = (await request('/today')).date;
const h = await request('/habits', 'POST', { name: 'HTTP hydration', method: 'count', target: 8 }, 201);
const t = await request('/habits', 'POST', { name: 'HTTP tasks', method: 'tasks', tasks: [{ name: 'Check' }] }, 201);
const schedule = { type: 'weekdays', days: [new Date(`${date}T00:00:00Z`).getUTCDay()] };
const t = await request('/habits', 'POST', { name: 'HTTP tasks', method: 'tasks', tasks: [{ name: 'Check', schedule }] }, 201);
const renamedTask = await request(`/habits/${t.id}/tasks/${t.tasks[0].id}`, 'PATCH', { name: 'Renamed task' });
check(JSON.stringify(renamedTask.schedule) === JSON.stringify(schedule), 'Task rename reset recurrence');
await Promise.all([request(`/habits/${h.id}`, 'PATCH', { name: 'Concurrent hydration' }), request(`/habits/${h.id}`, 'PATCH', { unit: 'glasses' })]);
const updated = await request(`/habits/${h.id}`);
check(updated.name === 'Concurrent hydration' && updated.unit === 'glasses', 'Overlapping patches lost unrelated settings');
await request(`/habits/${h.id}/days/${date}/progress`, 'PUT', { count: 7 });
await request(`/habits/${t.id}/days/${date}/tasks/${t.tasks[0].id}`, 'PUT', { done: true });
const c = await request('/charts', 'POST', { name: 'HTTP combined', habitIds: [h.id, t.id] }, 201);
const c = await request('/charts', 'POST', { name: 'HTTP combined', habitIds: [h.id, t.id], settings: { mainColor: '#ff0000', shadeCount: 8 } }, 201);
check((await request(`/charts/${c.id}/days/${date}`)).ratio === 0.5, 'Partial habit incorrectly contributed to combined score');
const renamedChart = await request(`/charts/${c.id}`, 'PATCH', { name: 'Renamed chart' });
check(renamedChart.settings.mainColor === '#ff0000' && renamedChart.settings.shadeCount === 8, 'Chart rename reset settings');
await request(`/habits/${t.id}`, 'PATCH', { archived: true });
await request(`/habits/${t.id}`, 'PATCH', { archived: false });
check((await request(`/habits/${t.id}/days/${date}`)).complete, 'Archive and restore lost task completion');
await stop(); await boot();
check((await request(`/habits/${h.id}/days/${date}`)).value === 7, 'Count did not survive restart');
check((await request(`/charts/${c.id}/days/${date}`)).ratio === 0.5, 'Tasks or membership did not survive restart');
@@ -53,5 +64,5 @@ try {
check(result.days[0].ratio === 1, 'Correction did not update combined calendar');
check((await fetch(`${origin}/api/habits`)).status === 401, 'HTTP authentication was bypassed');
check((await fetch(`${origin}/api/habits`, { method: 'POST', headers: { Cookie: `minabot_session=${token}`, Origin: 'https://evil.example', 'Content-Type': 'application/json' }, body: '{}' })).status === 403, 'HTTP Origin enforcement was bypassed');
console.log('Production HTTP smoke passed: migrations, count/task logs, combined scores, correction, authentication, CSRF, and persistence across restart.');
console.log('Production HTTP smoke passed: migrations, count/task logs, combined scores, correction, PATCH preservation, overlapping writes, archive/restore, authentication, CSRF, and persistence across restart.');
} finally { await stop(); rmSync(directory, { recursive: true, force: true }); }