← BBS SG Bank submission · All documents

03 — Requirements & Acceptance Tests

Derived from the observed behaviour of legacy/bank.cob. The modern implementation must match legacy semantics exactly, plus satisfy the modernization non-functionals. Every test below is re-runnable.

Result date: 2026-09-23 · Environment: python3 3.14, isolated GnuCOBOL 3.2.0 (/tmp/gcb-build/install/bin/cobc). Runners: scripts/test_bank.py (33 tests), tests/test_modern.py (5 tests), the site/tests/ suite (50 tests) and legacy-ui/tests/test_terminal.py (20 tests), plus the manual fixture below. These executable local suites total 108 passing tests; the Forge Testing v1 artifact (docs/forge/testing.md, 132 proposed cases) is a separate planning document, not an execution record.


1. Functional requirements

IDRequirementSource of truth
FR-1Process every record in operations.dat in file order, applying kinds D (deposit), W (withdraw), T (transfer).bank.cob L65-171
FR-2Reject an operation whose kind ∉ {D, W, T}.bank.cob L99-103
FR-3Reject an operation whose amount is non-positive or not a 12-digit numeric.bank.cob L94-97
FR-4Reject an operation whose source account is not present in accounts.dat.bank.cob L128-132
FR-5Reject a transfer whose destination is missing or equals the source.bank.cob L105-108, L128-132
FR-6Reject any non-deposit (W, T) whose amount exceeds the source balance.bank.cob L116-118
FR-7Persist resulting balances back to accounts.dat.bank.cob L133-171
FR-8Emit PROCESSED, REJECTED, and TOTAL_CENTS on stdout, zero-padded 8/8/16.bank.cob L84-86
FR-9For identical inputs, produce byte-identical accounts.dat and identical counts to legacy.bank.cob semantics
FR-10Keep the on-disk record layout unchanged (fixed-width-compatible).bank.cob L2-10, L34-48

2. Non-functional requirements

IDRequirementRationale
NFR-1Batch time is O(accounts + operations); no per-operation full-file scan.Removes the bank.cob L108 / L133 bottleneck.
NFR-2At most one write of accounts.dat per run; no temp-file shuffle.Reduce I/O churn.
NFR-3Runs on Python 3 standard library only.Portability, easy hosting.
NFR-4Malformed rows are rejected and processing continues (no whole-batch abort).Match legacy tolerance; met — bank.py L23-25, AT-11.
NFR-5Behaviour is continuously verifiable by automated tests.scripts/test_bank.py (legacy/parity) + tests/test_modern.py (engine) + scripts/benchmark.py.

3. Shared test fixture

T=$(mktemp -d)
printf '%s\n' '00000001|000000000100' '00000002|000000000100' > "$T/accounts.dat"
printf '%s\n' \
 'D|00000001|00000000|000000000050' \
 'W|00000001|00000000|000000000020' \
 'T|00000002|00000001|000000000030' \
 'W|00000001|00000000|000000099999' \
 'T|00000001|00000001|000000000001' \
 'Z|00000001|00000000|000000000001' \
 'D|00000009|00000000|000000000001' > "$T/operations.dat"
python3 modern/bank.py "$T"
cat "$T/accounts.dat"

Observed 2026-09-23:

PROCESSED=00000003
REJECTED=00000004
TOTAL_CENTS=0000000000000230
00000001|000000000160
00000002|000000000070

4. Acceptance test matrix

IDCoversGiven → When → ThenObserved 2026-09-23Status
AT-1FR-1, FR-7acct1=100; D acct1 50 → acct1=150acct1 advanced 100→150 within run
AT-2FR-1, FR-7acct1=150; W acct1 20 → acct1=130applied; no rejection
AT-3FR-1, FR-7T acct2→acct1 30 → acct2:100→70, acct1:130→160final acct1=160, acct2=70
AT-4FR-6W acct1 99999 with 160 available → rejectedcounted in REJECTED=4
AT-5FR-5T acct1→acct1 1 → rejected (self)counted in REJECTED=4
AT-6FR-2Z acct1 1 (bad kind) → rejectedcounted in REJECTED=4
AT-7FR-4, FR-5D acct9 1 (unknown source) → rejectedcounted in REJECTED=4
AT-8FR-8stdout zero-padded 8/8/16...00000003 / ...00000004 / 0000000000000230
AT-9FR-7, FR-10resulting accounts.dat layout unchanged`00000001\000000000160 (+acct2`)
AT-10FR-8TOTAL_CENTS = sum of balances160+70 = 230 = 0000000000000230
AT-11FR-3, NFR-4amount ABCDEFGHIJKL → reject & continuerejected; batch continued (bank.py L23-25); verified 2026-09-23
AT-12FR-9legacy vs modern on identical input → identical outputparity YES, byte-identical accounts.dat in all scripts/evidence/benchmark-*.txt; verified 2026-09-23
AT-13NFR-1speedup grows with account count (linear modern vs quadratic legacy)sweep 300→4.3x, 600→8.0x, 1200→13.1x, 2400→24.6x, all parity ok (benchmark-sweep.txt)

Expected counts cross-check: 3 accepted (D, W, T) and 4 rejected (insufficient, self, bad-kind, unknown-source) ⇒ PROCESSED=3, REJECTED=4. ✔

Automated coverage (2026-09-23). python3 tests/test_modern.pyRan 5 tests … OK:

TestCovers
test_deposit_withdrawal_and_transfer_use_integer_centsAT-1, AT-2, AT-3, AT-8
test_malformed_operations_are_rejected_and_batch_continuesAT-11
test_overdraft_unknown_recipient_and_self_transfer_are_rejectedAT-4, AT-5, AT-7
test_balance_overflow_is_rejected_without_widening_ledger_recordSafety divergence: 12-digit overflow rejection (see 02-before-after.md §4.3)
test_duplicate_accounts_fail_before_any_writeSafety divergence: duplicate-ID rejection

Plus scripts/test_bank.pyRan 33 tests … OK (with the isolated cobc): fixture generator and metrics-parser units, legacy contract tests, legacy/modern parity tests on byte-identical fixtures, characterization tests for documented divergences (including duplicate IDs and 12-digit overflow), and an end-to-end benchmark.py integration test.

Run them as python3 tests/test_modern.py and GNUBOL_PREFIX=/tmp/gcb-build/install python3 scripts/test_bank.py. The demo site (site/tests/, 50 tests) and the operator terminal (legacy-ui/tests/test_terminal.py, 20 tests) run separately.

5. Full parity / performance test (runnable)

# With the isolated GnuCOBOL 3.2.0 built by scripts/get_cobc.sh. Compiles legacy/bank.cob,
# generates identical fixtures for both programs, and asserts identical stdout +
# byte-identical accounts.dat before reporting any speedup.
GNUBOL_PREFIX=/tmp/gcb-build/install python3 scripts/benchmark.py --accounts 1200 --operations 400

unavailable and speedup: null when cobc is missing (never estimates).

1.003951 s → 0.035133 s = 28.6x) and benchmark-5000x500.txt (median 4.727858 s → 0.036914 s = 128.1x). Disclosure: the fixtures are synthetic and the legacy design is deliberately inefficient (whole-file rewrite per operation); these are not general claims about COBOL. See 02-before-after.md §6.


6. Definition of done

A requirement is done when its AT passes and has a saved transcript. Currently: AT-1…AT-13 all pass — AT-1…AT-11 via the fixture and the 5 tests/test_modern.py cases, AT-12/AT-13 via the byte-identical parity runs and the scaling sweep, plus 33 scripts/test_bank.py tests. The demo site (site/tests/, 50) and terminal (legacy-ui/tests/, 20) suites pass independently. The documented safety divergence — modern rejects duplicate account IDs and 12-digit balance overflow while legacy rewrites duplicate rows and truncates the overflow — is intentional and covered by tests (see 02-before-after.md §4.3). The remaining caveat is operational, not correctness: these were run locally with the isolated compiler, not yet against the public hosted prototype (no https://bbs-sg-bank-demo.pages.dev).

Next actions

  1. Add a tests/__init__.py (or a pytest wrapper) so both suites run via one command.
  2. Guard accounts.dat parsing (bank.py L13) to close the remaining robustness gap.
  3. Re-run the benchmark on the deployment host once hosted, to record production-machine timings.