Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions csv-table-hub-main/src/__tests__/log-step-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// The upload response is validated against a schema. A log step the API emits but the schema does not
// list makes zod reject the WHOLE response, so an import that succeeded on the server is reported as
// "Failed: Unexpected response shape" (what #186 did when it added the header_mapping step).

import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { processingLogSchema } from "@/lib/csv.functions";

const API = join(__dirname, "../../../api");

/** Every step name the loaders pass to _log(logs, "<step>", ...). */
function stepsTheApiEmits(): Set<string> {
const steps = new Set<string>();
for (const dir of ["services", "routers"]) {
for (const file of readdirSync(join(API, dir)).filter((f) => f.endsWith(".py"))) {
const source = readFileSync(join(API, dir, file), "utf8");
for (const m of source.matchAll(/_log\(\s*logs,\s*"([a-z_]+)"/g)) steps.add(m[1]);
}
}
return steps;
}

describe("processing log steps", () => {
it("finds the steps the API emits (guards the scan itself)", () => {
const steps = stepsTheApiEmits();
expect(steps.size).toBeGreaterThan(8);
expect(steps.has("receive")).toBe(true);
});

it("the schema accepts every step the API can emit", () => {
const known = new Set<string>(processingLogSchema.shape.step.options);
const unknown = [...stepsTheApiEmits()].filter((s) => !known.has(s));
expect(unknown).toEqual([]);
});

it("accepts the header_mapping entry a dynamic upload with a header mapping returns", () => {
const entry = { ts: 1, step: "header_mapping", level: "info", message: "Applied 2 header renames" };
expect(processingLogSchema.safeParse(entry).success).toBe(true);
});
});
54 changes: 54 additions & 0 deletions csv-table-hub-main/src/__tests__/tenant-isolation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// #176 (BR-36): upload history is kept in localStorage under one global key, so the next person
// to sign in on the same browser sees the previous tenant's file names and rejected values.
//
// #176 was closed as moot because the Streamlit migration (#212) replaces this screen, but the
// React app still ships until then. `it.fails` keeps this suite green while the leak exists and
// turns it red the moment it is fixed, so the test is then switched to a plain `it`.

import { act, renderHook } from "@testing-library/react";
import { beforeEach, expect, it } from "vitest";
import { useLocalStorageState } from "@/hooks/use-local-storage";
import { clearSession } from "@/lib/session";

type Job = { name: string };

function signInAs(tenant: string) {
sessionStorage.setItem(
"te.session",
JSON.stringify({
token: `t-${tenant}`,
expiresAt: "2099-01-01T00:00:00Z",
username: `user-${tenant}`,
tenant,
role: "uploader",
}),
);
}

beforeEach(() => {
window.localStorage.clear();
clearSession();
});

it("keeps a tenant's own history across a reload (the feature)", () => {
signInAs("acme");
const first = renderHook(() => useLocalStorageState<Job[]>([]));
act(() => first.result.current[1]([{ name: "acme-payroll.csv" }]));
first.unmount();

const reloaded = renderHook(() => useLocalStorageState<Job[]>([]));
expect(reloaded.result.current[0]).toEqual([{ name: "acme-payroll.csv" }]);
});

it.fails("#176: another tenant signing in on the same browser does not see the previous tenant's history", () => {
signInAs("acme");
const acme = renderHook(() => useLocalStorageState<Job[]>([]));
act(() => acme.result.current[1]([{ name: "acme-payroll.csv" }]));
acme.unmount();

clearSession(); // sign out: only the session token is removed
signInAs("globex");
const globex = renderHook(() => useLocalStorageState<Job[]>([]));

expect(globex.result.current[0]).toEqual([]);
});
3 changes: 2 additions & 1 deletion csv-table-hub-main/src/lib/csv.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ const rowErrorSchema = z.object({
});
export type RowError = z.infer<typeof rowErrorSchema>;

const processingLogSchema = z.object({
export const processingLogSchema = z.object({
ts: z.number(),
step: z.enum([
"receive",
"hash",
"duplicate_check",
"parse",
"header_mapping",
"validate_columns",
"create_table",
"cast_rows",
Expand Down
97 changes: 97 additions & 0 deletions docs/ISSUE_TESTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Which test verifies which issue

Every issue in the tracker at 9 Nov 2026 (46 closed, 23 open), with the test that fails if it comes back, or the reason there is none.
Hand-kept: when you close an issue, add its row. `docs/TEST_CASES.md` (generated) lists every pytest test; this file answers "is issue N guarded?".

Legend: **✅** guarded by a test · **⚠️** guarded, but the test is a known failure until the defect is fixed (strict, so it turns red when it is) · **📎** fix and tests are in an open PR, not on `main` yet · **👤** needs a person or another system · **—** nothing to test.

Frontend tests are vitest in `csv-table-hub-main/src/__tests__/` (`fe:` below). Browser tests are in `e2e/` and are not part of the default run (see `e2e/README.md`).

## Closed issues

| # | What | Status | Verified by |
|---|---|---|---|
| 99 | Per-tenant T&E tables | ✅ | `tests/system/test_tenant_isolation.py` (own tables, per-tenant counts, keys and triggers kept) |
| 102 | Lock accounts after failed sign-ins | ✅ | `tests/system/test_accounts.py` (`Lockout`) |
| 107 | Immutable source, append-only audit | ✅ | `tests/system/test_preserved_uploads.py`, `test_audit_append_only.py`, `test_original_bytes.py` |
| 108 | Review: paging, include/exclude, versioned edits | ✅ | `tests/system/test_review.py`, `test_review_cases.py`, `fe: review-table.test.tsx`, `e2e/test_streamlit_spike.py` |
| 114 | Delete merged branches | — | housekeeping |
| 116 | Local PostgreSQL not reachable on 5433 | 👤 | one dev machine's setup; closed on evidence attached to the issue |
| 117 | Password change and admin reset | ✅ | `tests/system/test_password_reset.py`, `fe: change-password.test.tsx` |
| 118 | "Failed to fetch" when the API is down | ✅ | `fe: session.test.ts` ("API unreachable"). The PowerShell start-up guard in `scripts/start-api.ps1` has no test |
| 119 | Upload times shown in UTC | ✅ | `fe: local-time.test.ts` |
| 120 | House writing style doc | — | docs |
| 123 | Every-row-rejected upload shown as migrated | ✅ | `tests/system/test_failed_uploads.py`, `fe: job-outcome.test.ts` |
| 130 | Frontend lock file out of sync | ✅ | `tests/unit/test_tooling_issues.py` (lock file agrees with `package.json`) |
| 132 | `npm audit` critical and high | ✅ / 👤 | `fe: table-rows-contract.test.ts` guards the zod 4 change. `npm audit` itself needs the registry and was not run |
| 133 | React hydration errors on `/auth` | ✅ | `fe: session.test.ts` |
| 151 | Terraform storage accounts audit | — | investigation |
| 152 | Frontend architecture decision | — | decision, see #154 and #212 |
| 154 | Streamlit spike | ✅ | `e2e/test_streamlit_spike.py` |
| 156 | Tracker home decision | — | decision |
| 162 | Cosmetic tracker doc edits | — | docs |
| 164 | Live pointers to superseded API doc | — | docs, closed not planned |
| 165 | STARTER_GUIDE dedup claim | — | docs, closed not planned |
| 166 | STARTER_GUIDE skips sign-in | — | docs |
| 169 | Test catalogue generator | ✅ | `tests/unit/test_tooling_issues.py` and the CI freshness step |
| 170 | All-failed T&E import removed the prior dataset | ✅ | `tests/system/test_te_reupload_registry.py` (`…keeps_a_prior_dynamic_table…`), `tests/system/test_failed_uploads.py` |
| 171 | `setup.sh` truncated the config on failure | ✅ | `tests/unit/test_tooling_issues.py` (runs the real config-write block) |
| 175 | Overwrite and mode switch bypassed the delete gate | ✅ | `tests/system/test_issue_regressions.py` (`UploadCannotBypassTheDeleteGate`) |
| 176 | Browser history leaks across tenants | ⚠️ | `fe: tenant-isolation.test.tsx`, `e2e/test_react_ui.py`. Closed as moot (the React screen is being replaced, #212), but the leak is live in the shipped app. Both tests turn red when it is fixed |
| 177 | Composite FKs for program consistency | ✅ | `tests/suites/test_06_program_consistency.sql` (Tier S) |
| 178 | Distinct rows dropped as duplicates | ✅ | `tests/system/test_issue_regressions.py` (`RowDedupKeepsDistinctRows`) |
| 179 | Frontend lint baseline | — | not a test: `npm run lint` is the check, and is not in CI. Closed as moot |
| 180 | Tier I eval ignored SQL errors | ✅ | `tests/unit/test_tooling_issues.py` |
| 181 | SQL test wrapper exited 0 on failures | ✅ / 👤 | `tests/unit/test_tooling_issues.py` covers the wrapper. The second half (`ON_ERROR_STOP` on the Tier S psql calls in `evals/runner.py`, fix on the unmerged `fix/181-sql-suite-exit-code` branch) has no test: CI fails any skipped or expected-fail pytest test, so it can't be recorded as one |
| 182 | Offline bundle swallowed download failures | ✅ | `tests/unit/test_tooling_issues.py` (runs the real download block with a failing `apt-get`) |
| 183 | Rate limiter used the API key prefix | ✅ | `tests/system/test_issue_regressions.py` (`RateLimiterIdentity`) |
| 185 | Preserved original differed from uploaded bytes | ✅ | `tests/system/test_original_bytes.py`, `fe: file-bytes.test.ts` |
| 186 | Header mappings ignored | ✅ | `tests/system/test_header_mapping.py`, `fe: log-step-contract.test.ts` (the schema accepts the step it added) |
| 187 | Failed uploads unreachable from the browser | ✅ | `fe: upload-attempts.test.tsx`, `fe: files-refresh.test.ts`, `e2e/test_react_ui.py` |
| 188 | Cancel did not interrupt an import | ✅ | `fe: uploader-state-machine.test.ts`, `fe: job-outcome.test.ts`, `e2e/test_react_ui.py`. Server-side cancel is not built (#110) |
| 189 | T&E errors named the wrong source row | ✅ | `tests/system/test_issue_regressions.py` (`TeDatabaseErrorsNameTheSourceRow`) |
| 190 | T&E review showed no validation errors | ✅ | `tests/system/test_review.py`, `tests/system/test_api_coverage.py`, `fe: review-table.test.tsx` |
| 191 | Registry race | ✅ | `tests/system/test_registry_race.py` |
| 192 | Ragged rows accounting | ✅ | `tests/system/test_ragged_rows.py` |
| 193 | Formula injection in exports | ✅ | `fe: export.test.ts` |
| 194 | Coverage floors for the loaders | ✅ | `tests/unit/test_tooling_issues.py` (the gate fails below a floor) |
| 207 | API doc covered 8 of 22 routes | ✅ | `tests/unit/test_tooling_issues.py` (the doc defers to the generated schema, which is served) |
| 215 | All-failed T&E re-upload overwrote the registry record | ✅ | `tests/system/test_te_reupload_registry.py`, `tests/system/test_te_loader_integration.py` |

## Open issues

| # | What | Status | Verified by |
|---|---|---|---|
| 31 | One-time cloud setup for terraform-test/staging | 👤 | cloud account work |
| 32 | `az` CLI SSL error on the dev machine | 👤 | one machine's setup |
| 34 | `terraform plan` provider handshake fails locally | 👤 | one machine's setup |
| 57 | Add SECURITY.md | ✅ | `tests/unit/test_open_issues.py`. The issue's done-when ("on `main`") is met, so it could be closed. The file still carries a placeholder address, noted in a comment inside it |
| 59 | Sample Migration Verification Report | 📎 | PR #216 (docs) |
| 60 | Air-gapped install procedure | ✅ / 👤 | `tests/unit/test_open_issues.py` (scripts parse, stop on error, guide names them). The offline dry run, due 15 Nov, is manual |
| 109 | ART M5: dedup and naming | — | milestone, not built |
| 110 | ART M6: approval and staged import | — | milestone, not built. Server-side cancel (#188 option b) belongs here |
| 111 | ART M7: sovereignty and reports | — | milestone, not built |
| 112 | Security review before the first external tenant | 👤 | review by a person. The #176 tests are evidence for it |
| 113 | Check OIDC sign-in against the real IdP | ✅ / 👤 | `tests/unit/test_oidc_https_jwks.py` covers the https key fetch, rotation and certificate checks. A sign-in against Keycloak or Entra ID is manual |
| 128 | Offline bundle for Windows, macOS and Linux | — | only the Ubuntu bundle exists |
| 159 | Verify `docs/VCRM.md` against the DID | 👤 | compare with an external document |
| 167 | Reorganise `tests/` into category folders | ✅ | Done on `main` (#221); `tests/README.md` says where a new test goes. The issue is still open, and could be closed |
| 168 | Accessibility CI job | ✅ | Done on `main` (#222): `tests/accessibility/` and its CI job. The issue is still open, and could be closed |
| 172 | Production database name disagreement | 📎 | PR #213 (`tests/test_prod_db_naming.py`) |
| 173 | Cluster-wide `te_app` role | 📎 | PR #208 (`tests/test_runtime_role_per_env.py`) |
| 174 | Docker image can bake local credentials | 📎 | PR #204 (`tests/test_docker_image_secrets.py`) |
| 184 | Tenant T&E schemas have no upgrade path | 📎 | PR #213 (`tests/test_tenant_te_migrations.py`) |
| 195 | Confirm inferred types, provenance flag | 📎 | PR #213 (`tests/test_type_provenance.py`) |
| 198 | Track checks that completed after merging #197 | — | process |
| 210 | Background job queue for large imports | — | feature, not built |
| 212 | Plan the Streamlit migration | — | a plan. `e2e/test_streamlit_spike.py` is the baseline it must keep passing |

## Known failure on purpose

A strict expected failure: the suite is green now and turns red when the defect is fixed, so the marker gets removed then.

- **#176** (React localStorage history leak): `fe: tenant-isolation.test.tsx` (`it.fails`) and `e2e/test_react_ui.py`. Neither runs in the pytest CI step, which fails any skipped or expected-fail test (`scripts/test_report.py --strict`).

## Found while writing these tests

- **Every dynamic import in the React app showed as Failed** since #202: the app's response schema did not list the new `header_mapping` log step. Fixed in PR #223, with a test that checks the schema against every step the API can emit.
Loading
Loading