Skip to content
Merged
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
399 changes: 399 additions & 0 deletions e2e/tests/social-messages.spec.ts

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion knowledge/changelogs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
3.25.2 || 28.08.2026
test(e2e) + fix(marketing): the social Messages surface e2e (social-e2e lane) — and the two DM bugs it caught. **The spec (`e2e/tests/social-messages.spec.ts`, 4 tests):** API floor pinning the app's exact DM pattern — the DM group is created with the deterministic NAME `dm-{sorted}` (invite_only, one `member` role, both participants as bare-username members), the API derives the group_id as `{provider}/groups/users/{creator}/{name}` (the creator is embedded in the ID — not symmetric), the recipient finds the group by name suffix in their own group list, messages are `posts` docs in the group (send → read-back → reply round-trip) + the deterministic-name no-duplicate test + the I3 anti-test (a third user's read of the DM group 403s "not a member"). Browser gauntlet: two pre-authed contexts (token cookies on social.localhost + auth.localhost) — A composes to B by username (the picker's compose path; the first send creates the group), B's context sees the conversation in the list (other party resolved from membership) and A's message in the thread, B replies (group reused, no second create), A's reload sees the reply; `[social-dms]` seam-log sequence asserted on both sides (find-null → create → sent; find-match → read → reply-sent; re-read 2), same group_id on both sides, no pageerror. **Bug 1 (the surface was broken end to end):** `dms.ts` derived the DM group as `web10.app/groups/{first}/dm-{second}` (the KB's well-known shape) and added members with provider-qualified keys (`web10.app/users/{username}`). Neither can exist: `POST /v3/groups/create` derives the group_id from the caller's token (`{provider}/groups/users/{creator}/{name}` — the well-known shape is only creatable by direct DB insert, like the boot-time discover group), and the node's user key is the BARE username, so the provider-qualified member keys never matched the real users — the recipient was never a member and every receive 403'd. Fix (in-lane, `src/data/dms.ts`): the deterministic identifier is the group NAME `dm-{sorted}` (the `messages-demo` findDmGroup reference pattern); both parties find the group by name suffix in their own group list; member keys are bare usernames; `listConversations` resolves the other party from membership (the name alone is ambiguous — usernames may contain dashes) with a `member_count === 2` guard; `readDms` on a not-yet-existing conversation returns `[]` instead of 403. **Bug 2 (isMe never matched):** `sender_provider` derived from a bare `author_key` is the literal string `web10` (`extractProvider`'s fallback), never the node's provider, so the provider-qualified isMe comparison never matched in ANY environment — own messages rendered as the other party's (left-aligned, no edit/delete menu). Now username-based (v3 DMs are same-node): DmsScreen bubble, MailView thread + formatFromTo, CrmView, and `classifyThread`/`replyAllTargets` in dms.ts. **Out of lane (`.context/social-e2e-messages-handoff.md`):** `groups.ts` still exports the broken `dmGroupId`/`ensureDmGroup`, and the SAME well-known-shape mismatch affects `followersGroupId`/`closeFriendsGroupId`/`ensureCommunity` (the feed/groups/profile lanes will hit it); KB drift — `db/clickhouse.md`'s `user_blacklist`/`group_blacklist` DDL is stale (code expects `updated_at`+`deleted`, a fresh CH built from the KB DDL 500s on every group read) and `social-contracts.md`'s DM Group ID column is uncreatable via the API. Screenshots re-captured (chat/mail/crm/settings, desktop + 375px) — green run per the temporary no-PNG-read override. 197 web10-social unit tests green, `tsc -b && vite build` clean, spec tsc-clean (the pre-existing exporter/gauntlet/messages-demo tsc errors are untouched).

3.25.1 || 28.08.2026
fix(marketing-social) + test(e2e): the social app's follows surface works — the follow button actually follows — and the groups e2e floor + gauntlet land (social-e2e lane, the M0 machine track). **The bug:** the app's `followersGroupId` returned `web10.app/groups/{username}/followers` — the wrong shape (missing the `users/` segment) and a hardcoded provider (`web10.app`) instead of the node's. The API derives a created group's ID from the token's `provider` claim as `{provider}/groups/users/{creator}/{slug}`, so the app was addressing a group that never exists: the follow button's `joinGroup` 404'd and silently reverted to "Follow", and `ensureFollowers` created the group under a doubled-username slug + a full-key member_key (`web10.app/users/{username}`) the membership checks never match (joins + discover auto-enrollment store the bare username). **The fix (`src/data/groups.ts` + `follows.ts`):** `followersGroupId(username, provider?)` now returns `{provider}/groups/users/{username}/followers` — the `provider` is the token's (the source of truth the API embeds), falling back to `API_HOST` (the two always agree — the token's provider is the API's own hostname). `ensureFollowers` creates under the bare slug `followers` (the API embeds the creator) with the bare username as the owner member_key. `isFollowing` now checks `getMyGroups()` membership — the API's `groups/get` does not return `my_role`, so the old per-group check was always false. The follow button (`UserProfileScreen.handleFollow`) gets `[social]` logging at each transition. **The e2e (`e2e/tests/social-groups.spec.ts`):** the API floor pins the app's exact feed read (getMyGroups minus discover → one multi-group posts read) + the follow/unfollow group ops + the feed-read delta (follow → the creator's post enters the feed; unfollow → it's gone, 403 on a direct read) + the I3 anti-test (a stranger who never followed cannot read the followers group). The browser gauntlet: viewer pre-authed via the token cookie (social.localhost + auth.localhost) → open the creator's `/u/:username` → follow (the real button) → the post appears in `/feed` → unfollow → it leaves, with console log-sequence verification. Group *management* (create/roles/invite) stays the authenticator + marketing-directory surface — its floors live in `groups-demo`. **Tests:** 3 e2e green (2 API floor + 1 browser gauntlet, stable across runs), 200 web10-social unit tests green (`follows.test.ts` expectations re-aligned to the new ID pattern), `tsc -b` + `vite build` clean, screenshots verified desktop + 375px.

3.25.0 || 27.08.2026
test(api): the tagged-post ad conformance floor (D55) — the ads lane's foundation. An ad is a `posts` doc tagged `ad` (a leaf-typed `offer` + a `status`), not a service — so the API has zero ad-specific branches, and the conformance is that the ad post is indistinguishable from a post except its tag + body fields. Pinned by `api/tests/test_ads.py` (5 tests): (1) the ad post is created through the existing `/v3/create` on `posts` (no `ads` service, no new endpoint) with `tags=["ad"]` + the leaf-typed `offer` + `status` in the body, attached to the creator's followers group; (2) the feed read (`read_documents_in_groups` over the followers group) returns it interleaved with normal posts — same shape, same keys, the ad fields are the only delta; (3) I3 — a non-follower is an access failure (D42 403) and the document query never runs, so the ad is never returned; (4) `status` is a plain body field the read does NOT filter — a paused ad comes back exactly like an active one (curation + the renderer filter client-side, D51). Plus a SQL-level pin: the feed read query selects the `tags` column but filters on neither tags nor status (`status` is a body field, not a column — it cannot be filtered in SQL at all). No production code changed — the API already treats ad posts as posts (verified: no `offer` / `ad`-tag branches in `api/app/v3/`). 795 API tests green, ruff clean. Gates the catalog + composer (both read this).

3.24.0 || 28.08.2026
feature(api) + feature(ui) + test(e2e) + docs(kb): groups default to NOT discoverable (D53 amendment) + the authenticator's group contract editors work and are torture-tested. Operator, on the discover tab: "in the group discover i am seeing all the groups. i think groups sshould default not discover, in these demo apps like notes etc, messages, where it is a backend thing for an app, why should that be in the discover tab" — then: "also i want the contract policy editors to work and be torture tested, and to make groups discoverable toggle stuff like that, talking about the authenticator." **The default flip (D53 amendment):** `create_group` now defaults `discoverable` to `False` for every join policy — listing is an opt-in, not a default. App-backend groups (notes, messages, DMs) are infrastructure the apps create on the user's behalf, not communities meant to be browsed, so they stay out of the directory unless an owner deliberately blasts them; the `invite_only` special-case is subsumed (all groups default to `false`). The DDL default flips to `0` (template + boot `ALTER`). The node stays readable-by-design (D41): the detail (by-ID read) is still open to any principal (unlisted-model) — only the *browse* surface is opt-in. **The backfill (one-time, sentinel-gated):** groups created under the earlier discoverable-by-default rule carry `discoverable = 1`; a boot migration (`_migrate_discoverable_default_flip`) delists them (appends a `discoverable = 0` row per live listed group) so the directory reflects the opt-in model. It runs exactly once (a `node_config` sentinel `migration:discoverable_default_flip` marks completion) and only ever moves groups OUT of the directory (membership / content access untouched); safe under concurrent gunicorn workers (duplicate rows dedup to one). **The contract policy editors (authenticator):** the "Settings" button was a TODO ("Join policy editor coming soon") — now a real `GroupSettingsDialog` join-policy editor (Open / Request / Invite-only picker → `v3UpdateGroup({join_policy})`). The roles editor (`GroupRolesDialog`) and the "List in directory" (`discoverable`) toggle already existed and are now verified end-to-end. All three contract controls (roles, join_policy, discoverable) work through the real UI → `POST /v3/groups/update` → persisted. **Bug fix (exposed by the torture tests):** `groupDisplayName` returned `users/<username>` (parts[2]/parts[3]) instead of the slug — so every group a user owned showed the same name; it now returns the slug (the last path segment). **Torture tests (new `e2e/tests/group-contract-editors.spec.ts`, 11 tests):** API floor (join_policy / roles / discoverable update persists; I3 anti-test — a non-member's update is rejected, the API maps a permission denial to the `CRUD` 401) + browser gauntlet (real UI: join-policy change → persisted + card badge updates; cancel fork → no change; save-failure fork → status-bar error, no crash, dialog stays open; roles add → persisted; empty-role-name anti-test → blocked, nothing saved; discoverable toggle ON → listed in the anon directory; toggle OFF → delisted). Every browser test asserts no pageerror (diagnostic dump). **KB:** D53 amended in `decisions.md` (default `false`, the operator's amendment quoted) + `groups/discoverability.md` (opt-in model + backfill note). **Tests:** 3 new API unit tests (backfill: skip-when-sentinel, delist+set-sentinel, delist-query-filters-discoverable-1) + the `TestCreateGroup` defaults re-aligned (not-discoverable for every join policy) + 5 new UI unit tests (GroupSettingsDialog: current-policy-selected, pick+save, save-failure, cancel; GroupCard: undefined→not-listed) + the 11 e2e torture tests. 789 API + 105 UI green, ruff + `tsc -b` clean, existing `groups-demo.spec.ts` (30) green (no regression).

Expand Down
2 changes: 1 addition & 1 deletion knowledge/strategy/parallel-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ exact counts (the `feed-demo` pattern).
- [ ] Feed (`e2e/tests/social-feed.spec.ts`) — API floor: the app's exact feed read (discover + followers multi-group, sort config) + I3 (a non-follower's group post is absent). Browser gauntlet: real D42 login → feed renders → post → reload persists.
- [✓ 3.25.1] Groups (`e2e/tests/social-groups.spec.ts`) — the app's groups surface: follows (followers groups) as the app drives them — follow → the creator's posts enter the feed → unfollow → they leave. API floor: the follow/unfollow group ops + the feed-read delta. Browser gauntlet: follow/unfollow through the app, feed reflects it. (Group *management* — create/roles/invite — is the authenticator + marketing directory surface; its floors live in `groups-demo`.)
- [ ] Profiles (`e2e/tests/social-profile.spec.ts`) — API floor: profile doc + posts read + follower count; I3 (a stranger's private data is not readable). Browser gauntlet: own profile (edit persists) + another user's public profile + the `/u/:username/p/:postId` deep link.
- [ ] Messages (`e2e/tests/social-messages.spec.ts`) — API floor: DM group contract + CRUD (deterministic DM group ID). Browser gauntlet: two-user DM round-trip through the app (send → receive → reply).
- [✓ 3.25.2] Messages (`e2e/tests/social-messages.spec.ts`) — API floor: DM group contract + CRUD (deterministic DM group ID). Browser gauntlet: two-user DM round-trip through the app (send → receive → reply).
- [ ] Settings (`e2e/tests/social-settings.spec.ts`) — API floor: settings doc read/write round-trip. Browser gauntlet: change a setting → persists across reload + sign-out/sign-in.
- [ ] Trending (`e2e/tests/social-trending.spec.ts`) — the `/discover` board surface (the in-app trending: D36 knobs over the node-default discover group). API floor: anon board read + engagement counts. Browser gauntlet: the board renders seeded posts, the knobs re-rank, deep-linkable state. (Complements the `discover-board` lane's board gauntlet — that one owns the moderation ops: seed → anon read → hide/restore round-trip.)
- [ ] Capstone gauntlet (`e2e/tests/social-gauntlet.spec.ts`) — one journey across all screens (login → feed → post → profile → DM → follow → settings → reload), log-sequence verified. **Gated on all six surface specs above.**
Expand Down
Binary file modified marketing/web10-social/screenshots/chat-375.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified marketing/web10-social/screenshots/chat-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified marketing/web10-social/screenshots/crm-375.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified marketing/web10-social/screenshots/crm-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified marketing/web10-social/screenshots/mail-375.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified marketing/web10-social/screenshots/mail-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified marketing/web10-social/screenshots/settings-375.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified marketing/web10-social/screenshots/settings-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion marketing/web10-social/src/components/Chat/CrmView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -692,8 +692,12 @@ function ContactDetail({
<div className="space-y-3">
{messages.map((msg) => {
const token = getWapi().readToken();
// Username-based: v3 DMs are same-node (bare-username member
// keys), and the sender_provider derived from a bare
// author_key is not the node's provider, so a
// provider-qualified comparison never matches.
const isMe = token
? `${token.provider}/${token.username}` === `${msg.sender_provider}/${msg.sender_username}`
? msg.sender_username === token.username
: false;
return (
<div
Expand Down
11 changes: 9 additions & 2 deletions marketing/web10-social/src/components/Chat/DmsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,10 @@ function MessageBubble({
}

return (
<div className={cn('flex items-end gap-1.5', isMe ? 'justify-end' : 'justify-start')}>
<div
data-testid="dm-message"
className={cn('flex items-end gap-1.5', isMe ? 'justify-end' : 'justify-start')}
>
<div
className={cn(
'group max-w-[75%] px-4 py-2.5 rounded-2xl text-sm leading-relaxed transition-shadow duration-150 relative',
Expand Down Expand Up @@ -940,7 +943,11 @@ export default function DmsScreen() {
msg={msg}
isMe={
token
? `${token.provider}/${token.username}` === `${msg.sender_provider}/${msg.sender_username}`
// Username-based: v3 DMs are same-node (bare-username
// member keys), and the sender_provider derived from a
// bare author_key is not the node's provider, so a
// provider-qualified comparison never matches.
? msg.sender_username === token.username
: false
}
onDelete={handleDeleteMessage}
Expand Down
10 changes: 5 additions & 5 deletions marketing/web10-social/src/components/Chat/MailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ function ThreadDetail({
onSent: (msg: DmRecord) => void;
}) {
const token = getWapi().readToken();
const myKey = token ? `${token.provider}/${token.username}` : '';
const myUsername = token?.username || '';
const [input, setInput] = useState('');
const [subject, setSubject] = useState('');
Expand All @@ -218,9 +217,10 @@ function ThreadDetail({
const threadSubject = thread.messages.find((m) => m.subject)?.subject || '';

function formatFromTo(msg: DmRecord) {
const senderKey = `${msg.sender_provider}/${msg.sender_username}`;
const recipientKey = `${msg.recipient_provider}/${msg.recipient_username}`;
const isMe = myKey === senderKey;
// Username-based: v3 DMs are same-node (bare-username member keys), and
// the sender_provider derived from a bare author_key is not the node's
// provider, so a provider-qualified comparison never matches.
const isMe = msg.sender_username === myUsername;
const fromName = isMe ? myUsername : msg.sender_username;
const toName = isMe ? msg.recipient_username : myUsername;
return { fromName, toName, isMe };
Expand Down Expand Up @@ -409,7 +409,7 @@ function ThreadDetail({
</div>
) : (
thread.messages.map((msg) => {
const isMe = myKey === `${msg.sender_provider}/${msg.sender_username}`;
const isMe = msg.sender_username === myUsername;
const { fromName, toName } = formatFromTo(msg);
return (
<div
Expand Down
Loading
Loading