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