# Disposable runtime evidence

Captured: 2026-08-02

Environment: local Development configuration, compiled Release API, Next development server, disposable SQLite data root under the current user's temporary directory. No production service, real mailbox, paid AI provider, or personal data was used.

## Default-provider endpoint results

Two synthetic local users were registered. User A owned one company, one job (`Syntetisk utvikler æøå`), one correspondence record, two CV variants, one career profile, and one text attachment. User B had no data.

| Request | User | Result | Evidence |
|---|---|---:|---|
| `GET /api/jobapplications?page=1&pageSize=20` | B | 200, 0 items | Empty-account isolation works. |
| `GET /api/companies` | B | 200, 0 items | Empty-account isolation works. |
| `GET /api/correspondence?jobApplicationId=1` | B | 200, 0 items | User A's message was not disclosed. |
| `GET /api/careerprofile` | B | 404 | User A's profile was not disclosed. |
| `GET /api/jobapplications/1` | A / B | 200 / 404 | Job ownership enforced. |
| `GET /api/attachments/1` | A / B | 200 / 404 | Attachment row and file ownership enforced. |
| `GET /api/cv/variants` | A and empty B | 500 | EF SQLite cannot translate the `DateTimeOffset` ordering. |
| `GET /api/jobapplications/1/workspace` | A | 500 | Same provider-translation family, reached on the core workspace. |
| `GET /api/jobapplications/1/ai/history` | A | 500 | Same provider-translation family. |
| `GET /api/ai/usage` | A | 500 | Same provider-translation family. |
| `GET /api/jobapplications/1/timeline` | A and B | 500 | Ambiguous endpoint selection occurs before ownership evaluation. |
| `GET /api/jobapplications/1/interview-prep` | A and B | 500 | Ambiguous endpoint selection occurs before ownership evaluation. |
| `GET /api/jobapplications/1/checklist` | A | 200 | Sibling workspace endpoint works. |
| `GET /api/jobapplications/1/analysis` | A | 200 | Sibling intelligence endpoint works. |
| `GET /api/jobapplications/1/match` | A | 200 | Sibling intelligence endpoint works. |

The application returned generic 500 problem responses; no secret-bearing exception details were exposed to the client.

## Background-service reproduction

The synthetic job was set to `Applied`, dated 40 days earlier than the audit, and had `Tags` and `ShortSummary` set to null. After a controlled API restart and more than the documented initial delays:

- status remained `Applied`, although the default rules threshold should have marked it ghosted;
- `Tags` remained null despite deterministic `SkillTagger` input containing `C#`;
- `ShortSummary` remained null.

This reproduces the deny-on-null tenant filter in a background scope with no HTTP user. The same query structure exists in rules, follow-up reminders, enrichment, and daily export. The queued CV processor is the counterexample: it deliberately uses `IgnoreQueryFilters()` for cross-tenant worker reads.

## Authentication lifecycle reproductions

### Logout

1. Logged in as User A and copied the issued synthetic cookie jar.
2. Posted logout with the CSRF cookie/header pair.
3. The browser jar received cookie deletion and subsequently got 401.
4. The copied pre-logout cookie still received 200 from `/api/auth/me`.

Result: logout clears client cookies but does not revoke the server-side `UserSession`.

### Email verification

A separate disposable API started with `Auth:RequireEmailVerification=true`, registration enabled, and email delivery disabled.

| Check | Result |
|---|---|
| Register synthetic account | 200 |
| Stored `EmailConfirmed` | `0` |
| Immediate `/api/auth/me` with registration-issued session | 200 |

The same synthetic row was then marked confirmed solely to isolate the email-change path. A normal login followed by `PUT /api/auth/profile` changed the email, returned 204, and left `EmailConfirmed=1`. No verification challenge was required for the new address.

## Backup and restoration rehearsal

The built-in SQLite backup runner created `jobtracker_backup_20260802_184234.db` from the disposable database. A read-only copied snapshot passed `PRAGMA integrity_check` and matched the current database:

| Dataset | Current | Restored copy |
|---|---:|---:|
| Users | 2 | 2 |
| Jobs | 1 | 1 |
| Companies | 1 | 1 |
| Attachment rows | 1 | 1 |
| CV variants | 2 | 2 |

A second isolated API was started from the restored database plus separately copied `Attachments/` and `keys/`. `/health` returned 200; synthetic login, job retrieval, and attachment download all returned 200.

Conclusion: SQLite database snapshotting works. Complete recovery additionally requires file storage, the Data Protection key ring, deployment secrets/configuration, and a documented restore procedure. MariaDB/MySQL backup is intentionally unsupported by the in-process runner and was not available for rehearsal.

## Safe local performance samples

Release API, disposable SQLite database with one job, ten requests after warm-up:

| Endpoint | Mean | p95 | Response bytes |
|---|---:|---:|---:|
| `/health` | 11.3 ms | 43.9 ms | 35 |
| `/api/jobapplications?page=1&pageSize=20` | 17.8 ms | 111.3 ms | 1,547 |
| `/api/companies` | 6.5 ms | 33.3 ms | 255 |

The static export contained 49 JavaScript chunks totalling 2,762,618 uncompressed bytes and one 293-byte CSS file. This aggregate is not an initial-page transfer measurement because Next.js loads route chunks selectively.

No load test was performed. Results do not establish production capacity.

## Cleanup

Listeners on audit ports 3001, 5402, 5403, and 5404 were resolved to their exact audit-owned command lines and stopped. Temporary evidence remains under the disposable local data root; no repository source/configuration was changed.
