---
name: forum-verify
description: Verify a GCC Forum change before committing — sweep every page on both boards, assert access in both directions, exercise write paths against the real tables, confirm no data drifted, and delete the harness. Use before every commit touching app/Forum/.
---

# Verifying a forum change

Run this before every commit. The board writes to tables holding twenty years of
posts, and its access rules fail silently — a leaked ticket renders as a
perfectly working page.

## Checklist

1. **Restart the app** if you added a CFC method — Lucee will not see a new
   signature otherwise.
2. **Page sweep** — every page, both boards, signed out and signed in.
3. **Access assertions — both directions.**
4. **Write paths** against the real tables, with cleanup.
5. **Row counts unchanged.**
6. **Timings, signed in.**
7. **Delete the harness.**

## 1. Restart first

```bash
docker restart gcc-local-app
```

Editing an existing method body recompiles; a **new method signature** does not,
and you get `Component [components.People] has no function with name [x]` while
the method is plainly on disk. `applicationStop()` does not clear it.

## 2. Page sweep

```bash
fails=0; i=0
for u in "p=index" "b=he&p=index" "p=section&s=100" "b=he&p=section&s=1" \
         "b=he&p=section&s=200" "p=thread&id=13366" "p=compose" "b=he&p=compose" \
         "b=he&p=tickets" "b=he&p=queue" "p=staff" "b=he&p=staff" \
         "p=search&q=fleet" "p=members" "p=profile&id=1" "p=messages" \
         "p=notifications" "p=moderation" "p=rules" "p=settings" \
         "hi=13366" "ch=100" "f=he_detail&hi=15400" "p=bogus" "b=xx&p=index"; do
  i=$((i+1)); f=/tmp/p$i.html
  curl -s -o "$f" "http://127.0.0.1:8888/Forum/index.cfm?$u"
  if grep -q "yellowfont" "$f"; then
    fails=$((fails+1))
    printf "FAIL %-28s %s\n" "$u" "$(grep -o 'Message: </span>[^<]*' "$f" | head -1 | cut -c17-100)"
  fi
done; echo "$fails failing of $i"; rm -f /tmp/p*.html
```

`yellowfont` is the class in the game's error template — grepping for it catches
a CFML error that still returned HTTP 200.

**Use a separate temp file per URL.** Reusing one silently reports the previous
page's size and hides differences.

Legacy URLs (`hi=`, `ch=`, `f=he_detail`) must resolve to the right board —
that is the compatibility contract with every old bookmark and PM link.

## 3. Access, both directions

The single most important step. A "staff can see it" assertion alone passed the
bug where section 105 listed in full to signed-out visitors.

```cfml
function asUser(uid, lvl, active) {
    session.userid = uid; session.username = "PROBE";
    session.adminflag = lvl; session.activeflag = active; session.confirmflag = 1;
    // MANDATORY between viewers, or the next level passes on the previous
    // one's cached section map and me().
    for (k in listToArray(structKeyList(request)))
        if (left(k,5) EQ "forum") structDelete(request, k);
    return new components.People();
}
```

Cover, at minimum:

| Viewer | Must |
|---|---|
| guest | see public sections and **Announcements (200/201)**; not private ones; empty tickets/queue |
| level 0, active | be able to file in 91; **not** post 22/200/201 |
| level 0, deactivated, **owning** a 91 thread | not browse 91; **read and reply to their own appeal**; see it in My Tickets; create only in 10/91/92 |
| a different level-0 player | **not** read that appeal |
| Guide (3) | browse 12, not 92; have a queue |
| Admin (9) | browse 92; post 200 and 201 |
| level 5 | post 200 but **not** 201 |

Pull the fixture from real data rather than inventing one:

```cfml
own = queryExecute("select h.id, h.userid from he h join `user` u on u.id=h.userid
    where h.type=91 and u.adminflag=0 and h.userid>0 order by h.id desc limit 1",
    {}, {datasource:"gcc"});
```

Also confirm the Forum is unchanged: The Vault still lists 1,341, section 100
still lists >7,000, sections 104/105 still hidden from guests.

## 4. Write paths

Against the real tables. Record every id and delete it afterwards.

```cfml
made = { threads: [] };
try {
    r = F.createThread("hef", 100, "ZZTEST title", "Body.", { token: F.token() });
    if (r.ok) arrayAppend(made.threads, r.id);
    // reply, edit, pin, lock, move, answer, report, archive, restore, subscribe
} catch (any e) { say("EXCEPTION", false, e.message); }

// NOT a finally block -- see docs/CONVENTIONS.md. Cleanup runs either way here.
for (t in made.threads) {
    queryExecute("delete from hef      where id=:i",       {i:t}, {datasource:"gcc"});
    queryExecute("delete from hef_old  where id=:i",       {i:t}, {datasource:"gcc"});
    queryExecute("delete from hef2     where belongto=:i", {i:t}, {datasource:"gcc"});
    queryExecute("delete from hef2_old where belongto=:i", {i:t}, {datasource:"gcc"});
    queryExecute("delete from hef_s    where id=:i",       {i:t}, {datasource:"gcc"});
    queryExecute("delete from forum_thread_meta where thread_id=:i", {i:t}, {datasource:"gcc"});
    // ... forum_read, forum_subscribe, forum_notify, forum_report, forum_modlog
}
```

Assert the cleanup worked, do not assume it.

## 5. Row counts unchanged

```sql
SELECT 'hef', COUNT(*) FROM hef      UNION ALL SELECT 'hef2', COUNT(*) FROM hef2
UNION ALL SELECT 'hef_old', COUNT(*) FROM hef_old
UNION ALL SELECT 'hef2_old', COUNT(*) FROM hef2_old
UNION ALL SELECT 'he', COUNT(*) FROM he UNION ALL SELECT 'he2', COUNT(*) FROM he2
UNION ALL SELECT 'he_old', COUNT(*) FROM he_old
UNION ALL SELECT 'he2_old', COUNT(*) FROM he2_old;
```

If a count moved, find out which row and why **before** committing. A row you
cannot account for is a row you may have destroyed.

Real posts made by real people during testing are not drift — check the newest
ids before assuming the harness leaked.

## 6. Timings, signed in

`myStanding()` only runs for a session, so a signed-out profile hides a 1.7s
regression.

```bash
for u in "p=index" "p=section&s=100" "p=thread&id=13366" "p=members"; do
  printf "%-24s %s\n" "$u" \
    "$(curl -s -b /tmp/cj -o /dev/null -w '%{time_total}' \
       "http://127.0.0.1:8888/Forum/index.cfm?$u")"
done
```

Baseline in [`../../docs/DEVELOPMENT.md`](../../docs/DEVELOPMENT.md). Index ~35ms,
section ~67ms, thread ~33ms, members ~86ms.

## 7. Clean up

```bash
rm -f app/Forum/_t.cfm app/Forum/_sess.cfm
git status --short app/Forum
```

Nothing untracked but the files you meant to add. Then confirm no legacy game
file was touched:

```bash
git status --short | grep -v "^??" | grep -v "app/Forum/\|app/Admin/sql/"
```

Empty is the answer you want. `hef.cfm` and the `f_he*.cfm` family stay
untouched — both boards run side by side.
