@volter/twin-x 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +138 -0
  3. package/dist/client/x-mirror.bundle.js +321 -0
  4. package/dist/client/x-mirror.d.ts +45 -0
  5. package/dist/client/x-mirror.js +417 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +29 -0
  8. package/dist/src/index.d.ts +14 -0
  9. package/dist/src/index.js +68 -0
  10. package/dist/src/x-budget.d.ts +54 -0
  11. package/dist/src/x-budget.js +123 -0
  12. package/dist/src/x-capabilities.d.ts +3 -0
  13. package/dist/src/x-capabilities.js +1106 -0
  14. package/dist/src/x-conformance.d.ts +8 -0
  15. package/dist/src/x-conformance.js +91 -0
  16. package/dist/src/x-connector.d.ts +125 -0
  17. package/dist/src/x-connector.js +546 -0
  18. package/dist/src/x-media.d.ts +87 -0
  19. package/dist/src/x-media.js +275 -0
  20. package/dist/src/x-mirror-ui.d.ts +61 -0
  21. package/dist/src/x-mirror-ui.js +253 -0
  22. package/dist/src/x-problems.d.ts +38 -0
  23. package/dist/src/x-problems.js +130 -0
  24. package/dist/src/x-scopes.d.ts +7 -0
  25. package/dist/src/x-scopes.js +62 -0
  26. package/dist/src/x-server.d.ts +14 -0
  27. package/dist/src/x-server.js +127 -0
  28. package/dist/src/x-twin.d.ts +21 -0
  29. package/dist/src/x-twin.js +1534 -0
  30. package/package.json +58 -0
  31. package/src/cli.ts +27 -0
  32. package/src/index.ts +132 -0
  33. package/src/x-budget.ts +150 -0
  34. package/src/x-capabilities.ts +1161 -0
  35. package/src/x-conformance.ts +113 -0
  36. package/src/x-connector.ts +546 -0
  37. package/src/x-media.ts +295 -0
  38. package/src/x-mirror-ui.ts +263 -0
  39. package/src/x-problems.ts +143 -0
  40. package/src/x-scopes.ts +67 -0
  41. package/src/x-server.ts +126 -0
  42. package/src/x-twin.ts +1545 -0
@@ -0,0 +1,1161 @@
1
+ // THE X PACK'S CAPABILITY MANIFEST — the real vendor surface as the DENOMINATOR, not a list of
2
+ // what got built. The denominator here is X API v2's POST OBJECT and the per-user timelines and
3
+ // engagement verbs over it, plus user lookup by id and by username (the curated boundary
4
+ // `x-spec-census.json` declares in `scopePaths`), plus the media a post carries — image and video
5
+ // upload. Follows, lists, spaces, communities, direct messages, compliance and usage
6
+ // are X surface this manifest deliberately does NOT enumerate — they are outside the census's
7
+ // committed boundary, and claiming them here would inflate a denominator nothing measures.
8
+ // (`packages/twin/xidentity` owns X's OAuth + identity surface and its own denominator.)
9
+ import { mkdtempSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { checkCapabilities, type CapabilityReport, type CapabilitySpec, verifyBoundary } from '@volter/world-tooling';
13
+ import { pushXAction, xRequestForAction, syncXFromReal, type XExecute } from './x-connector.ts';
14
+ import { X_BUDGET_CEILING, X_CALL_WEIGHTS, X_RATE_BUDGET, XBudget, XBudgetError, xCallWeight } from './x-budget.ts';
15
+ import { handleXTwinRequest } from './x-twin.ts';
16
+ import { createElement } from 'react';
17
+ import { renderToStaticMarkup } from 'react-dom/server';
18
+ import { PostRow, ProfileHeader } from '../client/x-mirror.tsx';
19
+ import { createXMirrorServer, joinedLabel, POST_READ_QUERY, PROFILE_READ_QUERY, tweetsById, usersById, type XRow } from './x-mirror-ui.ts';
20
+
21
+ type Body = Record<string, any>;
22
+ type Step = { m: string; p: string; b?: unknown; headers?: Record<string, string>; readOnly?: boolean };
23
+
24
+ const ORG = '1000';
25
+ const MEMBER = '2000';
26
+ const TOKEN = 'org-voice-token';
27
+ const FULL_SCOPES = ['tweet.read', 'tweet.write', 'users.read'];
28
+ const AUTH = { authorization: `Bearer ${TOKEN}` };
29
+ // X returns id/text/edit_history_tweet_ids and NOTHING ELSE unless the caller asks. Every
30
+ // verify below that asserts a threading field therefore REQUESTS it — which is itself half the
31
+ // proof of `x.fields.tweet_fields`: drop the projection and these reads stop carrying the
32
+ // fields they assert on.
33
+ const THREAD_FIELDS = 'tweet.fields=author_id,conversation_id,created_at,in_reply_to_user_id,referenced_tweets';
34
+
35
+ type Handle = (step: Step) => Promise<{ status: number; body: unknown; headers?: Record<string, string> }>;
36
+
37
+ /**
38
+ * A throwaway world with the org's account, a member's account, and a bearer the way xidentity's
39
+ * flow would have minted one. Every verify below runs against real twin state — a create→read→
40
+ * assert, never a status-only check.
41
+ */
42
+ async function withWorld(fn: (h: Handle, root: string) => Promise<boolean>, opts: { scopes?: string[] } = {}): Promise<boolean> {
43
+ const root = mkdtempSync(join(tmpdir(), 'x-cap-'));
44
+ const h: Handle = (step) => handleXTwinRequest({
45
+ method: step.m,
46
+ path: step.p,
47
+ body: step.b === undefined ? undefined : JSON.stringify(step.b),
48
+ root,
49
+ readOnly: step.readOnly,
50
+ occurredAt: new Date().toISOString(),
51
+ headers: step.headers ?? AUTH,
52
+ });
53
+ try {
54
+ return await verifyBoundary('x.withWorld', async () => {
55
+ await h({ m: 'POST', p: '/_twin/accounts', b: { id: ORG, username: 'orgvoice', name: 'Org Voice' }, headers: {} });
56
+ await h({ m: 'POST', p: '/_twin/accounts', b: { id: MEMBER, username: 'member', name: 'A Member' }, headers: {} });
57
+ await h({ m: 'POST', p: '/_twin/tokens', b: { token: TOKEN, account_id: ORG, scopes: opts.scopes ?? FULL_SCOPES }, headers: {} });
58
+ return fn(h, root);
59
+ });
60
+ } finally {
61
+ rmSync(root, { recursive: true, force: true });
62
+ }
63
+ }
64
+
65
+ /** Seed an inbound mention from the member and return its id — the input to the reply class. */
66
+ async function seedMention(h: Handle, text = 'hey @orgvoice can you help?'): Promise<string> {
67
+ const seeded = await h({ m: 'POST', p: '/_twin/posts', b: { author_id: MEMBER, text }, headers: {} });
68
+ return String((seeded.body as Body).data.id);
69
+ }
70
+
71
+ // ── media fixtures: the smallest bytes the twin reads as an image and as a video ─────────────
72
+ function box(type: string, payload: Uint8Array): Uint8Array {
73
+ const out = new Uint8Array(8 + payload.length);
74
+ new DataView(out.buffer).setUint32(0, out.length);
75
+ out.set(new TextEncoder().encode(type), 4);
76
+ out.set(payload, 8);
77
+ return out;
78
+ }
79
+ const concat = (...parts: Uint8Array[]): Uint8Array => {
80
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
81
+ let at = 0;
82
+ for (const p of parts) { out.set(p, at); at += p.length; }
83
+ return out;
84
+ };
85
+ /** A PNG header naming a 64x48 image (the twin reads type and size from the header, as X does). */
86
+ function tinyPng(): Uint8Array {
87
+ const ihdr = new Uint8Array(13);
88
+ new DataView(ihdr.buffer).setUint32(0, 64);
89
+ new DataView(ihdr.buffer).setUint32(4, 48);
90
+ ihdr.set([8, 2, 0, 0, 0], 8);
91
+ return concat(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), box('IHDR', ihdr), new Uint8Array(4));
92
+ }
93
+ /** An MP4's boxes for a 3-second 640x360 video: ftyp, then moov with mvhd and a video tkhd. */
94
+ function tinyMp4(): Uint8Array {
95
+ const mvhd = new Uint8Array(100);
96
+ new DataView(mvhd.buffer).setUint32(12, 1000);
97
+ new DataView(mvhd.buffer).setUint32(16, 3000);
98
+ const tkhd = new Uint8Array(84);
99
+ new DataView(tkhd.buffer).setUint32(76, 640 << 16);
100
+ new DataView(tkhd.buffer).setUint32(80, 360 << 16);
101
+ return concat(box('ftyp', new TextEncoder().encode('isom\0\0\x02\0isom')), box('mdat', new Uint8Array(4096)), box('moov', concat(box('mvhd', mvhd), box('trak', box('tkhd', tkhd)))));
102
+ }
103
+ const b64 = (bytes: Uint8Array): string => Buffer.from(bytes).toString('base64');
104
+ const MEDIA_AUTH = { authorization: 'Bearer org-media-token' };
105
+ /** Register a bearer that also holds `media.write`, which the upload endpoints require. */
106
+ async function mediaToken(h: Handle): Promise<void> {
107
+ await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'org-media-token', account_id: ORG, scopes: [...FULL_SCOPES, 'media.write'] }, headers: {} });
108
+ }
109
+
110
+ const done = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], verify: CapabilitySpec['verify']): CapabilitySpec =>
111
+ ({ id, area, title, dimension, tier, expected: 'done', verify });
112
+ const todo = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier']): CapabilitySpec =>
113
+ ({ id, area, title, dimension, tier, expected: 'todo' });
114
+
115
+ export const X_CAPABILITIES: CapabilitySpec[] = [
116
+ // ── streaming + media (X's OAuth 2.0 authorize/token/refresh/revoke round trip is owned by
117
+ // packages/twin/xidentity; this pack CONSUMES the user access token that flow mints) ─────────
118
+ todo('x.stream.filtered', 'streaming', 'Filtered stream (GET /2/tweets/search/stream): serve the long-lived streaming connection over the twin\'s stored posts, matching the filtered-stream rules, with reconnect and backfill_minutes semantics', 'api', 'common'),
119
+ todo('x.stream.sampled', 'streaming', 'Sampled volume streams (GET /2/tweets/sample/stream and sample10): serve the streaming connection over the twin\'s stored posts with the vendor\'s partitioning parameters', 'api', 'niche'),
120
+ done('x.media.upload', 'media', 'Media upload: POST /2/media/upload for an image, and the chunked initialize/append/finalize protocol with its STATUS-polled processing states for a video, persisting the bytes and returning a media_id attachable to a post', 'api', 'common', () =>
121
+ withWorld(async (h) => {
122
+ await mediaToken(h);
123
+ // an image in one request, base64 in a JSON body: its size and dimensions read from its bytes
124
+ const image = await h({ m: 'POST', p: '/2/media/upload', b: { media: b64(tinyPng()), media_category: 'tweet_image' }, headers: MEDIA_AUTH });
125
+ const img = (image.body as Body).data;
126
+ if (image.status !== 200 || !/^3_\d+$/.test(img?.media_key) || img.image?.w !== 64 || img.image?.h !== 48 || img.expires_after_secs !== 86400) return false;
127
+ // media.write is required: the posting token without it is refused
128
+ const noScope = await h({ m: 'POST', p: '/2/media/upload', b: { media: b64(tinyPng()), media_category: 'tweet_image' } });
129
+ if (noScope.status !== 403) return false;
130
+ // a video in two segments, then processing walked by STATUS
131
+ const mp4 = tinyMp4();
132
+ const init = await h({ m: 'POST', p: '/2/media/upload/initialize', b: { media_type: 'video/mp4', total_bytes: mp4.length, media_category: 'tweet_video' }, headers: MEDIA_AUTH });
133
+ const id = (init.body as Body).data?.id as string;
134
+ if (init.status !== 200 || !/^7_\d+$/.test((init.body as Body).data?.media_key)) return false;
135
+ const half = Math.floor(mp4.length / 2);
136
+ for (const [index, part] of [[0, mp4.subarray(0, half)], [1, mp4.subarray(half)]] as const) {
137
+ const appended = await h({ m: 'POST', p: `/2/media/upload/${id}/append`, b: { media: b64(part), segment_index: index }, headers: MEDIA_AUTH });
138
+ if (appended.status !== 200) return false;
139
+ }
140
+ const finalized = (await h({ m: 'POST', p: `/2/media/upload/${id}/finalize`, headers: MEDIA_AUTH })).body as Body;
141
+ if (finalized.data?.size !== mp4.length || finalized.data?.processing_info?.state !== 'pending') return false;
142
+ const early = await h({ m: 'POST', p: '/2/tweets', b: { text: 'too soon', media: { media_ids: [id] } }, headers: MEDIA_AUTH });
143
+ if (early.status !== 400) return false;
144
+ const states: string[] = [];
145
+ for (let i = 0; i < 2; i += 1) states.push(((await h({ m: 'GET', p: `/2/media/upload?command=STATUS&media_id=${id}`, headers: MEDIA_AUTH })).body as Body).data?.processing_info?.state);
146
+ return states.join(',') === 'in_progress,succeeded';
147
+ })),
148
+ todo('x.media.video_poster_frame', 'media', "A video's preview_image_url as a frame of the video (the twin decodes nothing and serves a stand-in poster at the video's size)", 'api', 'common'),
149
+ todo('x.media.video_transcode_variants', 'media', "The variants X's transcoder publishes (an HLS playlist and several MP4 bit rates) rather than the one uploaded file", 'api', 'niche'),
150
+ todo('x.media.gif', 'media', 'Animated GIF upload (tweet_gif) and its animated_gif media type', 'api', 'common'),
151
+ todo('x.media.alt_text', 'media', 'Alt text on an image (POST /2/media/metadata) and the alt_text media field', 'api', 'common'),
152
+
153
+ // ── the ORIGINAL POST class (POST /2/tweets, no reply object) ────────────────────────────────
154
+ done('x.tweets.create', 'tweets', 'Create an original post (POST /2/tweets)', 'api', 'core', () =>
155
+ withWorld(async (h) => {
156
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: 'the org speaks' } });
157
+ const body = created.body as Body;
158
+ if (created.status !== 201 || typeof body.data?.id !== 'string' || body.data.text !== 'the org speaks') return false;
159
+ // Read it back through the twin's own state: a create that does not land is not a create.
160
+ const seenFrom = await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions`, headers: AUTH });
161
+ // The org's own post mentions nobody, so the member's mentions timeline stays empty —
162
+ // which is the read-back that proves the post is real STATE, not an echoed request.
163
+ return seenFrom.status === 200 && (seenFrom.body as Body).meta.result_count === 0;
164
+ })),
165
+ done('x.tweets.create_shape', 'tweets', "Create answers X's thin {id, text, edit_history_tweet_ids} envelope", 'api', 'core', () =>
166
+ withWorld(async (h) => {
167
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: 'shape check' } });
168
+ const data = (created.body as Body).data;
169
+ return created.status === 201
170
+ && Object.keys(data).sort().join(',') === 'edit_history_tweet_ids,id,text'
171
+ && Array.isArray(data.edit_history_tweet_ids)
172
+ && data.edit_history_tweet_ids.length === 1
173
+ && data.edit_history_tweet_ids[0] === data.id;
174
+ })),
175
+ done('x.tweets.create_is_its_own_conversation', 'tweets', 'An original post starts its own conversation', 'api', 'core', () =>
176
+ withWorld(async (h) => {
177
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member welcome' } });
178
+ const id = (created.body as Body).data.id;
179
+ const mentions = await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions?${THREAD_FIELDS}` });
180
+ const row = (mentions.body as Body).data[0];
181
+ // conversation_id === its own id, and it carries no referenced_tweets: this is a root post.
182
+ return row.id === id && row.conversation_id === id && row.referenced_tweets === undefined && row.in_reply_to_user_id === undefined;
183
+ })),
184
+ done('x.tweets.create_requires_text', 'tweets', 'A create with no text is the invalid-request problem', 'api', 'core', () =>
185
+ withWorld(async (h) => {
186
+ const r = await h({ m: 'POST', p: '/2/tweets', b: {} });
187
+ const body = r.body as Body;
188
+ return r.status === 400
189
+ && body.type === 'https://api.x.com/2/problems/invalid-request'
190
+ && body.title === 'Invalid Request'
191
+ && Array.isArray(body.errors) && body.errors[0].parameters.text !== undefined;
192
+ })),
193
+ done('x.tweets.ids_are_snowflake_shaped_and_unique', 'tweets', 'Minted post ids are numeric snowflakes, never a row count', 'api', 'core', () =>
194
+ withWorld(async (h) => {
195
+ const ids: string[] = [];
196
+ for (const text of ['one', 'two', 'three']) {
197
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text } });
198
+ ids.push((r.body as Body).data.id);
199
+ }
200
+ // Derived from the id SET already in state, so a pulled vendor id above the row count can
201
+ // never be clobbered by a later local create.
202
+ return ids.every((id) => /^\d{19}$/.test(id)) && new Set(ids).size === 3
203
+ && BigInt(ids[1]!) > BigInt(ids[0]!) && BigInt(ids[2]!) > BigInt(ids[1]!);
204
+ })),
205
+
206
+ // ── the MENTION-REPLY class (POST /2/tweets WITH reply.in_reply_to_tweet_id) ─────────────────
207
+ // Same route, same method, same vendor operation as the class above. Everything that makes
208
+ // them two things lives in the body field these verifies exercise.
209
+ done('x.tweets.reply', 'tweets', 'Reply to a post (POST /2/tweets with reply.in_reply_to_tweet_id)', 'api', 'core', () =>
210
+ withWorld(async (h) => {
211
+ const mentionId = await seedMention(h);
212
+ // The text carries `@member` because that is what a reply on X looks like: the client
213
+ // prepends the parent author's handle, so a reply IS a mention of them. The twin does not
214
+ // rewrite text, so the verify posts what a client would post.
215
+ const replied = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member happy to help', reply: { in_reply_to_tweet_id: mentionId } } });
216
+ if (replied.status !== 201) return false;
217
+ const replyId = (replied.body as Body).data.id;
218
+ // Read the reply back out of real twin state through the MEMBER's mentions timeline, and
219
+ // assert the THREADING the body field caused — not the status the request returned.
220
+ const conversation = await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions?${THREAD_FIELDS}` });
221
+ const rows = (conversation.body as Body).data as Body[];
222
+ const stored = rows.find((r) => r.id === replyId);
223
+ return stored !== undefined
224
+ && stored.in_reply_to_user_id === MEMBER
225
+ && stored.conversation_id === mentionId
226
+ && stored.referenced_tweets?.[0]?.type === 'replied_to'
227
+ && stored.referenced_tweets?.[0]?.id === mentionId;
228
+ })),
229
+ done('x.tweets.reply_joins_the_parent_conversation', 'tweets', "A reply inherits the parent's conversation_id, not its own", 'api', 'core', () =>
230
+ withWorld(async (h) => {
231
+ const rootId = await seedMention(h);
232
+ const first = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member one', reply: { in_reply_to_tweet_id: rootId } } });
233
+ const firstId = (first.body as Body).data.id;
234
+ const second = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member two', reply: { in_reply_to_tweet_id: firstId } } });
235
+ const secondId = (second.body as Body).data.id;
236
+ const rows = (await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions?${THREAD_FIELDS}` })).body as Body;
237
+ const deep = (rows.data as Body[]).find((r) => r.id === secondId);
238
+ // Two levels down the thread and still in the ROOT's conversation — the vendor's rule.
239
+ return deep?.conversation_id === rootId && deep?.referenced_tweets?.[0]?.id === firstId;
240
+ })),
241
+ done('x.tweets.reply_to_missing_post_refuses', 'tweets', 'A reply to a post that does not exist is resource-not-found', 'api', 'core', () =>
242
+ withWorld(async (h) => {
243
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'to nobody', reply: { in_reply_to_tweet_id: '404404404404404404' } } });
244
+ const body = r.body as Body;
245
+ return r.status === 404
246
+ && body.errors?.[0]?.resource_type === 'tweet'
247
+ && body.errors?.[0]?.parameter === 'reply.in_reply_to_tweet_id'
248
+ && body.errors?.[0]?.type === 'https://api.twitter.com/2/problems/resource-not-found';
249
+ })),
250
+ done('x.tweets.reply_to_deleted_post_refuses', 'tweets', 'A reply to a RETRACTED post refuses rather than resurrecting the thread', 'api', 'common', () =>
251
+ withWorld(async (h) => {
252
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: 'temporary' } });
253
+ const id = (created.body as Body).data.id;
254
+ await h({ m: 'DELETE', p: `/2/tweets/${id}` });
255
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'late', reply: { in_reply_to_tweet_id: id } } });
256
+ return r.status === 404;
257
+ })),
258
+ done('x.tweets.reply_and_post_are_one_route', 'tweets', 'Reply and original post are the SAME method+path, told apart only by the body', 'api', 'core', () =>
259
+ withWorld(async (h) => {
260
+ const mentionId = await seedMention(h);
261
+ const post = await h({ m: 'POST', p: '/2/tweets', b: { text: 'a' } });
262
+ const reply = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member b', reply: { in_reply_to_tweet_id: mentionId } } });
263
+ const rows = (await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions?${THREAD_FIELDS}` })).body as Body;
264
+ const stored = (rows.data as Body[]).find((r) => r.id === (reply.body as Body).data.id);
265
+ // Identical requests apart from one field; two different things stored. THIS is the fact
266
+ // the census's body-field discriminator exists to make expressible in a grant.
267
+ return post.status === 201 && reply.status === 201
268
+ && stored?.referenced_tweets?.[0]?.type === 'replied_to'
269
+ && stored?.conversation_id === mentionId;
270
+ })),
271
+ done('x.tweets.reply_null_discriminator_is_an_original_post', 'tweets', 'A null in_reply_to_tweet_id is an original post, not a broken reply', 'api', 'common', () =>
272
+ withWorld(async (h) => {
273
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'plain', reply: { in_reply_to_tweet_id: null } } });
274
+ if (r.status !== 201) return false;
275
+ const id = (r.body as Body).data.id;
276
+ const seeded = await h({ m: 'POST', p: '/_twin/posts', b: { author_id: MEMBER, text: `@orgvoice re ${id}` }, headers: {} });
277
+ const rows = (await h({ m: 'GET', p: `/2/users/${ORG}/mentions` })).body as Body;
278
+ return String((seeded.body as Body).data.id) === (rows.data as Body[])[0]!.id;
279
+ })),
280
+
281
+ // ── the QUOTE class (POST /2/tweets WITH quote_tweet_id) ─────────────────────────────────────
282
+ // A third body-field discriminator on the same one route. A quote CITES rather than answers, so
283
+ // for the census's reply/original split it is an ORIGINAL post — the org speaking under its own
284
+ // name — and it starts its own conversation.
285
+ done('x.tweets.quote', 'tweets', 'Quote post (POST /2/tweets with quote_tweet_id)', 'api', 'core', () =>
286
+ withWorld(async (h) => {
287
+ const quotedId = await seedMention(h, 'the member said something quotable');
288
+ const quote = await h({ m: 'POST', p: '/2/tweets', b: { text: 'worth reading', quote_tweet_id: quotedId } });
289
+ if (quote.status !== 201) return false;
290
+ const id = (quote.body as Body).data.id;
291
+ const row = ((await h({ m: 'GET', p: `/2/tweets/${id}?${THREAD_FIELDS}` })).body as Body).data;
292
+ const missing = await h({ m: 'POST', p: '/2/tweets', b: { text: 'nope', quote_tweet_id: '404404404404404404' } });
293
+ // The quoted post is REFERENCED as `quoted`, not `replied_to`; the quote is its own
294
+ // conversation root and answers nobody. All three follow from the one body field.
295
+ return row.id === id && row.conversation_id === id && row.in_reply_to_user_id === undefined
296
+ && row.referenced_tweets?.length === 1
297
+ && row.referenced_tweets[0].type === 'quoted' && row.referenced_tweets[0].id === quotedId
298
+ && missing.status === 404
299
+ && (missing.body as Body).errors?.[0]?.parameter === 'quote_tweet_id'
300
+ && (missing.body as Body).errors?.[0]?.resource_type === 'tweet';
301
+ })),
302
+
303
+ // ── lookup (GET /2/tweets/:id and GET /2/tweets?ids=) ────────────────────────────────────────
304
+ done('x.tweets.lookup_by_id', 'tweets', 'Post lookup (GET /2/tweets/:id)', 'api', 'core', () =>
305
+ withWorld(async (h) => {
306
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: 'read me back' } });
307
+ const id = (created.body as Body).data.id;
308
+ const found = await h({ m: 'GET', p: `/2/tweets/${id}?tweet.fields=author_id,created_at` });
309
+ const row = (found.body as Body).data;
310
+ // NEVER A FABRICATED RECORD. An id nobody created comes back inside `errors`, by name, with
311
+ // no `data` at all — the difference between a twin and a mock that agrees with any question.
312
+ const ghost = (await h({ m: 'GET', p: '/2/tweets/404404404404404404' })).body as Body;
313
+ const malformed = await h({ m: 'GET', p: '/2/tweets/not-a-snowflake' });
314
+ // …and a RETRACTED post stays gone rather than being resurrected by a later read.
315
+ await h({ m: 'DELETE', p: `/2/tweets/${id}` });
316
+ const afterDelete = (await h({ m: 'GET', p: `/2/tweets/${id}` })).body as Body;
317
+ return found.status === 200 && row.id === id && row.text === 'read me back'
318
+ && row.author_id === ORG && typeof row.created_at === 'string'
319
+ && ghost.data === undefined && ghost.errors?.[0]?.resource_type === 'tweet'
320
+ && ghost.errors[0].value === '404404404404404404' && ghost.errors[0].parameter === 'id'
321
+ && afterDelete.data === undefined && afterDelete.errors?.[0]?.value === id
322
+ && malformed.status === 400 && (malformed.body as Body).errors?.[0]?.parameters?.id?.[0] === 'not-a-snowflake';
323
+ })),
324
+ done('x.tweets.lookup_by_ids', 'tweets', 'Bulk post lookup (GET /2/tweets?ids=)', 'api', 'core', () =>
325
+ withWorld(async (h) => {
326
+ const first = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'one' } })).body as Body).data.id;
327
+ const second = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'two' } })).body as Body).data.id;
328
+ const r = await h({ m: 'GET', p: `/2/tweets?ids=${first},404404404404404404,${second}` });
329
+ const body = r.body as Body;
330
+ const noIds = await h({ m: 'GET', p: '/2/tweets' });
331
+ const rows = body.data as Body[];
332
+ // PARTIAL, the way X answers a bulk lookup: the ids that exist in `data`, in the order the
333
+ // caller asked for them, and the one that does not in `errors` — never three invented rows.
334
+ return r.status === 200
335
+ && rows.map((row) => row.id).join(',') === `${first},${second}`
336
+ && rows.map((row) => row.text).join(',') === 'one,two'
337
+ && body.errors?.length === 1 && body.errors[0].value === '404404404404404404'
338
+ && body.errors[0].parameter === 'ids'
339
+ && noIds.status === 400 && (noIds.body as Body).errors?.[0]?.parameters?.ids !== undefined;
340
+ })),
341
+
342
+ // ── the field projection (tweet.fields / expansions) ─────────────────────────────────────────
343
+ done('x.fields.tweet_fields', 'fields', 'tweet.fields projection on every post-returning read', 'api', 'core', () =>
344
+ withWorld(async (h) => {
345
+ const id = await seedMention(h);
346
+ const bare = ((await h({ m: 'GET', p: `/2/users/${ORG}/mentions` })).body as Body).data[0];
347
+ const asked = ((await h({ m: 'GET', p: `/2/users/${ORG}/mentions?tweet.fields=author_id,created_at` })).body as Body).data[0];
348
+ const lookup = ((await h({ m: 'GET', p: `/2/tweets/${id}?tweet.fields=conversation_id` })).body as Body).data;
349
+ const unknown = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?tweet.fields=not_a_field` });
350
+ const unmodelled = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?tweet.fields=public_metrics` });
351
+ // THE DEFAULT IS X'S THREE FIELDS AND NOTHING ELSE. A twin that returned everything always
352
+ // would pass an integrator whose client forgets tweet.fields and break them in production.
353
+ return Object.keys(bare).sort().join(',') === 'edit_history_tweet_ids,id,text'
354
+ && Object.keys(asked).sort().join(',') === 'author_id,created_at,edit_history_tweet_ids,id,text'
355
+ && asked.author_id === MEMBER && typeof asked.created_at === 'string'
356
+ && Object.keys(lookup).sort().join(',') === 'conversation_id,edit_history_tweet_ids,id,text'
357
+ && lookup.conversation_id === id
358
+ // an UNDOCUMENTED value is the vendor's refusal; a DOCUMENTED value this twin holds no
359
+ // state for is refused too, rather than silently omitted (a caller cannot tell an
360
+ // unmodelled field from an absent one).
361
+ && unknown.status === 400 && (unknown.body as Body).errors?.[0]?.parameters?.['tweet.fields']?.[0] === 'not_a_field'
362
+ && unmodelled.status === 400 && (unmodelled.body as Body).errors?.[0]?.parameters?.['tweet.fields']?.[0] === 'public_metrics';
363
+ })),
364
+ done('x.fields.expansions', 'fields', 'expansions + the `includes` sidecar (author_id, referenced_tweets.id)', 'api', 'core', () =>
365
+ withWorld(async (h) => {
366
+ const mentionId = await seedMention(h);
367
+ const replied = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member on it', reply: { in_reply_to_tweet_id: mentionId } } });
368
+ const replyId = (replied.body as Body).data.id;
369
+ const expanded = (await h({ m: 'GET', p: `/2/tweets/${replyId}?expansions=author_id,referenced_tweets.id` })).body as Body;
370
+ const bare = (await h({ m: 'GET', p: `/2/tweets/${replyId}` })).body as Body;
371
+ const unmodelled = await h({ m: 'GET', p: `/2/tweets/${replyId}?expansions=geo.place_id` });
372
+ const unknown = await h({ m: 'GET', p: `/2/tweets/${replyId}?expansions=not_an_expansion` });
373
+ return expanded.data.author_id === ORG
374
+ && expanded.data.referenced_tweets?.[0]?.id === mentionId
375
+ && expanded.includes.users?.length === 1
376
+ && expanded.includes.users[0].id === ORG && expanded.includes.users[0].username === 'orgvoice'
377
+ && expanded.includes.users[0].name === 'Org Voice'
378
+ && expanded.includes.tweets?.length === 1
379
+ && expanded.includes.tweets[0].id === mentionId
380
+ && expanded.includes.tweets[0].text === 'hey @orgvoice can you help?'
381
+ // NOTHING expanded means NO sidecar and no source field — the expansion is what produces
382
+ // both, so a twin that always shipped `includes` would prove nothing here.
383
+ && bare.includes === undefined && bare.data.author_id === undefined && bare.data.referenced_tweets === undefined
384
+ && unmodelled.status === 400 && unknown.status === 400;
385
+ })),
386
+
387
+ done('x.fields.user_fields', 'fields', 'user.fields projection on user lookup and inside `includes`', 'api', 'common', () =>
388
+ withWorld(async (h) => {
389
+ await h({ m: 'POST', p: '/_twin/accounts', b: { id: '3000', username: 'profiled', name: 'Profiled', description: 'we build things', location: 'Brooklyn', url: 'https://example.org', verified: true }, headers: {} });
390
+ const own = ((await h({ m: 'POST', p: '/_twin/posts', b: { author_id: '3000', text: 'from the profiled account' }, headers: {} })).body as Body).data.id;
391
+ const asked = (await h({ m: 'GET', p: '/2/users/3000?user.fields=description,location,url,verified,protected,created_at,most_recent_tweet_id' })).body as Body;
392
+ const bare = (await h({ m: 'GET', p: '/2/users/3000' })).body as Body;
393
+ // The same parser shapes the users a post read expands.
394
+ const expanded = (await h({ m: 'GET', p: `/2/tweets/${own}?expansions=author_id&user.fields=description` })).body as Body;
395
+ const unmodelled = await h({ m: 'GET', p: '/2/users/3000?user.fields=public_metrics' });
396
+ const unknown = await h({ m: 'GET', p: '/2/users/3000?user.fields=not_a_field' });
397
+ return asked.data.description === 'we build things' && asked.data.location === 'Brooklyn'
398
+ && asked.data.url === 'https://example.org' && asked.data.verified === true && asked.data.protected === false
399
+ && typeof asked.data.created_at === 'string' && asked.data.most_recent_tweet_id === own
400
+ // X's default user object is exactly three fields: nothing rides along unasked.
401
+ && Object.keys(bare.data).sort().join(',') === 'id,name,username'
402
+ && expanded.includes.users[0].description === 'we build things'
403
+ && unmodelled.status === 400 && (unmodelled.body as Body).errors?.[0]?.parameters?.['user.fields']?.[0] === 'public_metrics'
404
+ && unknown.status === 400;
405
+ })),
406
+
407
+ // ── user lookup ──────────────────────────────────────────────────────────────────────────────
408
+ done('x.users.lookup_by_id', 'users', 'User lookup by id (GET /2/users/:id)', 'api', 'core', () =>
409
+ withWorld(async (h) => {
410
+ const found = await h({ m: 'GET', p: `/2/users/${ORG}` });
411
+ const missing = await h({ m: 'GET', p: '/2/users/424242' });
412
+ const malformed = await h({ m: 'GET', p: '/2/users/not-an-id' });
413
+ const missingBody = missing.body as Body;
414
+ return found.status === 200 && (found.body as Body).data.id === ORG
415
+ && (found.body as Body).data.username === 'orgvoice' && (found.body as Body).data.name === 'Org Voice'
416
+ // X's partial-error 200: no `data`, one `errors` row naming the user id.
417
+ && missing.status === 200 && missingBody.data === undefined
418
+ && missingBody.errors?.[0]?.resource_type === 'user' && missingBody.errors[0].value === '424242'
419
+ && malformed.status === 400;
420
+ })),
421
+ done('x.users.lookup_by_username', 'users', 'User lookup by username (GET /2/users/by/username/:username)', 'api', 'core', () =>
422
+ withWorld(async (h) => {
423
+ const exact = (await h({ m: 'GET', p: '/2/users/by/username/orgvoice' })).body as Body;
424
+ // Handles are case-insensitive at X.
425
+ const cased = (await h({ m: 'GET', p: '/2/users/by/username/OrgVoice' })).body as Body;
426
+ const missing = await h({ m: 'GET', p: '/2/users/by/username/nobodyhere' });
427
+ const malformed = await h({ m: 'GET', p: '/2/users/by/username/has-a-dash' });
428
+ const anon = await h({ m: 'GET', p: '/2/users/by/username/orgvoice', headers: {} });
429
+ return exact.data?.id === ORG && cased.data?.id === ORG
430
+ && missing.status === 200 && (missing.body as Body).errors?.[0]?.parameter === 'username'
431
+ && (missing.body as Body).errors[0].resource_type === 'user'
432
+ && malformed.status === 400 && anon.status === 401;
433
+ })),
434
+ done('x.users.lookup_scope_gate', 'users', 'User lookup refuses a token without users.read', 'api', 'common', () =>
435
+ withWorld(async (h) => {
436
+ const refused = await h({ m: 'GET', p: `/2/users/${ORG}` });
437
+ return refused.status === 403;
438
+ }, { scopes: ['tweet.read', 'tweet.write'] })),
439
+
440
+ // ── the timelines ────────────────────────────────────────────────────────────────────────────
441
+ done('x.timelines.user_tweets', 'timelines', 'User posts timeline (GET /2/users/:id/tweets)', 'api', 'core', () =>
442
+ withWorld(async (h) => {
443
+ const first = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'first thing' } })).body as Body).data.id;
444
+ const second = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'second thing' } })).body as Body).data.id;
445
+ await seedMention(h, 'a member post @orgvoice that is NOT ours');
446
+ const rows = ((await h({ m: 'GET', p: `/2/users/${ORG}/tweets?tweet.fields=author_id` })).body as Body).data as Body[];
447
+ // DIRTY STATE ON PURPOSE: retract the newer post and read the timeline again. A fresh-root
448
+ // verify could never see a delete failing to leave the author's own timeline.
449
+ await h({ m: 'DELETE', p: `/2/tweets/${second}` });
450
+ const after = ((await h({ m: 'GET', p: `/2/users/${ORG}/tweets` })).body as Body).data as Body[];
451
+ const theirs = (await h({ m: 'GET', p: `/2/users/${MEMBER}/tweets` })).body as Body;
452
+ const stranger = await h({ m: 'GET', p: '/2/users/999999/tweets' });
453
+ return rows.map((r) => r.id).join(',') === `${second},${first}`
454
+ && rows.every((r) => r.author_id === ORG)
455
+ && after.map((r) => r.id).join(',') === String(first)
456
+ && theirs.meta.result_count === 1
457
+ && stranger.status === 404 && (stranger.body as Body).errors?.[0]?.resource_type === 'user';
458
+ })),
459
+ done('x.timelines.reverse_chronological', 'timelines', 'Home timeline (GET /2/users/:id/timelines/reverse_chronological)', 'api', 'core', () =>
460
+ withWorld(async (h) => {
461
+ const mine = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'our own words' } })).body as Body).data.id;
462
+ const theirs = await seedMention(h, 'a member post that tags nobody');
463
+ const before = (await h({ m: 'GET', p: `/2/users/${ORG}/timelines/reverse_chronological` })).body as Body;
464
+ await h({ m: 'POST', p: '/_twin/follows', b: { follower_id: ORG, followed_id: MEMBER }, headers: {} });
465
+ const after = (await h({ m: 'GET', p: `/2/users/${ORG}/timelines/reverse_chronological?tweet.fields=author_id` })).body as Body;
466
+ const someoneElses = await h({ m: 'GET', p: `/2/users/${MEMBER}/timelines/reverse_chronological` });
467
+ // THE FOLLOW EDGE IS WHAT CHANGES THE ANSWER. Before it the home timeline is the account's
468
+ // own words; after it, it is those plus the account it follows. A twin with no graph could
469
+ // not tell these two reads apart, which is why the graph is real seeded state.
470
+ return before.data.length === 1 && before.data[0].id === mine
471
+ && (after.data as Body[]).map((r) => r.id).join(',') === `${theirs},${mine}`
472
+ && (after.data as Body[]).map((r) => r.author_id).join(',') === `${MEMBER},${ORG}`
473
+ && someoneElses.status === 403 && (someoneElses.body as Body).title === 'Forbidden';
474
+ })),
475
+ done('x.timelines.pagination', 'timelines', 'pagination_token / next_token paging on a timeline', 'api', 'core', () =>
476
+ withWorld(async (h) => {
477
+ const ids: string[] = [];
478
+ for (let i = 0; i < 12; i += 1) ids.push(await seedMention(h, `n${i} @orgvoice`));
479
+ const first = (await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=5` })).body as Body;
480
+ const again = (await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=5` })).body as Body;
481
+ const second = (await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=5&pagination_token=${first.meta.next_token}` })).body as Body;
482
+ const third = (await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=5&pagination_token=${second.meta.next_token}` })).body as Body;
483
+ const bad = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?pagination_token=not-a-token` });
484
+ const walked = [...(first.data as Body[]), ...(second.data as Body[]), ...(third.data as Body[])].map((r) => r.id);
485
+ return walked.join(',') === [...ids].reverse().join(',')
486
+ && new Set(walked).size === 12
487
+ && (third.data as Body[]).length === 2
488
+ // the LAST page carries no token — an unconditional one walks a client into an empty page
489
+ // forever — and the token is a pure function of state, so the same page hands back the same
490
+ // token every time.
491
+ && third.meta.next_token === undefined
492
+ && typeof first.meta.next_token === 'string' && again.meta.next_token === first.meta.next_token
493
+ && bad.status === 400 && (bad.body as Body).errors?.[0]?.parameters?.pagination_token?.[0] === 'not-a-token';
494
+ })),
495
+ done('x.timelines.since_until_id', 'timelines', 'since_id / until_id / start_time / end_time timeline windows', 'api', 'core', () =>
496
+ withWorld(async (h, root) => {
497
+ // PINNED, DISTINCT occurredAt values. `created_at` is what the WRITE stored, so a time
498
+ // window can only be asserted over posts whose times were chosen rather than rolled.
499
+ const at = (day: number) => `2026-08-2${day}T00:00:00.000Z`;
500
+ const seed = async (day: number, text: string) => {
501
+ const r = await handleXTwinRequest({ method: 'POST', path: '/_twin/posts', body: JSON.stringify({ author_id: MEMBER, text }), root, occurredAt: at(day) });
502
+ return String((r.body as Body).data.id);
503
+ };
504
+ const one = await seed(1, 'one @orgvoice');
505
+ const two = await seed(2, 'two @orgvoice');
506
+ const three = await seed(3, 'three @orgvoice');
507
+ const ids = (r: { body: unknown }) => ((r.body as Body).data as Body[] | undefined ?? []).map((row) => row.id).join(',');
508
+ const since = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?since_id=${one}` });
509
+ const until = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?until_id=${three}` });
510
+ const window = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?start_time=${at(2)}&end_time=${at(3)}` });
511
+ // X'S OWN PRECEDENCE: an id bound and a time bound on the same side cannot both apply, and
512
+ // the id wins. since_id=one WITH start_time=day-3 must still answer two rows, not one.
513
+ const precedence = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?since_id=${one}&start_time=${at(3)}` });
514
+ const badId = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?since_id=not-an-id` });
515
+ const badTime = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?start_time=yesterday` });
516
+ return ids(since) === `${three},${two}`
517
+ && ids(until) === `${two},${one}`
518
+ && ids(window) === String(two)
519
+ && ids(precedence) === `${three},${two}`
520
+ && badId.status === 400 && badTime.status === 400
521
+ && (badTime.body as Body).errors?.[0]?.parameters?.start_time?.[0] === 'yesterday';
522
+ })),
523
+
524
+ // ── recent search ────────────────────────────────────────────────────────────────────────────
525
+ done('x.search.recent', 'search', 'Recent search (GET /2/tweets/search/recent)', 'api', 'core', () =>
526
+ withWorld(async (h) => {
527
+ const ours = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'shipping the twin today' } })).body as Body).data.id;
528
+ await seedMention(h, 'the member is shipping something else');
529
+ const term = (await h({ m: 'GET', p: '/2/tweets/search/recent?query=shipping' })).body as Body;
530
+ const scoped = (await h({ m: 'GET', p: '/2/tweets/search/recent?query=shipping from:orgvoice&tweet.fields=author_id' })).body as Body;
531
+ const phrase = (await h({ m: 'GET', p: '/2/tweets/search/recent?query="shipping the twin"' })).body as Body;
532
+ const nothing = (await h({ m: 'GET', p: '/2/tweets/search/recent?query=nobodyeversaidthis' })).body as Body;
533
+ const noQuery = await h({ m: 'GET', p: '/2/tweets/search/recent' });
534
+ // Grammar this twin does not model is REFUSED, never ignored: answering a query the twin
535
+ // did not honour is a fake success wearing a 200 (x.search.query_grammar stays a todo).
536
+ const ungrammatical = await h({ m: 'GET', p: '/2/tweets/search/recent?query=shipping -twin' });
537
+ // Search bounds max_results at 10 where the timelines allow 5 — the same value, two answers.
538
+ const tooSmall = await h({ m: 'GET', p: '/2/tweets/search/recent?query=shipping&max_results=5' });
539
+ const timelineOk = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=5` });
540
+ // X's keyword operator matches TOKENS, not substrings. Substring matching answered `ship`
541
+ // — and `hipping` — with these rows, a confident result for a query the vendor returns
542
+ // nothing for, which is worse than refusing the query.
543
+ const substring = (await h({ m: 'GET', p: '/2/tweets/search/recent?query=hipp' })).body as Body;
544
+ const phraseOrder = (await h({ m: 'GET', p: '/2/tweets/search/recent?query="twin the shipping"' })).body as Body;
545
+ const hashtagLike = (await h({ m: 'GET', p: '/2/tweets/search/recent?query=today' })).body as Body;
546
+ return substring.meta.result_count === 0
547
+ && phraseOrder.meta.result_count === 0
548
+ && hashtagLike.meta.result_count === 1
549
+ && term.meta.result_count === 2
550
+ && (scoped.data as Body[]).length === 1
551
+ && (scoped.data as Body[])[0]!.id === ours && (scoped.data as Body[])[0]!.author_id === ORG
552
+ && (phrase.data as Body[]).length === 1 && (phrase.data as Body[])[0]!.id === ours
553
+ && nothing.data === undefined && nothing.meta.result_count === 0
554
+ && noQuery.status === 400
555
+ && ungrammatical.status === 400 && (ungrammatical.body as Body).errors?.[0]?.parameters?.query?.[0] === '-twin'
556
+ && tooSmall.status === 400 && timelineOk.status === 200;
557
+ })),
558
+
559
+ // ── post length ──────────────────────────────────────────────────────────────────────────────
560
+ done('x.errors.text_length_limit', 'errors', 'Post length limits (280 / long-post tiers) and their refusal', 'api', 'core', () =>
561
+ withWorld(async (h) => {
562
+ const at280 = 'a'.repeat(280);
563
+ const accepted = await h({ m: 'POST', p: '/2/tweets', b: { text: at280 } });
564
+ const refused = await h({ m: 'POST', p: '/2/tweets', b: { text: 'a'.repeat(281) } });
565
+ // THE REFUSAL REFUSED. A 400 that still wrote is exactly the bug a status-only verify
566
+ // cannot see, so the author's own timeline is read back afterwards.
567
+ const stored = ((await h({ m: 'GET', p: `/2/users/${ORG}/tweets` })).body as Body).data as Body[];
568
+ // The long-post tier is an ACCOUNT, not a different endpoint.
569
+ await h({ m: 'POST', p: '/_twin/accounts', b: { id: '3000', username: 'premium', name: 'Premium', post_character_limit: 25000 }, headers: {} });
570
+ await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'premium-token', account_id: '3000', scopes: FULL_SCOPES }, headers: {} });
571
+ const premium = { authorization: 'Bearer premium-token' };
572
+ const long = await h({ m: 'POST', p: '/2/tweets', b: { text: 'b'.repeat(1200) }, headers: premium });
573
+ const beyondPremium = await h({ m: 'POST', p: '/2/tweets', b: { text: 'b'.repeat(25001) }, headers: premium });
574
+ const impossibleTier = await h({ m: 'POST', p: '/_twin/accounts', b: { username: 'nope', post_character_limit: 26000 }, headers: {} });
575
+ return accepted.status === 201 && refused.status === 400
576
+ && (refused.body as Body).errors?.[0]?.parameters?.text?.[0] === '281'
577
+ && stored.length === 1 && stored[0]!.text === at280
578
+ && long.status === 201 && beyondPremium.status === 400 && impossibleTier.status === 400;
579
+ })),
580
+
581
+ // ── retraction (DELETE /2/tweets/:id) ────────────────────────────────────────────────────────
582
+ done('x.tweets.delete', 'tweets', 'Retract a post (DELETE /2/tweets/:id)', 'api', 'core', () =>
583
+ withWorld(async (h) => {
584
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member oops' } });
585
+ const id = (created.body as Body).data.id;
586
+ const before = (await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions` })).body as Body;
587
+ const deleted = await h({ m: 'DELETE', p: `/2/tweets/${id}` });
588
+ const after = (await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions` })).body as Body;
589
+ // Deleted AND gone from the timeline — a delete that answers 200 while the post keeps
590
+ // showing is exactly the bug a status-only verify would miss.
591
+ return before.meta.result_count === 1
592
+ && deleted.status === 200 && (deleted.body as Body).data.deleted === true
593
+ && after.meta.result_count === 0;
594
+ })),
595
+ done('x.tweets.delete_unknown_refuses', 'tweets', 'Deleting a post that does not exist is resource-not-found', 'api', 'core', () =>
596
+ withWorld(async (h) => {
597
+ const r = await h({ m: 'DELETE', p: '/2/tweets/404404404404404404' });
598
+ return r.status === 404 && (r.body as Body).errors?.[0]?.resource_type === 'tweet';
599
+ })),
600
+ done('x.tweets.delete_twice_refuses', 'tweets', 'A second delete of the same post refuses (not a silent success)', 'api', 'common', () =>
601
+ withWorld(async (h) => {
602
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: 'once' } });
603
+ const id = (created.body as Body).data.id;
604
+ const first = await h({ m: 'DELETE', p: `/2/tweets/${id}` });
605
+ const second = await h({ m: 'DELETE', p: `/2/tweets/${id}` });
606
+ return first.status === 200 && second.status === 404;
607
+ })),
608
+ done('x.tweets.delete_only_your_own', 'tweets', "A public voice cannot retract someone ELSE's post", 'api', 'core', () =>
609
+ withWorld(async (h) => {
610
+ const foreignId = await seedMention(h);
611
+ const r = await h({ m: 'DELETE', p: `/2/tweets/${foreignId}` });
612
+ const still = (await h({ m: 'GET', p: `/2/users/${ORG}/mentions` })).body as Body;
613
+ return r.status === 403 && (r.body as Body).title === 'Forbidden' && still.meta.result_count === 1;
614
+ })),
615
+
616
+ // ── the mentions timeline (GET /2/users/:id/mentions) ────────────────────────────────────────
617
+ done('x.mentions.list', 'mentions', 'Read the mentions timeline (GET /2/users/:id/mentions)', 'api', 'core', () =>
618
+ withWorld(async (h) => {
619
+ const mentionId = await seedMention(h);
620
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?${THREAD_FIELDS}` });
621
+ const body = r.body as Body;
622
+ return r.status === 200
623
+ && body.data.length === 1 && body.data[0].id === mentionId
624
+ && body.data[0].author_id === MEMBER
625
+ && body.meta.result_count === 1 && body.meta.newest_id === mentionId && body.meta.oldest_id === mentionId;
626
+ })),
627
+ done('x.mentions.empty_omits_data', 'mentions', "An EMPTY timeline omits `data` entirely, as the vendor does", 'api', 'core', () =>
628
+ withWorld(async (h) => {
629
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions` });
630
+ const body = r.body as Body;
631
+ // A client reading `response.data.length` breaks against the real vendor if the twin
632
+ // invents `[]` here. Faithfulness is the absence.
633
+ return r.status === 200 && body.data === undefined && body.meta.result_count === 0 && Object.keys(body).join(',') === 'meta';
634
+ })),
635
+ done('x.mentions.newest_first', 'mentions', 'The timeline is ordered newest-first by post id', 'api', 'core', () =>
636
+ withWorld(async (h) => {
637
+ const first = await seedMention(h, 'first @orgvoice');
638
+ const second = await seedMention(h, 'second @orgvoice');
639
+ const third = await seedMention(h, 'third @orgvoice');
640
+ const rows = ((await h({ m: 'GET', p: `/2/users/${ORG}/mentions` })).body as Body).data as Body[];
641
+ return rows.map((r) => r.id).join(',') === [third, second, first].join(',');
642
+ })),
643
+ done('x.mentions.excludes_own_posts', 'mentions', "The org's own posts are not its own mentions", 'api', 'core', () =>
644
+ withWorld(async (h) => {
645
+ await h({ m: 'POST', p: '/2/tweets', b: { text: 'talking about @orgvoice, i.e. me' } });
646
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions` });
647
+ return (r.body as Body).meta.result_count === 0;
648
+ })),
649
+ done('x.mentions.handle_boundaries', 'mentions', '@handle matching respects word boundaries and is case-insensitive', 'api', 'common', () =>
650
+ withWorld(async (h) => {
651
+ await h({ m: 'POST', p: '/_twin/posts', b: { author_id: MEMBER, text: 'ping @OrgVoice!' }, headers: {} });
652
+ await h({ m: 'POST', p: '/_twin/posts', b: { author_id: MEMBER, text: 'not @orgvoicebot though' }, headers: {} });
653
+ await h({ m: 'POST', p: '/_twin/posts', b: { author_id: MEMBER, text: 'nor foo@orgvoice' }, headers: {} });
654
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions` });
655
+ return (r.body as Body).meta.result_count === 1;
656
+ })),
657
+ done('x.mentions.max_results_bounds', 'mentions', 'max_results is bounded 5..100, out of range is the invalid-request problem', 'api', 'common', () =>
658
+ withWorld(async (h) => {
659
+ await seedMention(h);
660
+ const low = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=1` });
661
+ const high = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=500` });
662
+ const okRange = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=5` });
663
+ return low.status === 400 && high.status === 400 && okRange.status === 200
664
+ && (low.body as Body).errors?.[0]?.parameters?.max_results?.[0] === '1';
665
+ })),
666
+ done('x.mentions.max_results_pages', 'mentions', 'max_results actually bounds the page', 'api', 'common', () =>
667
+ withWorld(async (h) => {
668
+ for (let i = 0; i < 7; i += 1) await seedMention(h, `n${i} @orgvoice`);
669
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions?max_results=5` });
670
+ const body = r.body as Body;
671
+ return body.data.length === 5 && body.meta.result_count === 5;
672
+ })),
673
+ done('x.mentions.unknown_user_refuses', 'mentions', 'A mentions read for an account that does not exist is resource-not-found', 'api', 'common', () =>
674
+ withWorld(async (h) => {
675
+ const r = await h({ m: 'GET', p: '/2/users/999999/mentions' });
676
+ return r.status === 404 && (r.body as Body).errors?.[0]?.resource_type === 'user';
677
+ })),
678
+ done('x.mentions.excludes_retracted', 'mentions', 'A retracted post leaves the mentions timeline', 'api', 'common', () =>
679
+ withWorld(async (h) => {
680
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member hi' } });
681
+ await h({ m: 'DELETE', p: `/2/tweets/${(created.body as Body).data.id}` });
682
+ const r = await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions` });
683
+ return (r.body as Body).meta.result_count === 0;
684
+ })),
685
+
686
+ // ── auth: the bearer xidentity's flow mints, and the scopes it carries ────────────────────────
687
+ done('x.auth.bearer_required', 'auth', 'An absent bearer is the about:blank 401 problem', 'api', 'core', () =>
688
+ withWorld(async (h) => {
689
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions`, headers: {} });
690
+ const body = r.body as Body;
691
+ return r.status === 401 && body.type === 'about:blank' && body.title === 'Unauthorized' && body.status === 401;
692
+ })),
693
+ done('x.auth.unknown_bearer_refuses', 'auth', 'An unregistered bearer is refused, not silently accepted', 'api', 'core', () =>
694
+ withWorld(async (h) => {
695
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'sneaky' }, headers: { authorization: 'Bearer not-a-real-token' } });
696
+ return r.status === 401;
697
+ })),
698
+ done('x.auth.malformed_authorization_refuses', 'auth', 'A non-Bearer Authorization header is refused', 'api', 'common', () =>
699
+ withWorld(async (h) => {
700
+ const basic = await h({ m: 'POST', p: '/2/tweets', b: { text: 'x' }, headers: { authorization: `Basic ${TOKEN}` } });
701
+ const bare = await h({ m: 'POST', p: '/2/tweets', b: { text: 'x' }, headers: { authorization: TOKEN } });
702
+ return basic.status === 401 && bare.status === 401;
703
+ })),
704
+ done('x.auth.write_scope_gate', 'auth', 'Posting without tweet.write is the 403 problem', 'api', 'core', () =>
705
+ withWorld(async (h) => {
706
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'unauthorized voice' } });
707
+ const body = r.body as Body;
708
+ return r.status === 403 && body.title === 'Forbidden' && String(body.detail).includes('tweet.write');
709
+ }, { scopes: ['tweet.read', 'users.read'] })),
710
+ done('x.auth.read_scope_gate', 'auth', 'Reading mentions without tweet.read is the 403 problem', 'api', 'core', () =>
711
+ withWorld(async (h) => {
712
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions` });
713
+ return r.status === 403 && String((r.body as Body).detail).includes('tweet.read');
714
+ }, { scopes: ['tweet.write', 'users.read'] })),
715
+ done('x.auth.reply_scope_equals_post_scope', 'auth', 'A reply requires the same vendor scope an original post does', 'api', 'core', () =>
716
+ withWorld(async (h) => {
717
+ // The VENDOR does not distinguish them — one operation, one scope. The estate's ladder
718
+ // lives in the census/grant layer, not in a scope X does not publish. Recording that here
719
+ // keeps a future reader from inventing a `tweet.reply` scope X has never had.
720
+ const mention = await h({ m: 'POST', p: '/_twin/posts', b: { author_id: MEMBER, text: '@orgvoice hi' }, headers: {} });
721
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'hello', reply: { in_reply_to_tweet_id: (mention.body as Body).data.id } } });
722
+ return r.status === 403 && String((r.body as Body).detail).includes('tweet.write');
723
+ }, { scopes: ['tweet.read', 'users.read'] })),
724
+
725
+ // ── error surface ────────────────────────────────────────────────────────────────────────────
726
+ done('x.errors.unmodelled_route_404s', 'errors', 'An unmodelled /2 route fails like the vendor, never a fake success', 'api', 'core', () =>
727
+ withWorld(async (h) => {
728
+ // Recent search IS modelled now, so the unmodelled-route claim moved to routes that are
729
+ // genuinely still absent: post COUNTS and the engagement verbs.
730
+ const counts = await h({ m: 'GET', p: '/2/tweets/counts/recent?query=volter' });
731
+ const like = await h({ m: 'POST', p: `/2/users/${ORG}/likes`, b: { tweet_id: '1' } });
732
+ const hidden = await h({ m: 'PUT', p: '/2/tweets/1900000000000000001/hidden', b: { hidden: true } });
733
+ return counts.status === 404 && (counts.body as Body).title === 'Not Found Error'
734
+ && like.status === 404 && hidden.status === 404;
735
+ })),
736
+ done('x.errors.problem_content_type', 'errors', 'Problem envelopes carry application/problem+json', 'api', 'common', () =>
737
+ withWorld(async (h) => {
738
+ const r = await h({ m: 'GET', p: `/2/users/${ORG}/mentions`, headers: {} });
739
+ return r.headers?.['content-type'] === 'application/problem+json';
740
+ })),
741
+ done('x.errors.malformed_body_refuses', 'errors', 'An unparseable request body is the invalid-request problem', 'api', 'common', () =>
742
+ withWorld(async (h, root) => {
743
+ const r = await handleXTwinRequest({ method: 'POST', path: '/2/tweets', body: '{not json', headers: AUTH, root });
744
+ return r.status === 400 && (r.body as Body).type === 'https://api.x.com/2/problems/invalid-request';
745
+ })),
746
+ done('x.errors.rate_limit_429_code_88', 'errors', 'The armed 15-minute-window refusal is 429 with legacy code 88', 'api', 'common', () =>
747
+ withWorld(async (h) => {
748
+ await h({ m: 'POST', p: '/_twin/rate_limit', b: { armed: true, limit: 200 }, headers: {} });
749
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'blocked' } });
750
+ const body = r.body as Body;
751
+ return r.status === 429
752
+ && body.errors?.[0]?.code === 88 && body.errors?.[0]?.message === 'Rate limit exceeded'
753
+ && r.headers?.['x-rate-limit-remaining'] === '0'
754
+ && typeof r.headers?.['x-rate-limit-reset'] === 'string';
755
+ })),
756
+ done('x.errors.read_only_refuses_writes', 'errors', 'A read-only twin refuses every write with 405', 'api', 'core', () =>
757
+ withWorld(async (h) => {
758
+ const post = await h({ m: 'POST', p: '/2/tweets', b: { text: 'nope' }, readOnly: true });
759
+ const del = await h({ m: 'DELETE', p: '/2/tweets/1', readOnly: true });
760
+ const read = await h({ m: 'GET', p: `/2/users/${ORG}/mentions`, readOnly: true });
761
+ return post.status === 405 && del.status === 405 && read.status === 200;
762
+ })),
763
+
764
+ // ── connector (pull/push over an INJECTED client) ────────────────────────────────────────────
765
+ done('x.connector.pull_mentions', 'connector', 'Pull the mentions timeline from the real vendor into observed state', 'connector', 'core', () =>
766
+ withWorld(async (h, root) => {
767
+ // A PATH-AWARE fake, because the pull now reads two timelines: what was said to the org
768
+ // and what the org itself said. A fake that answered every path identically would fold the
769
+ // same rows twice and prove nothing about either read.
770
+ const execute: XExecute = async (_m, path) => (path.includes('/mentions')
771
+ ? { data: [{ id: '1900000000000000001', text: 'hey @orgvoice', author_id: MEMBER, created_at: '2026-08-24T00:00:00.000Z', conversation_id: '1900000000000000001' }], meta: { result_count: 1 } }
772
+ : { meta: { result_count: 0 } });
773
+ const result = await syncXFromReal(execute, { userId: ORG, root });
774
+ const rows = ((await h({ m: 'GET', p: `/2/users/${ORG}/mentions?${THREAD_FIELDS}` })).body as Body).data as Body[];
775
+ return result.observed === 1 && result.deltasAppended > 0 && rows.length === 1
776
+ && rows[0]!.id === '1900000000000000001' && rows[0]!.author_id === MEMBER;
777
+ })),
778
+ done('x.connector.pull_idempotent', 'connector', 'A second identical pull appends zero deltas', 'connector', 'core', () =>
779
+ withWorld(async (_h, root) => {
780
+ const execute: XExecute = async (_m, path) => (path.includes('/mentions')
781
+ ? { data: [{ id: '1900000000000000002', text: 'hey @orgvoice', author_id: MEMBER, created_at: '2026-08-24T00:00:00.000Z' }], meta: { result_count: 1 } }
782
+ : { meta: { result_count: 0 } });
783
+ await syncXFromReal(execute, { userId: ORG, root });
784
+ const second = await syncXFromReal(execute, { userId: ORG, root });
785
+ return second.observed === 1 && second.deltasAppended === 0;
786
+ })),
787
+ done('x.connector.empty_timeline_is_not_a_refusal', 'connector', "An empty timeline (meta only, no data) pulls as zero rows, not an error", 'connector', 'core', () =>
788
+ withWorld(async (_h, root) => {
789
+ const execute: XExecute = async () => ({ meta: { result_count: 0 } });
790
+ const result = await syncXFromReal(execute, { userId: ORG, root });
791
+ return result.observed === 0 && result.deltasAppended === 0;
792
+ })),
793
+ // COUNTING THROWS IS NOT A TEST. The first version of this verify counted exceptions, and a
794
+ // DEAD connector seam throws too — so it was green against the saboteur that exists to catch
795
+ // exactly this (scripts/mutation-test.ts, `connectorDead`). A refusal has to be identified by
796
+ // the REFUSAL: the vendor's own reply has to reach the caller inside the error, real observed
797
+ // state has to survive it untouched, and a legitimate pull in the same run has to still fold.
798
+ // All three break under a dead seam; none of them breaks because "an exception happened".
799
+ done('x.connector.refused_pull_throws', 'connector', 'A refusal-shaped reply THROWS naming the refusal, and folds NOTHING over observed state', 'connector', 'core', () =>
800
+ withWorld(async (h, root) => {
801
+ // A LEGITIMATE pull FIRST, so there is real observed state for a refusal to damage — and so
802
+ // this verify can never be satisfied by everything throwing.
803
+ const good: XExecute = async (_m, path) => (path.includes('/mentions')
804
+ ? { data: [{ id: '1900000000000000001', text: 'hey @orgvoice', author_id: MEMBER, created_at: '2026-08-24T00:00:00.000Z', conversation_id: '1900000000000000001' }], meta: { result_count: 1 } }
805
+ : { meta: { result_count: 0 } });
806
+ const folded = await syncXFromReal(good, { userId: ORG, root });
807
+ if (folded.observed !== 1 || folded.deltasAppended === 0) return false;
808
+
809
+ // Each case asserts a fragment of the VENDOR'S OWN REPLY in the message the caller sees.
810
+ // The saboteur's message ('connector seam is dead') carries none of them.
811
+ const cases: Array<{ execute: XExecute; names: string }> = [
812
+ // a 200 carrying a problem envelope: neither `data` nor `meta`
813
+ { execute: async () => ({ title: 'Unauthorized', type: 'about:blank', status: 401 }), names: 'about:blank' },
814
+ // a row whose id is not an X-shaped snowflake — a foreign id entering the projection
815
+ { execute: async () => ({ data: [{ id: 'not-a-snowflake', text: 'x' }], meta: { result_count: 1 } }), names: 'not-a-snowflake' },
816
+ // `data` present but not an array
817
+ { execute: async () => ({ data: { id: '1900000000000000009' }, meta: { result_count: 1 } }), names: '1900000000000000009' },
818
+ ];
819
+ for (const { execute, names } of cases) {
820
+ let message: string | undefined;
821
+ try {
822
+ await syncXFromReal(execute, { userId: ORG, root });
823
+ } catch (error) {
824
+ message = error instanceof Error ? error.message : String(error);
825
+ }
826
+ if (message === undefined) return false;
827
+ if (!/^x mentions pull (refused or malformed|returned a row)/.test(message)) return false;
828
+ if (!message.includes(names)) return false;
829
+ }
830
+
831
+ // And the refusals folded NOTHING: the legitimately observed post is still the only row,
832
+ // unchanged. A refusal that quietly emptied the timeline would pass every assertion above.
833
+ const rows = ((await h({ m: 'GET', p: `/2/users/${ORG}/mentions` })).body as Body).data as Body[];
834
+ return rows.length === 1 && rows[0]!.id === '1900000000000000001' && rows[0]!.text === 'hey @orgvoice';
835
+ })),
836
+ done('x.connector.push_request_shape', 'connector', 'The push request shape carries the reply discriminator unchanged', 'connector', 'core', () => {
837
+ const post = xRequestForAction({ operation: 'x.post.create', subject: { type: 'post', id: '9000000000000000001' }, fields: { text: 'hello' } });
838
+ const reply = xRequestForAction({ operation: 'x.post.reply', subject: { type: 'post', id: '9000000000000000002' }, fields: { text: 'hi', referenced_tweets: [{ type: 'replied_to', id: '1900000000000000001' }] } });
839
+ const quote = xRequestForAction({ operation: 'x.post.quote', subject: { type: 'post', id: '9000000000000000003' }, fields: { text: 'worth reading', referenced_tweets: [{ type: 'quoted', id: '1900000000000000002' }] } });
840
+ const del = xRequestForAction({ operation: 'x.post.delete', subject: { type: 'post', id: '1900000000000000009' }, fields: {} });
841
+ return post?.method === 'POST' && post.path === '/2/tweets' && JSON.parse(post.body!).reply === undefined
842
+ && reply?.method === 'POST' && reply.path === '/2/tweets' && JSON.parse(reply.body!).reply.in_reply_to_tweet_id === '1900000000000000001'
843
+ && quote?.method === 'POST' && quote.path === '/2/tweets'
844
+ && JSON.parse(quote.body!).quote_tweet_id === '1900000000000000002' && JSON.parse(quote.body!).reply === undefined
845
+ && del?.method === 'DELETE' && del.path === '/2/tweets/1900000000000000009';
846
+ }),
847
+ done('x.connector.push_confirms_the_real_id', 'connector', "A pushed post is confirmed under the id the VENDOR minted, retiring the local one", 'connector', 'core', () =>
848
+ withWorld(async (h, root) => {
849
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: '@member pushed' } });
850
+ const localId = (created.body as Body).data.id;
851
+ const { pendingActions } = await import('@volter/world-core');
852
+ const action = pendingActions('x', root).find((a) => a.operation === 'x.post.create');
853
+ if (!action) return false;
854
+ const execute: XExecute = async () => ({ data: { id: '1911111111111111111', text: '@member pushed' } });
855
+ const result = await pushXAction(execute, action as never, { root });
856
+ const rows = ((await h({ m: 'GET', p: `/2/users/${MEMBER}/mentions` })).body as Body).data as Body[];
857
+ // The public id is live and the locally minted one is retired IN THE SAME confirmation, so
858
+ // no window exists in which a later delete could address a post that does not exist.
859
+ return result.realId === '1911111111111111111'
860
+ && rows.length === 1 && rows[0]!.id === '1911111111111111111'
861
+ && !rows.some((r) => r.id === localId);
862
+ })),
863
+ done('x.connector.pull_own_timeline', 'connector', "Pull the org's OWN posted timeline from the real vendor", 'connector', 'core', () =>
864
+ withWorld(async (h, root) => {
865
+ // A post the org made SOMEWHERE ELSE — on x.com, or in a run this twin never saw. Nothing
866
+ // local knows it exists, so a pull that reads only mentions comes back still believing the
867
+ // org never spoke.
868
+ const execute: XExecute = async (_m, path) => (path.includes('/mentions')
869
+ ? { meta: { result_count: 0 } }
870
+ : { data: [{ id: '1900000000000000077', text: 'said this from a phone', author_id: ORG, created_at: '2026-08-24T00:00:00.000Z', conversation_id: '1900000000000000077' }], meta: { result_count: 1 } });
871
+ const before = (await h({ m: 'GET', p: `/2/users/${ORG}/tweets` })).body as Body;
872
+ const result = await syncXFromReal(execute, { userId: ORG, root });
873
+ const rows = ((await h({ m: 'GET', p: `/2/users/${ORG}/tweets?tweet.fields=author_id,created_at` })).body as Body).data as Body[];
874
+ // …and the read really is the OWN-timeline leg: opting out of it observes nothing at all.
875
+ const optedOut = await syncXFromReal(execute, { userId: ORG, root, includeOwnTimeline: false });
876
+ return before.meta.result_count === 0
877
+ && result.observed === 1 && result.deltasAppended > 0
878
+ && rows.length === 1 && rows[0]!.id === '1900000000000000077'
879
+ && rows[0]!.text === 'said this from a phone' && rows[0]!.author_id === ORG
880
+ && rows[0]!.created_at === '2026-08-24T00:00:00.000Z'
881
+ && optedOut.observed === 0;
882
+ })),
883
+ done('x.connector.pull_pagination', 'connector', 'Follow next_token across a multi-page mentions pull', 'connector', 'core', () =>
884
+ withWorld(async (h, root) => {
885
+ const asked: string[] = [];
886
+ const page = (ids: string[], author: string, next?: string) => ({
887
+ data: ids.map((id) => ({ id, text: author === MEMBER ? `hey @orgvoice ${id}` : `the org said ${id}`, author_id: author, created_at: '2026-08-24T00:00:00.000Z' })),
888
+ meta: { result_count: ids.length, ...(next === undefined ? {} : { next_token: next }) },
889
+ });
890
+ // BOTH legs page. The own-timeline leg answering a single empty page left half the claim
891
+ // ("both pull legs follow next_token") resting on the mentions leg alone — and the two legs
892
+ // carry DIFFERENT authors, so the mentions read below can only see the mentions leg's rows.
893
+ const execute: XExecute = async (_m, path) => {
894
+ asked.push(path);
895
+ if (path.includes('/mentions')) {
896
+ return path.includes('pagination_token=PAGE2') ? page(['1900000000000000012'], MEMBER) : page(['1900000000000000011'], MEMBER, 'PAGE2');
897
+ }
898
+ return path.includes('pagination_token=OWN2') ? page(['1900000000000000022'], ORG) : page(['1900000000000000021'], ORG, 'OWN2');
899
+ };
900
+ const result = await syncXFromReal(execute, { userId: ORG, root });
901
+ const rows = ((await h({ m: 'GET', p: `/2/users/${ORG}/mentions` })).body as Body).data as Body[];
902
+ const own = ((await h({ m: 'GET', p: `/2/users/${ORG}/tweets` })).body as Body).data as Body[];
903
+ // A vendor that keeps handing back the SAME token is refused by name rather than paged
904
+ // forever — and, just as important, rather than truncated in silence.
905
+ let looped: string | undefined;
906
+ try {
907
+ await syncXFromReal(async () => page(['1900000000000000013'], MEMBER, 'SAME'), { userId: ORG, root });
908
+ } catch (error) {
909
+ looped = error instanceof Error ? error.message : String(error);
910
+ }
911
+ // …and a vendor that keeps handing back DISTINCT tokens forever hits the page cap, which
912
+ // the repeated-token guard would otherwise always reach first.
913
+ let capped: string | undefined;
914
+ try {
915
+ let issued = 0;
916
+ await syncXFromReal(async () => page([`19000000000000001${(issued += 1) % 10}`], MEMBER, `T${issued}`), { userId: ORG, root });
917
+ } catch (error) {
918
+ capped = error instanceof Error ? error.message : String(error);
919
+ }
920
+ return result.observed === 4
921
+ && own.map((r) => r.id).sort().join(',') === '1900000000000000021,1900000000000000022'
922
+ && asked.filter((path) => path.includes('/mentions')).length === 2
923
+ && asked.filter((path) => path.includes('/tweets') && !path.includes('/mentions')).length === 2
924
+ && asked.some((path) => path.includes('pagination_token=PAGE2'))
925
+ && asked.some((path) => path.includes('pagination_token=OWN2'))
926
+ && rows.map((r) => r.id).sort().join(',') === '1900000000000000011,1900000000000000012'
927
+ && looped !== undefined && /repeated next_token \(SAME\)/.test(looped)
928
+ && capped !== undefined && /exceeded 32 pages/.test(capped);
929
+ })),
930
+ done('x.connector.live_refuses_unmapped_paths', 'connector', 'The live executor refuses any X path this pack has not modelled', 'connector', 'core', async () => {
931
+ let calls = 0;
932
+ const execute = (await import('./x-connector.ts')).liveXExecute('token', { fetchImpl: (async () => { calls += 1; return new Response('{}'); }) as unknown as typeof fetch, budgetOptions: { path: join(mkdtempSync(join(tmpdir(), 'x-budget-')), 'ledger.json') } });
933
+ // Same rule as x.connector.refused_pull_throws: the refusal is identified by NAMING the path
934
+ // it refused, not by an exception having occurred.
935
+ for (const path of ['/2/users/1/likes', '/1.1/statuses/update.json', '/2/dm_conversations']) {
936
+ let message: string | undefined;
937
+ try {
938
+ await execute('POST', path, { body: '{}' });
939
+ } catch (error) {
940
+ message = error instanceof Error ? error.message : String(error);
941
+ }
942
+ if (message === undefined) return false;
943
+ if (!message.startsWith('liveXExecute: refusing to call an unmapped X path:')) return false;
944
+ if (!message.endsWith(path)) return false;
945
+ }
946
+ // And it refused BEFORE any I/O: the injected fake was never called.
947
+ return calls === 0;
948
+ }),
949
+ done('x.connector.live_refuses_injected_authorization', 'connector', 'The live executor refuses a caller-supplied Authorization header', 'connector', 'core', async () => {
950
+ let calls = 0;
951
+ const execute = (await import('./x-connector.ts')).liveXExecute('token', { fetchImpl: (async () => { calls += 1; return new Response('{}'); }) as unknown as typeof fetch, budgetOptions: { path: join(mkdtempSync(join(tmpdir(), 'x-budget-')), 'ledger.json') } });
952
+ // /2/tweets IS a mapped path, so an exception here can only come from the header refusal —
953
+ // and it must say so, by name, before any I/O.
954
+ let message: string | undefined;
955
+ try {
956
+ await execute('POST', '/2/tweets', { headers: { Authorization: 'Bearer someone-elses' }, body: '{}' });
957
+ } catch (error) {
958
+ message = error instanceof Error ? error.message : String(error);
959
+ }
960
+ return message === 'liveXExecute: refusing an injected Authorization header; the guarded credential is fixed at construction' && calls === 0;
961
+ }),
962
+
963
+ // ── the rate budget (the fail-closed backstop on the live path) ──────────────────────────────
964
+ // A TRAP THIS PACK WALKED INTO ONCE, WRITTEN DOWN SO THE NEXT READER DOES NOT. `write` and
965
+ // `other` are BOTH 5 — deliberately, so unmodelled surface is priced at the write. That makes
966
+ // `xCallWeight('POST', '/2/tweets') === X_CALL_WEIGHTS.write` a TAUTOLOGY: delete the
967
+ // `^POST /2/tweets$` rule and the default answers 5 and the assertion still passes. The
968
+ // mutation gate cannot see it either, because it sabotages module seams, not a data table.
969
+ // So every assertion below is one the DEFAULT would answer differently.
970
+ done('x.budget.prices_a_write_above_a_read', 'rate_limit', 'A post costs more budget than reading the mentions timeline', 'connector', 'core', () => {
971
+ const read = xCallWeight('GET', '/2/users/1000/mentions?tweet.fields=author_id');
972
+ const unruled = xCallWeight('GET', '/2/tweets/1900000000000000001');
973
+ // The READ rule is the only weight distinguishable from the fallback, so it carries the
974
+ // behavioural half: a path the rules do not name falls to `other`, and the mentions read
975
+ // must NOT, or the rule never fired.
976
+ // (No `read === unruled` guard: the compiler narrows both to their literal weights after the
977
+ // checks above and rejects the comparison as provably false — which is the property itself.)
978
+ if (read !== X_CALL_WEIGHTS.read || unruled !== X_CALL_WEIGHTS.other) return false;
979
+ // The WRITE rules cannot be told from the fallback by their weight, so they are asserted
980
+ // where they actually live: the committed declaration the kernel registry is armed from.
981
+ const matchers = X_RATE_BUDGET.rules?.map((rule) => `${rule.match} ${rule.weight}`) ?? [];
982
+ return matchers.includes(`^POST /2/tweets$ ${X_CALL_WEIGHTS.write}`)
983
+ && matchers.includes(`^DELETE /2/tweets/ ${X_CALL_WEIGHTS.write}`)
984
+ && matchers.includes(`^GET /2/users/[^/]+/mentions$ ${X_CALL_WEIGHTS.read}`)
985
+ && X_CALL_WEIGHTS.write > X_CALL_WEIGHTS.read;
986
+ }),
987
+ done('x.budget.case_insensitive_method', 'rate_limit', "A lower-cased method and a trailing slash are priced as the call really is", 'connector', 'core', () => {
988
+ // Priced on the READ path for the reason above: an un-normalized key misses the rule and
989
+ // falls to `other` (5), which is distinguishable from `read` (1). On a write path it would
990
+ // fall to 5 and look identical to a hit, proving nothing.
991
+ return xCallWeight('get', '/2/users/1000/mentions') === X_CALL_WEIGHTS.read
992
+ && xCallWeight('GET', '/2/users/1000/mentions/') === X_CALL_WEIGHTS.read
993
+ && xCallWeight('get', '/2/users/1000/mentions/?tweet.fields=author_id') === X_CALL_WEIGHTS.read;
994
+ }),
995
+ done('x.budget.enforced_fail_closed', 'rate_limit', 'The declared ceiling stops the live path BEFORE the request goes out', 'connector', 'core', async () => {
996
+ let calls = 0;
997
+ const ledger = join(mkdtempSync(join(tmpdir(), 'x-budget-')), 'ledger.json');
998
+ const execute = (await import('./x-connector.ts')).liveXExecute('token', {
999
+ fetchImpl: (async () => { calls += 1; return new Response(JSON.stringify({ data: { id: '1900000000000000001', text: 'x' } }), { headers: { 'content-type': 'application/json' } }); }) as unknown as typeof fetch,
1000
+ budgetOptions: { path: ledger },
1001
+ });
1002
+ // Ten writes at weight 5 exhaust the 50-unit window exactly; the eleventh must never reach
1003
+ // fetch. Nothing here waits on a wall clock: the guard refuses synchronously.
1004
+ let refusedAt = 0;
1005
+ for (let i = 0; i < 20; i += 1) {
1006
+ try { await execute('POST', '/2/tweets', { body: '{"text":"x"}' }); } catch (error) { if (error instanceof XBudgetError) { refusedAt = i; break; } throw error; }
1007
+ }
1008
+ return refusedAt > 0 && calls === refusedAt && calls * X_CALL_WEIGHTS.write <= X_BUDGET_CEILING;
1009
+ }),
1010
+ done('x.budget.declared_ceiling_is_the_pack_descriptor', 'rate_limit', "The pack descriptor arms the SAME declaration the connector imports", 'connector', 'common', async () => {
1011
+ const { pack } = await import('./index.ts');
1012
+ const { X_RATE_BUDGET } = await import('./x-budget.ts');
1013
+ return pack.rateBudget === X_RATE_BUDGET && new XBudget({ path: join(mkdtempSync(join(tmpdir(), 'x-budget-')), 'l.json') }).path.endsWith('l.json');
1014
+ }),
1015
+
1016
+ // ── the twin control plane (how a WORLD seeds a rehearsal) ───────────────────────────────────
1017
+ done('x.twin_control.seed_account_and_token', 'twin_control', 'A world seeds an account and the bearer xidentity would have minted', 'api', 'core', () =>
1018
+ withWorld(async (h) => {
1019
+ const r = await h({ m: 'POST', p: '/2/tweets', b: { text: 'seeded and speaking' } });
1020
+ return r.status === 201;
1021
+ })),
1022
+ done('x.twin_control.rejects_unshaped_username', 'twin_control', 'A seeded account must carry an X-shaped username', 'api', 'common', () =>
1023
+ withWorld(async (h) => {
1024
+ const tooLong = await h({ m: 'POST', p: '/_twin/accounts', b: { username: 'averyveryverylongusername' }, headers: {} });
1025
+ const punctuation = await h({ m: 'POST', p: '/_twin/accounts', b: { username: 'has.a.dot' }, headers: {} });
1026
+ return tooLong.status === 400 && punctuation.status === 400;
1027
+ })),
1028
+ done('x.twin_control.token_must_name_a_real_account', 'twin_control', 'A seeded token cannot name an account that does not exist', 'api', 'common', () =>
1029
+ withWorld(async (h) => {
1030
+ const r = await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'orphan', account_id: '777777' }, headers: {} });
1031
+ return r.status === 400;
1032
+ })),
1033
+ done('x.twin_control.is_not_vendor_surface', 'twin_control', 'The control plane is refused in read-only mode and is not under /2', 'api', 'common', () =>
1034
+ withWorld(async (h) => {
1035
+ const readOnly = await h({ m: 'POST', p: '/_twin/accounts', b: { username: 'nope' }, headers: {}, readOnly: true });
1036
+ const unknown = await h({ m: 'POST', p: '/_twin/nothing', b: {}, headers: {} });
1037
+ return readOnly.status === 405 && unknown.status === 404;
1038
+ })),
1039
+
1040
+ // ── the enumerated surface this pack does NOT model yet ──────────────────────────────────────
1041
+ todo('x.tweets.poll', 'tweets', 'Post with a poll (POST /2/tweets with poll.options/duration_minutes)', 'api', 'common'),
1042
+ done('x.tweets.media_attachment', 'tweets', 'Post with media (POST /2/tweets with media.media_ids), read back through attachments.media_keys and includes.media', 'api', 'common', () =>
1043
+ withWorld(async (h) => {
1044
+ await mediaToken(h);
1045
+ const up = (await h({ m: 'POST', p: '/2/media/upload', b: { media: b64(tinyPng()), media_category: 'tweet_image' }, headers: MEDIA_AUTH })).body as Body;
1046
+ const mediaId = up.data?.id as string;
1047
+ // another account's media id is X's own refusal, and nothing lands
1048
+ await h({ m: 'POST', p: '/_twin/tokens', b: { token: 'member-token', account_id: MEMBER, scopes: FULL_SCOPES }, headers: {} });
1049
+ const foreign = await h({ m: 'POST', p: '/2/tweets', b: { text: 'not mine', media: { media_ids: [mediaId] } }, headers: { authorization: 'Bearer member-token' } });
1050
+ if (foreign.status !== 400 || (foreign.body as Body).errors?.[0]?.message !== 'Your media IDs are invalid.') return false;
1051
+ const created = await h({ m: 'POST', p: '/2/tweets', b: { text: 'with a picture', media: { media_ids: [mediaId] } } });
1052
+ const postId = (created.body as Body).data?.id as string;
1053
+ if (created.status !== 201) return false;
1054
+ const read = (await h({ m: 'GET', p: `/2/tweets/${postId}?expansions=attachments.media_keys&media.fields=url,type,width,height` })).body as Body;
1055
+ const media = read.includes?.media?.[0];
1056
+ // the default projection carries no attachments; the expansion brings them and the sidecar
1057
+ const plain = (await h({ m: 'GET', p: `/2/tweets/${postId}` })).body as Body;
1058
+ return read.data?.attachments?.media_keys?.[0] === up.data.media_key && media?.media_key === up.data.media_key
1059
+ && media.type === 'photo' && media.width === 64 && media.height === 48 && /^https:\/\/pbs\.twimg\.com\/media\/[0-9a-f]{64}\.png$/.test(media.url)
1060
+ && plain.data?.attachments === undefined && plain.includes === undefined;
1061
+ })),
1062
+ todo('x.tweets.reply_settings', 'tweets', 'reply_settings (mentionedUsers / following / subscribers)', 'api', 'common'),
1063
+ todo('x.tweets.exclude_reply_user_ids', 'tweets', 'reply.exclude_reply_user_ids on a reply', 'api', 'niche'),
1064
+ todo('x.tweets.super_followers_only', 'tweets', 'for_super_followers_only posts', 'api', 'niche'),
1065
+ todo('x.tweets.geo_place', 'tweets', 'geo.place_id tagging on a post', 'api', 'niche'),
1066
+ todo('x.tweets.hide_reply', 'tweets', 'Hide/unhide a reply (PUT /2/tweets/:id/hidden)', 'api', 'common'),
1067
+ todo('x.tweets.quote_tweets_list', 'tweets', 'Quote posts of a post (GET /2/tweets/:id/quote_tweets)', 'api', 'common'),
1068
+ todo('x.tweets.liking_users', 'tweets', 'Users who liked a post (GET /2/tweets/:id/liking_users)', 'api', 'common'),
1069
+ todo('x.tweets.retweeted_by', 'tweets', 'Users who reposted a post (GET /2/tweets/:id/retweeted_by)', 'api', 'common'),
1070
+ todo('x.fields.entities', 'fields', 'entities (mentions, urls, hashtags) on a returned post', 'api', 'common'),
1071
+ todo('x.fields.public_metrics', 'fields', 'public_metrics (reply/repost/like/quote counts) on a post', 'api', 'common'),
1072
+ todo('x.timelines.exclude', 'timelines', 'exclude=retweets,replies on a timeline read', 'api', 'common'),
1073
+ todo('x.search.seven_day_window', 'search', "Recent search's seven-day corpus window, and the refusal for a start_time older than it", 'api', 'common'),
1074
+ todo('x.search.all', 'search', 'Full-archive search (GET /2/tweets/search/all)', 'api', 'niche'),
1075
+ todo('x.search.counts', 'search', 'Post counts (GET /2/tweets/counts/recent and /all)', 'api', 'niche'),
1076
+ todo('x.search.query_grammar', 'search', "X's search query grammar (operators, groups, negation)", 'api', 'niche'),
1077
+ todo('x.engagement.like', 'engagement', 'Like / unlike a post (POST and DELETE /2/users/:id/likes)', 'api', 'common'),
1078
+ todo('x.engagement.retweet', 'engagement', 'Repost / un-repost (POST and DELETE /2/users/:id/retweets)', 'api', 'common'),
1079
+ todo('x.engagement.bookmark', 'engagement', 'Bookmarks (GET, POST and DELETE /2/users/:id/bookmarks)', 'api', 'niche'),
1080
+ todo('x.engagement.liked_tweets', 'engagement', 'Posts a user liked (GET /2/users/:id/liked_tweets)', 'api', 'niche'),
1081
+ todo('x.streaming.rules', 'streaming', 'Filtered-stream rule management (GET and POST /2/tweets/search/stream/rules)', 'api', 'niche'),
1082
+ todo('x.rate_limit.published_figures', 'rate_limit', "Pin the budget to X's published per-endpoint posting limits (the ceiling here is conservative, not transcribed)", 'connector', 'core'),
1083
+ todo('x.rate_limit.response_headers', 'rate_limit', 'x-rate-limit-limit/remaining/reset headers on every /2 response, counted per window', 'api', 'common'),
1084
+ todo('x.rate_limit.window_enforcement', 'rate_limit', 'Enforce the real 15-minute window in the twin rather than only on an armed refusal', 'api', 'common'),
1085
+ todo('x.errors.scope_wording', 'errors', "Pin X's own wording for the insufficient-scope 403 (the twin states the refusal plainly)", 'api', 'common'),
1086
+ todo('x.errors.resource_not_found_wording', 'errors', "Pin X's own detail/title wording for the resource-not-found problem", 'api', 'common'),
1087
+ todo('x.errors.text_length_weighting', 'errors', "X's WEIGHTED character count (CJK and emoji cost two, a URL counts 23) rather than the twin's plain code-point count", 'api', 'common'),
1088
+ todo('x.errors.duplicate_content', 'errors', "X's duplicate-content refusal for posting the same text twice", 'api', 'common'),
1089
+ todo('x.auth.app_only_bearer', 'auth', 'App-only (OAuth 2.0 client-credentials) bearer for the public read endpoints', 'api', 'common'),
1090
+ todo('x.auth.oauth1_user_context', 'auth', "OAuth 1.0a user-context signing, X's alternate auth for these endpoints", 'api', 'niche'),
1091
+ todo('x.auth.access_tier_gate', 'auth', "Access-tier gating (Free/Basic/Pro), which decides whether an endpoint exists for a token at all", 'api', 'common'),
1092
+ // ══ MIRROR (dimension: 'ui' — every one is DATA-COUPLED: seed through the handler, read the
1093
+ // SAME route the screen reads, render the mirror's OWN component over it) ══════════════════════
1094
+ done('x.mirror.profile_header', 'mirror', "The profile header renders the account the user-lookup route serves", 'ui', 'core', () =>
1095
+ withWorld(async (h) => {
1096
+ await h({ m: 'POST', p: '/_twin/accounts', b: { id: '3000', username: 'profiled', name: 'Profiled Org', description: 'we build things', location: 'Brooklyn', verified: true }, headers: {} });
1097
+ const user = ((await h({ m: 'GET', p: `/2/users/by/username/profiled?${PROFILE_READ_QUERY}` })).body as Body).data as XRow | undefined;
1098
+ if (!user) return false;
1099
+ const markup = renderToStaticMarkup(createElement(ProfileHeader, { user }));
1100
+ return markup.includes('Profiled Org') && markup.includes('@profiled') && markup.includes('we build things')
1101
+ && markup.includes('Brooklyn') && markup.includes(joinedLabel(user.created_at)) && markup.includes('class="badge"');
1102
+ })),
1103
+ done('x.mirror.timeline', 'mirror', "The timeline renders the account's own posts newest first, each with name, @handle, time and text", 'ui', 'core', () =>
1104
+ withWorld(async (h) => {
1105
+ const older = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'the older post' } })).body as Body).data.id as string;
1106
+ const newer = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'the newer post, see @member' } })).body as Body).data.id as string;
1107
+ const page = (await h({ m: 'GET', p: `/2/users/${ORG}/tweets?${POST_READ_QUERY}` })).body as Body;
1108
+ const rows = (page.data ?? []) as XRow[];
1109
+ const users = usersById(page.includes);
1110
+ const tweets = tweetsById(page.includes);
1111
+ const markup = rows.map((post) => renderToStaticMarkup(createElement(PostRow, { post, users, tweets, nowMs: Date.parse(String(post.created_at)) + 120_000 }))).join('');
1112
+ const iNewer = markup.indexOf(`data-post-id="${newer}"`);
1113
+ const iOlder = markup.indexOf(`data-post-id="${older}"`);
1114
+ return rows.length === 2 && iNewer !== -1 && iOlder !== -1 && iNewer < iOlder
1115
+ && markup.includes('Org Voice') && markup.includes('@orgvoice') && markup.includes('>2m<')
1116
+ // the mention is an entity link, as x.com draws it
1117
+ && markup.includes('href="#/member"');
1118
+ })),
1119
+ done('x.mirror.quote_card', 'mirror', 'A quote post renders the quoted post inside its card, from the `includes` the read expanded', 'ui', 'common', () =>
1120
+ withWorld(async (h) => {
1121
+ const mentionId = await seedMention(h, 'a member said something @orgvoice');
1122
+ const quote = ((await h({ m: 'POST', p: '/2/tweets', b: { text: 'worth reading', quote_tweet_id: mentionId } })).body as Body).data.id as string;
1123
+ const read = (await h({ m: 'GET', p: `/2/tweets/${quote}?${POST_READ_QUERY}` })).body as Body;
1124
+ if (!read.data) return false;
1125
+ const markup = renderToStaticMarkup(createElement(PostRow, { post: read.data as XRow, users: usersById(read.includes), tweets: tweetsById(read.includes), nowMs: Date.now() }));
1126
+ return markup.includes('class="quote"') && markup.includes('a member said something') && markup.includes('@member') && markup.includes('worth reading');
1127
+ })),
1128
+ done('x.mirror.serves_the_vendor_api_on_its_own_origin', 'mirror', 'The mirror origin serves the REAL X handler — one serving code path, not a second app', 'ui', 'core', async () => {
1129
+ const root = mkdtempSync(join(tmpdir(), 'x-cap-mirror-'));
1130
+ const server = await createXMirrorServer({ root, port: 0 });
1131
+ try {
1132
+ const json = { 'content-type': 'application/json' };
1133
+ await fetch(`${server.url}/_twin/accounts`, { method: 'POST', headers: json, body: JSON.stringify({ id: ORG, username: 'orgvoice', name: 'Org Voice' }) });
1134
+ await fetch(`${server.url}/_twin/tokens`, { method: 'POST', headers: json, body: JSON.stringify({ token: TOKEN, account_id: ORG, scopes: FULL_SCOPES }) });
1135
+ // 1. a WRITE to X's real path, through the MIRROR origin
1136
+ const posted = await fetch(`${server.url}/2/tweets`, { method: 'POST', headers: { ...json, ...AUTH }, body: JSON.stringify({ text: 'written through the mirror origin' }) });
1137
+ if (posted.status !== 201) return false;
1138
+ const id = ((await posted.json()) as Body).data.id;
1139
+ // 2. it landed in the SAME projection the API serves — read back through the handler directly
1140
+ const direct = await handleXTwinRequest({ method: 'GET', path: `/2/tweets/${id}`, headers: AUTH, root });
1141
+ // 3. the mirror origin enforces the twin's OWN auth: no bearer is X's 401, not a mirror bypass
1142
+ const anon = await fetch(`${server.url}/2/users/by/username/orgvoice`);
1143
+ const shell = await (await fetch(`${server.url}/`)).text();
1144
+ return direct.status === 200 && (direct.body as Body).data?.text === 'written through the mirror origin'
1145
+ && anon.status === 401 && shell.includes('<div id="root">');
1146
+ } finally {
1147
+ server.stop();
1148
+ rmSync(root, { recursive: true, force: true });
1149
+ }
1150
+ }),
1151
+ todo('x.mirror.notifications_all', 'mirror', "Notifications beyond Mentions (likes, reposts, follows) — the twin models no engagement events to list", 'ui', 'common'),
1152
+ todo('x.mirror.profile_counts', 'mirror', "The Following / Followers counts under the profile header (needs user public_metrics)", 'ui', 'common'),
1153
+
1154
+ // ── Pull-surface coverage audit gaps — filed as manifest todos so
1155
+ // the demand-ordered build list is drawn from manifest todos. See pull-audit.json.
1156
+ todo('x.connector.pull_conversation', 'connector', 'Connector: pull the replies under a conversation from the real vendor', 'connector', 'common'),
1157
+ ];
1158
+
1159
+ export async function xCapabilities(): Promise<CapabilityReport> {
1160
+ return checkCapabilities('x', X_CAPABILITIES);
1161
+ }