@volter/twin-standard 1.0.30 → 1.0.32

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 (149) hide show
  1. package/dist/src/archive-source.d.ts +13 -0
  2. package/dist/src/archive-source.js +176 -0
  3. package/dist/src/check-sources.d.ts +19 -2
  4. package/dist/src/check-sources.js +95 -37
  5. package/dist/src/cli.js +52 -9
  6. package/dist/src/clients/ai-gateway.d.ts +1 -0
  7. package/dist/src/clients/ai-gateway.js +3 -0
  8. package/dist/src/clients/anthropic-messages.d.ts +10 -0
  9. package/dist/src/clients/anthropic-messages.js +52 -0
  10. package/dist/src/clients/anthropic.d.ts +1 -0
  11. package/dist/src/clients/anthropic.js +3 -0
  12. package/dist/src/clients/aws.d.ts +2 -0
  13. package/dist/src/clients/aws.js +137 -0
  14. package/dist/src/clients/cerebras.d.ts +1 -0
  15. package/dist/src/clients/cerebras.js +3 -0
  16. package/dist/src/clients/clerk.d.ts +2 -0
  17. package/dist/src/clients/clerk.js +99 -0
  18. package/dist/src/clients/cloudflare.d.ts +2 -0
  19. package/dist/src/clients/cloudflare.js +86 -0
  20. package/dist/src/clients/elevenlabs.d.ts +2 -0
  21. package/dist/src/clients/elevenlabs.js +39 -0
  22. package/dist/src/clients/gemini.d.ts +2 -0
  23. package/dist/src/clients/gemini.js +92 -0
  24. package/dist/src/clients/github.d.ts +2 -0
  25. package/dist/src/clients/github.js +71 -0
  26. package/dist/src/clients/groq.d.ts +1 -0
  27. package/dist/src/clients/groq.js +3 -0
  28. package/dist/src/clients/harness.d.ts +36 -0
  29. package/dist/src/clients/harness.js +74 -0
  30. package/dist/src/clients/merge.d.ts +1 -0
  31. package/dist/src/clients/merge.js +8 -0
  32. package/dist/src/clients/openai-compatible.d.ts +9 -0
  33. package/dist/src/clients/openai-compatible.js +51 -0
  34. package/dist/src/clients/openai.d.ts +2 -0
  35. package/dist/src/clients/openai.js +65 -0
  36. package/dist/src/clients/openrouter.d.ts +1 -0
  37. package/dist/src/clients/openrouter.js +3 -0
  38. package/dist/src/clients/perplexity.d.ts +1 -0
  39. package/dist/src/clients/perplexity.js +3 -0
  40. package/dist/src/clients/planetscale.d.ts +2 -0
  41. package/dist/src/clients/planetscale.js +109 -0
  42. package/dist/src/clients/posthog.d.ts +2 -0
  43. package/dist/src/clients/posthog.js +145 -0
  44. package/dist/src/clients/run.d.ts +1 -0
  45. package/dist/src/clients/run.js +25 -0
  46. package/dist/src/clients/sentry.d.ts +2 -0
  47. package/dist/src/clients/sentry.js +167 -0
  48. package/dist/src/clients/slack.d.ts +2 -0
  49. package/dist/src/clients/slack.js +107 -0
  50. package/dist/src/clients/stripe.d.ts +2 -0
  51. package/dist/src/clients/stripe.js +161 -0
  52. package/dist/src/clients/tinybird.d.ts +2 -0
  53. package/dist/src/clients/tinybird.js +71 -0
  54. package/dist/src/clients/tunnel.d.ts +2 -0
  55. package/dist/src/clients/tunnel.js +99 -0
  56. package/dist/src/clients/upstash.d.ts +2 -0
  57. package/dist/src/clients/upstash.js +143 -0
  58. package/dist/src/clients/volteridentity.d.ts +2 -0
  59. package/dist/src/clients/volteridentity.js +128 -0
  60. package/dist/src/clients/xai.d.ts +2 -0
  61. package/dist/src/clients/xai.js +23 -0
  62. package/dist/src/conformance.d.ts +33 -0
  63. package/dist/src/conformance.js +520 -0
  64. package/dist/src/derive.js +113 -22
  65. package/dist/src/gate.js +22 -16
  66. package/dist/src/index.d.ts +5 -3
  67. package/dist/src/index.js +5 -3
  68. package/dist/src/lanes.d.ts +2 -0
  69. package/dist/src/lanes.js +7 -0
  70. package/dist/src/p3-rules.js +0 -2
  71. package/dist/src/protocol-3.d.ts +1 -0
  72. package/dist/src/protocol-3.js +76 -30
  73. package/dist/src/published.d.ts +6 -0
  74. package/dist/src/published.js +65 -11
  75. package/dist/src/spec-documents.js +2 -1
  76. package/dist/src/spec-ir-client.d.ts +5 -1
  77. package/dist/src/spec-ir-client.js +12 -2
  78. package/dist/src/spec-ir.d.ts +7 -2
  79. package/dist/src/spec-ir.js +42 -10
  80. package/dist/src/verdict.d.ts +14 -0
  81. package/dist/src/verdict.js +30 -0
  82. package/dist/src/walk/behavior-journey.d.ts +47 -0
  83. package/dist/src/walk/behavior-journey.js +1138 -0
  84. package/dist/src/walk/git-client.d.ts +26 -0
  85. package/dist/src/walk/git-client.js +139 -0
  86. package/dist/src/walk/journey-kit.d.ts +445 -0
  87. package/dist/src/walk/journey-kit.js +743 -0
  88. package/dist/src/walk/shape.d.ts +54 -0
  89. package/dist/src/walk/shape.js +210 -0
  90. package/dist/src/walk/sse-example.d.ts +2 -0
  91. package/dist/src/walk/sse-example.js +32 -0
  92. package/dist/src/walk/vendor-fetch.d.ts +20 -0
  93. package/dist/src/walk/vendor-fetch.js +92 -0
  94. package/dist/src/walk/walk-database.d.ts +12 -0
  95. package/dist/src/walk/walk-database.js +96 -0
  96. package/dist/src/walk/xml-view.d.ts +2 -0
  97. package/dist/src/walk/xml-view.js +55 -0
  98. package/package.json +39 -3
  99. package/src/archive-source.ts +141 -0
  100. package/src/check-sources.ts +83 -33
  101. package/src/cli.ts +40 -8
  102. package/src/clients/ai-gateway.ts +4 -0
  103. package/src/clients/anthropic-messages.ts +60 -0
  104. package/src/clients/anthropic.ts +4 -0
  105. package/src/clients/aws.ts +119 -0
  106. package/src/clients/cerebras.ts +4 -0
  107. package/src/clients/clerk.ts +97 -0
  108. package/src/clients/cloudflare.ts +80 -0
  109. package/src/clients/elevenlabs.ts +38 -0
  110. package/src/clients/gemini.ts +89 -0
  111. package/src/clients/github.ts +67 -0
  112. package/src/clients/groq.ts +4 -0
  113. package/src/clients/harness.ts +101 -0
  114. package/src/clients/merge.ts +9 -0
  115. package/src/clients/openai-compatible.ts +59 -0
  116. package/src/clients/openai.ts +66 -0
  117. package/src/clients/openrouter.ts +4 -0
  118. package/src/clients/perplexity.ts +4 -0
  119. package/src/clients/planetscale.ts +98 -0
  120. package/src/clients/posthog.ts +132 -0
  121. package/src/clients/run.ts +18 -0
  122. package/src/clients/sentry.ts +157 -0
  123. package/src/clients/slack.ts +98 -0
  124. package/src/clients/stripe.ts +129 -0
  125. package/src/clients/tinybird.ts +69 -0
  126. package/src/clients/tunnel.ts +79 -0
  127. package/src/clients/upstash.ts +137 -0
  128. package/src/clients/volteridentity.ts +108 -0
  129. package/src/clients/xai.ts +25 -0
  130. package/src/conformance.ts +446 -0
  131. package/src/derive.ts +87 -16
  132. package/src/gate.ts +20 -14
  133. package/src/index.ts +5 -3
  134. package/src/lanes.ts +7 -0
  135. package/src/p3-rules.ts +0 -2
  136. package/src/protocol-3.ts +71 -27
  137. package/src/published.ts +59 -11
  138. package/src/spec-documents.ts +2 -1
  139. package/src/spec-ir-client.ts +15 -4
  140. package/src/spec-ir.ts +43 -5
  141. package/src/verdict.ts +28 -0
  142. package/src/walk/behavior-journey.ts +806 -0
  143. package/src/walk/git-client.ts +138 -0
  144. package/src/walk/journey-kit.ts +858 -0
  145. package/src/walk/shape.ts +208 -0
  146. package/src/walk/sse-example.ts +25 -0
  147. package/src/walk/vendor-fetch.ts +80 -0
  148. package/src/walk/walk-database.ts +97 -0
  149. package/src/walk/xml-view.ts +44 -0
@@ -0,0 +1,74 @@
1
+ // THE VENDOR'S OWN CLIENT — how conformance drives a pack the way an application does (docs/contributing/architecture.md,
2
+ // "Conformance"): the vendor's official SDK, unmodified, in a Node process as an application runs it, against the pack
3
+ // served as a World runs it. Its credentials are the ones the pack's own credential door issued; its connections reach the
4
+ // served port as a World's interception routes the vendor's hosts, the vendor's URL and Host unchanged (an HTTP request
5
+ // over node:http with its Host kept, a socket's TCP connection made to the served port). A case's expectations are the
6
+ // vendor's documented semantics, each cited, never the pack author's life.
7
+ import { createServer, request as httpRequest } from 'node:http';
8
+ import { Agent as HttpsAgent } from 'node:https';
9
+ import { connect } from 'node:net';
10
+ import { writeFileSync } from 'node:fs';
11
+ /** The World's proxy in front of a pack served on `served`: it ends the caller's TLS (here, none was begun) and forwards
12
+ * each request and socket upgrade to the pack with `x-forwarded-proto: https`, the vendor's Host unchanged, as a World's
13
+ * proxy forwards a caller of the vendor's https host (twin-fetch.ts, twinPublicBase: the links and socket URLs the pack
14
+ * answers are then the vendor's secure ones). Answers the proxy's port. */
15
+ export async function worldProxy(served) {
16
+ const server = createServer((req, res) => {
17
+ const forward = httpRequest({ host: '127.0.0.1', port: served, method: req.method, path: req.url, headers: { ...req.headers, 'x-forwarded-proto': 'https' } }, (answer) => {
18
+ res.writeHead(answer.statusCode ?? 502, answer.headers);
19
+ answer.pipe(res);
20
+ });
21
+ forward.on('error', () => { res.writeHead(502); res.end(); });
22
+ req.pipe(forward);
23
+ });
24
+ server.on('upgrade', (req, socket, head) => {
25
+ const upstream = connect(served, '127.0.0.1', () => {
26
+ const lines = [`${req.method} ${req.url} HTTP/1.1`, ...Object.entries({ ...req.headers, 'x-forwarded-proto': 'https' }).flatMap(([k, v]) => (Array.isArray(v) ? v : [v]).map((x) => `${k}: ${x}`)), '', ''];
27
+ upstream.write(lines.join('\r\n'));
28
+ if (head.length)
29
+ upstream.write(head);
30
+ upstream.pipe(socket);
31
+ socket.pipe(upstream);
32
+ });
33
+ upstream.on('error', () => socket.destroy());
34
+ socket.on('error', () => upstream.destroy());
35
+ });
36
+ await new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
37
+ server.unref();
38
+ return server.address().port;
39
+ }
40
+ /** The context for a pack reached at `port` of this machine (the World's proxy in front of it). */
41
+ export function clientContext(port, host, env, scenarioPath) {
42
+ const routedFetch = (async (input, init) => {
43
+ const request = new Request(input, init);
44
+ const url = new URL(request.url);
45
+ const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : Buffer.from(await request.arrayBuffer());
46
+ const headers = Object.fromEntries(request.headers);
47
+ headers.host = url.host;
48
+ return new Promise((resolve, reject) => {
49
+ const sent = httpRequest({ host: '127.0.0.1', port, method: request.method, path: `${url.pathname}${url.search}`, headers }, (res) => {
50
+ const chunks = [];
51
+ res.on('data', (c) => chunks.push(c));
52
+ res.on('end', () => {
53
+ const answer = new Headers();
54
+ for (const [k, v] of Object.entries(res.headers))
55
+ if (v !== undefined)
56
+ for (const one of Array.isArray(v) ? v : [v])
57
+ answer.append(k, one);
58
+ resolve(new Response(res.statusCode === 204 || res.statusCode === 304 ? null : Buffer.concat(chunks), { status: res.statusCode ?? 0, headers: answer }));
59
+ });
60
+ res.on('error', reject);
61
+ });
62
+ sent.on('error', reject);
63
+ sent.end(body);
64
+ });
65
+ });
66
+ const agent = new (class extends HttpsAgent {
67
+ createConnection() { return connect(port, '127.0.0.1'); }
68
+ })({ keepAlive: false });
69
+ return {
70
+ host, env, fetch: routedFetch, agent,
71
+ connect: () => connect(port, '127.0.0.1'),
72
+ scenario: (doc) => writeFileSync(scenarioPath, JSON.stringify(doc)),
73
+ };
74
+ }
@@ -0,0 +1 @@
1
+ export declare const cases: import("./harness.js").ClientCase[];
@@ -0,0 +1,8 @@
1
+ // Merge Gateway through the SDKs its callers use: openai-node on its chat completions, Anthropic's SDK on its Messages
2
+ // surface (both of its OpenAI- and Anthropic-compatible APIs), the shared cases.
3
+ import { messagesCases } from "./anthropic-messages.js";
4
+ import { chatCases } from "./openai-compatible.js";
5
+ export const cases = [
6
+ ...chatCases({ key: 'MERGE_GATEWAY_API_KEY', base: (host) => `https://${host}/v1`, model: 'zai/glm-5.3-flash' }),
7
+ ...messagesCases({ key: 'MERGE_GATEWAY_API_KEY', base: (host) => `https://${host}`, model: 'zai/glm-5.3-flash', toolTurn: { toolCalls: [{ name: 'lookup', arguments: {} }] } }),
8
+ ];
@@ -0,0 +1,9 @@
1
+ import type { ClientCase, ClientContext } from './harness.js';
2
+ /** A vendor's chat as openai-node reaches it: the variable its key is issued in, the base URL on its host, a model it serves. */
3
+ export type Compatible = {
4
+ key: string;
5
+ base: (host: string) => string;
6
+ model: string;
7
+ };
8
+ export declare function openaiClient(c: ClientContext, v: Compatible): Promise<import("openai").default>;
9
+ export declare function chatCases(v: Compatible): ClientCase[];
@@ -0,0 +1,51 @@
1
+ const CHAT = 'https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create';
2
+ const TOOLS = [{ type: 'function', function: { name: 'lookup', parameters: { type: 'object', properties: {} } } }];
3
+ const SCRIPT = { handlers: [
4
+ { id: 'calls', on: { hasTool: 'lookup' }, respond: { toolCalls: [{ name: 'lookup', arguments: {} }] } },
5
+ { id: 'words', on: { userTextIncludes: 'hello' }, respond: { text: 'scripted words' } },
6
+ ] };
7
+ export async function openaiClient(c, v) {
8
+ const { default: OpenAI } = await import('openai');
9
+ return new OpenAI({ apiKey: c.env[v.key] ?? '', baseURL: v.base(c.host), fetch: c.fetch, maxRetries: 0, timeout: 10_000 });
10
+ }
11
+ export function chatCases(v) {
12
+ const ask = async (c, extra = {}) => (await openaiClient(c, v)).chat.completions.create({ model: v.model, messages: [{ role: 'user', content: 'hello' }], ...extra });
13
+ return [
14
+ {
15
+ id: 'chat-answers',
16
+ asks: "a chat completion is answered, the World's scenario loaded",
17
+ source: `${CHAT} "Creates a model response for the given chat conversation."`,
18
+ async run(c) {
19
+ c.scenario(SCRIPT);
20
+ const answer = await ask(c).catch((e) => e);
21
+ if (answer instanceof Error)
22
+ return [`the client's chat.completions.create failed: ${answer.message}`];
23
+ return answer.choices?.[0]?.message?.content === 'scripted words' ? [] : [`the scripted turn was not answered: ${JSON.stringify(answer.choices?.[0]?.message).slice(0, 120)}`];
24
+ },
25
+ },
26
+ {
27
+ id: 'chat-tool-choice-none',
28
+ asks: 'tool_choice none is answered with a message, never a tool call',
29
+ source: `${CHAT} "none means the model will not call any tool and instead generates a message."`,
30
+ async run(c) {
31
+ c.scenario(SCRIPT);
32
+ const none = await ask(c, { tools: TOOLS, tool_choice: 'none' }).catch((e) => e);
33
+ if (none instanceof Error)
34
+ return [`the client's chat.completions.create failed: ${none.message}`];
35
+ return none.choices?.[0]?.message?.tool_calls?.length ? ['tool_choice none was answered with a tool call'] : [];
36
+ },
37
+ },
38
+ {
39
+ id: 'scenario-each-turn',
40
+ asks: "the World's scenario rewritten between two turns decides the second (no reboot)",
41
+ source: 'architecture "The scenario is read from the World\'s store at every turn"',
42
+ async run(c) {
43
+ c.scenario({ handlers: [{ id: 'one', on: { userTextIncludes: 'hello' }, respond: { text: 'first script' } }] });
44
+ const first = await ask(c).then((a) => a.choices?.[0]?.message?.content, (e) => `error ${e.message}`);
45
+ c.scenario({ handlers: [{ id: 'two', on: { userTextIncludes: 'hello' }, respond: { text: 'second script' } }] });
46
+ const second = await ask(c).then((a) => a.choices?.[0]?.message?.content, (e) => `error ${e.message}`);
47
+ return first === 'first script' && second === 'second script' ? [] : [`the turns answered "${first}" then "${second}"`];
48
+ },
49
+ },
50
+ ];
51
+ }
@@ -0,0 +1,2 @@
1
+ import type { ClientCase } from './harness.js';
2
+ export declare const cases: ClientCase[];
@@ -0,0 +1,65 @@
1
+ import { chatCases } from "./openai-compatible.js";
2
+ const CHAT = 'https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create';
3
+ const REALTIME = 'https://developers.openai.com/api/reference/resources/realtime/server-events';
4
+ async function client(c) {
5
+ const { default: OpenAI } = await import('openai');
6
+ return new OpenAI({ apiKey: c.env.OPENAI_API_KEY ?? '', baseURL: `https://${c.host}/v1`, fetch: c.fetch, maxRetries: 0, timeout: 10_000 });
7
+ }
8
+ const TOOLS = [{ type: 'function', function: { name: 'lookup', parameters: { type: 'object', properties: {} } } }];
9
+ // a turn that calls the tool wherever one is offered, and one that answers in words
10
+ const SCRIPT = { handlers: [
11
+ { id: 'calls', on: { hasTool: 'lookup' }, respond: { toolCalls: [{ name: 'lookup', arguments: {} }] } },
12
+ { id: 'words', on: { userTextIncludes: 'hello' }, respond: { text: 'scripted words' } },
13
+ ] };
14
+ /** A Realtime session through the client (openai-node's server-side Realtime client, on the ws library, as a Node
15
+ * application opens it), until its response is done: its first event and the output items' types, or what went wrong. */
16
+ async function realtimeTurn(c, toolChoice) {
17
+ const { OpenAIRealtimeWS } = await import('openai/realtime/ws');
18
+ const openai = await client(c);
19
+ return new Promise((done) => {
20
+ const rt = new OpenAIRealtimeWS({ model: 'gpt-realtime', options: { createConnection: c.connect } }, openai);
21
+ let first;
22
+ const timer = setTimeout(() => { rt.close(); done({ first, error: 'no response.done in 10 s' }); }, 10_000);
23
+ rt.on('error', (e) => { clearTimeout(timer); rt.close(); done({ first, error: String(e.message ?? e) }); });
24
+ rt.socket.on('open', () => {
25
+ rt.send({ type: 'session.update', session: { type: 'realtime', output_modalities: ['text'], tools: [{ type: 'function', name: 'lookup', parameters: { type: 'object', properties: {} } }], tool_choice: toolChoice } });
26
+ rt.send({ type: 'conversation.item.create', item: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hello' }] } });
27
+ rt.send({ type: 'response.create' });
28
+ });
29
+ rt.on('event', (e) => {
30
+ first ??= e.type;
31
+ if (e.type === 'response.done') {
32
+ clearTimeout(timer);
33
+ rt.close();
34
+ done({ first, output: (e.response?.output ?? []).map((o) => o.type) });
35
+ }
36
+ });
37
+ });
38
+ }
39
+ export const cases = [
40
+ ...chatCases({ key: 'OPENAI_API_KEY', base: (host) => `https://${host}/v1`, model: 'gpt-4o' }),
41
+ {
42
+ id: 'realtime-connects',
43
+ asks: 'a Realtime connection opens and its first event is session.created',
44
+ source: `${REALTIME} "Emitted automatically when a new connection is established as the first server event."`,
45
+ async run(c) {
46
+ c.scenario(SCRIPT);
47
+ const turn = await realtimeTurn(c, 'auto');
48
+ if (turn.error && !turn.first)
49
+ return [`the client's Realtime connection failed: ${turn.error}`];
50
+ return turn.first === 'session.created' ? [] : [`the first server event was ${turn.first ?? 'none'}`];
51
+ },
52
+ },
53
+ {
54
+ id: 'realtime-tool-choice-none',
55
+ asks: "a Realtime response under tool_choice none carries no function call",
56
+ source: `${CHAT} "none means the model will not call any tool and instead generates a message."`,
57
+ async run(c) {
58
+ c.scenario(SCRIPT);
59
+ const turn = await realtimeTurn(c, 'none');
60
+ if (turn.error)
61
+ return [`the Realtime turn failed: ${turn.error}`];
62
+ return turn.output?.includes('function_call') ? ['tool_choice none was answered with a function_call item'] : [];
63
+ },
64
+ },
65
+ ];
@@ -0,0 +1 @@
1
+ export declare const cases: import("./harness.js").ClientCase[];
@@ -0,0 +1,3 @@
1
+ // OpenRouter's chat through openai-node (its OpenAI-compatible API), the shared chat cases.
2
+ import { chatCases } from "./openai-compatible.js";
3
+ export const cases = chatCases({ key: 'OPENROUTER_API_KEY', base: (host) => `https://${host}/api/v1`, model: 'openai/gpt-4o' });
@@ -0,0 +1 @@
1
+ export declare const cases: import("./harness.js").ClientCase[];
@@ -0,0 +1,3 @@
1
+ // Perplexity's chat through openai-node (its OpenAI-compatible API), the shared chat cases.
2
+ import { chatCases } from "./openai-compatible.js";
3
+ export const cases = chatCases({ key: 'PERPLEXITY_API_KEY', base: (host) => `https://${host}`, model: 'test-model' });
@@ -0,0 +1,2 @@
1
+ import type { ClientCase } from './harness.js';
2
+ export declare const cases: ClientCase[];
@@ -0,0 +1,109 @@
1
+ const DRIVER = 'https://planetscale.com/docs/tutorials/planetscale-serverless-driver';
2
+ const README = 'https://github.com/planetscale/database-js';
3
+ const ERRORS = 'https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html';
4
+ /** A connection as Dub's lib/planetscale/connection.ts makes one: `connect({ url })`, here with the World's fetch. */
5
+ async function connection(c) {
6
+ const { connect } = (await import('@planetscale/database'));
7
+ return connect({ url: c.env.PLANETSCALE_DATABASE_URL, fetch: c.fetch });
8
+ }
9
+ export const cases = [
10
+ {
11
+ id: 'parameters-and-types',
12
+ asks: "SQL sent with ? parameters, each replaced by its value, and the rows read back as the column's JavaScript type, as objects and as arrays",
13
+ source: `${DRIVER} "The driver package will replace the ? entries in the query with the values passed in the array, in the order in which they were placed."; ${README} "Column values are converted to their corresponding JavaScript data types."; ${README} "Rows can be returned as an object or an array of column values by passing an as option to execute ."`,
14
+ async run(c) {
15
+ try {
16
+ if (!c.env.PLANETSCALE_DATABASE_URL)
17
+ return ['the credential door issued no PLANETSCALE_DATABASE_URL'];
18
+ const conn = await connection(c);
19
+ await conn.execute('CREATE TABLE hotels (id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, address VARCHAR(200) NOT NULL, stars FLOAT NOT NULL, PRIMARY KEY (id))');
20
+ const out = [];
21
+ const inserted = await conn.execute('INSERT INTO hotels (`name`, `address`, `stars`) VALUES (?, ?, ?)', ['The Galt House', '140 N Fourth St, Louisville, KY 40202', 4.2]);
22
+ if (inserted.rowsAffected !== 1)
23
+ out.push(`the insert affected ${inserted.rowsAffected} rows, not 1`);
24
+ if (inserted.insertId !== '1')
25
+ out.push(`the insert's id is ${JSON.stringify(inserted.insertId)}, not "1"`);
26
+ if (!inserted.statement.includes("'The Galt House'"))
27
+ out.push(`the statement sent was ${JSON.stringify(inserted.statement)}`);
28
+ const read = await conn.execute('SELECT id, name, stars FROM hotels WHERE name = ?', ['The Galt House']);
29
+ const row = read.rows[0];
30
+ if (!row)
31
+ return [...out, 'the hotel inserted was not read back'];
32
+ if (row.id !== 1)
33
+ out.push(`its id read back as ${JSON.stringify(row.id)}, not the number 1`);
34
+ if (row.name !== 'The Galt House')
35
+ out.push(`its name read back as ${JSON.stringify(row.name)}`);
36
+ if (row.stars !== 4.2)
37
+ out.push(`its stars read back as ${JSON.stringify(row.stars)} (${read.types.stars}), not the number 4.2`);
38
+ if (read.types.id !== 'UINT32')
39
+ out.push(`its id's type is ${String(read.types.id)}, not UINT32`);
40
+ const arrays = await conn.execute('SELECT id, name FROM hotels', null, { as: 'array' });
41
+ if (JSON.stringify(arrays.rows) !== JSON.stringify([[1, 'The Galt House']]))
42
+ out.push(`as arrays the rows are ${JSON.stringify(arrays.rows)}`);
43
+ return out;
44
+ }
45
+ catch (e) {
46
+ return [`the driver's calls failed: ${e.name}: ${e.message}`];
47
+ }
48
+ },
49
+ },
50
+ {
51
+ id: 'transaction-rolls-back',
52
+ asks: 'a transaction whose callback returns is committed, and one whose callback throws is rolled back, its writes gone',
53
+ source: `${README} "Use the transaction function to safely perform database transactions. If any unhandled errors are thrown during execution of the transaction, the transaction will be rolled back."`,
54
+ async run(c) {
55
+ try {
56
+ if (!c.env.PLANETSCALE_DATABASE_URL)
57
+ return ['the credential door issued no PLANETSCALE_DATABASE_URL'];
58
+ const conn = await connection(c);
59
+ await conn.execute('CREATE TABLE slotted_counters (record_type VARCHAR(40) NOT NULL, record_id INT NOT NULL, count INT NOT NULL, PRIMARY KEY (record_type, record_id))');
60
+ await conn.transaction(async (tx) => {
61
+ await tx.execute("INSERT INTO slotted_counters (record_type, record_id, count) VALUES ('branch_count', 42, 1)");
62
+ await tx.execute("INSERT INTO slotted_counters (record_type, record_id, count) VALUES ('branch_count', 43, 1)");
63
+ });
64
+ const refused = new Error('the second branch was refused');
65
+ const raised = await conn.transaction(async (tx) => {
66
+ await tx.execute("UPDATE slotted_counters SET count = count + 1 WHERE record_id = 42");
67
+ throw refused;
68
+ }).then(() => undefined, (e) => e);
69
+ const out = [];
70
+ if (raised !== refused)
71
+ out.push(`the failed transaction raised ${String(raised?.message)}, not its callback's error`);
72
+ const after = await conn.execute('SELECT record_id, count FROM slotted_counters ORDER BY record_id');
73
+ if (JSON.stringify(after.rows) !== JSON.stringify([{ record_id: 42, count: 1 }, { record_id: 43, count: 1 }]))
74
+ out.push(`after a commit and a rollback the counters are ${JSON.stringify(after.rows)}, not both at 1`);
75
+ return out;
76
+ }
77
+ catch (e) {
78
+ return [`the driver's calls failed: ${e.name}: ${e.message}`];
79
+ }
80
+ },
81
+ },
82
+ {
83
+ id: 'duplicate-key-raises',
84
+ asks: "inserting a primary key the table holds raises the driver's DatabaseError, its message ending in MySQL's errno and sqlstate, (errno 1062) (sqlstate 23000)",
85
+ source: `${ERRORS} "ER_DUP_ENTRY"; ${ERRORS} "Duplicate entry '%s' for key '%s'"`,
86
+ async run(c) {
87
+ try {
88
+ if (!c.env.PLANETSCALE_DATABASE_URL)
89
+ return ['the credential door issued no PLANETSCALE_DATABASE_URL'];
90
+ const conn = await connection(c);
91
+ await conn.execute('CREATE TABLE links (domain VARCHAR(190) NOT NULL, `key` VARCHAR(190) NOT NULL, PRIMARY KEY (domain, `key`))');
92
+ await conn.execute('INSERT INTO links (domain, `key`) VALUES (?, ?)', ['go.trailmark.io', 'pricing']);
93
+ const raised = await conn.execute('INSERT INTO links (domain, `key`) VALUES (?, ?)', ['go.trailmark.io', 'pricing'])
94
+ .then(() => undefined, (e) => e);
95
+ if (!raised)
96
+ return ['inserting the same key twice succeeded'];
97
+ const out = [];
98
+ if (raised.name !== 'DatabaseError')
99
+ out.push(`it raised ${String(raised.name)}, not the driver's DatabaseError`);
100
+ if (!/ \(errno 1062\) \(sqlstate 23000\)$/.test(String(raised.message)))
101
+ out.push(`its message is ${JSON.stringify(raised.message)}, not MySQL's errno 1062 and sqlstate 23000`);
102
+ return out;
103
+ }
104
+ catch (e) {
105
+ return [`the driver's calls failed: ${e.name}: ${e.message}`];
106
+ }
107
+ },
108
+ },
109
+ ];
@@ -0,0 +1,2 @@
1
+ import type { ClientCase } from './harness.js';
2
+ export declare const cases: ClientCase[];
@@ -0,0 +1,145 @@
1
+ const NODE = 'https://posthog.com/docs/libraries/node';
2
+ const CORE = 'https://unpkg.com/@posthog/core@1.45.1/src/posthog-core-stateless.ts';
3
+ const IDENTIFY = 'https://posthog.com/docs/product-analytics/person-properties';
4
+ async function client(c, options = {}) {
5
+ const { PostHog } = await import('posthog-node');
6
+ return new PostHog(options.key ?? c.env.POSTHOG_KEY ?? '', {
7
+ host: 'https://' + c.host, fetch: options.fetch ?? c.fetch, flushAt: options.flushAt ?? 100,
8
+ flushInterval: 0, fetchRetryCount: 2, fetchRetryDelay: 1, requestTimeout: 10_000,
9
+ });
10
+ }
11
+ async function read(c, resource, query = '') {
12
+ const response = await c.fetch((c.env.POSTHOG_API_HOST || 'https://us.posthog.com') + '/api/projects/' + c.env.POSTHOG_PROJECT_ID + '/' + resource + '/' + query, {
13
+ headers: { authorization: 'Bearer ' + c.env.POSTHOG_PERSONAL_API_KEY },
14
+ });
15
+ if (!response.ok)
16
+ throw new Error(resource + ' readback answered ' + response.status + ': ' + await response.text());
17
+ return await response.json();
18
+ }
19
+ export const cases = [
20
+ {
21
+ id: 'queued-captures-and-groups-drained-on-shutdown',
22
+ asks: 'shutdown drains queued captures and groupIdentify into a compressed batch; events and group properties read back',
23
+ source: NODE + ' "which can be awaited to ensure all queued events are sent to the API"',
24
+ async run(c) {
25
+ const requests = [];
26
+ const forwarded = (async (url, init) => {
27
+ const request = new Request(url, init);
28
+ if (request.method === 'POST')
29
+ requests.push(request.clone());
30
+ return c.fetch(request);
31
+ });
32
+ const sdk = await client(c, { fetch: forwarded });
33
+ sdk.groupIdentify({ groupType: 'organization', groupKey: 'sdk-harbourline', distinctId: 'sdk-manager', properties: { name: 'Harbourline SDK case' } });
34
+ sdk.capture({ distinctId: 'sdk-manager', event: 'sdk_meeting_started', properties: { meeting: 'morning' }, groups: { organization: 'sdk-harbourline' } });
35
+ sdk.capture({ distinctId: 'sdk-manager', event: 'sdk_meeting_finished', properties: { meeting: 'morning' }, groups: { organization: 'sdk-harbourline' } });
36
+ await sdk.shutdown();
37
+ const failures = [];
38
+ if (requests.length !== 1 || !new URL(requests[0].url).pathname.startsWith('/batch'))
39
+ failures.push('three queued calls did not drain as one batch');
40
+ if (requests[0]?.headers.get('content-encoding') !== 'gzip')
41
+ failures.push('the default Node transport did not send gzip');
42
+ const events = await read(c, 'events', '?distinct_id=sdk-manager');
43
+ for (const event of ['sdk_meeting_started', 'sdk_meeting_finished']) {
44
+ const row = events.results.find((r) => r.event === event);
45
+ if (!row || row.properties.meeting !== 'morning' || row.properties.$groups.organization !== 'sdk-harbourline')
46
+ failures.push(event + ' lost its properties or group link');
47
+ }
48
+ const types = await read(c, 'groups_types');
49
+ const groupType = (Array.isArray(types) ? types : types.results).find((r) => r.group_type === 'organization');
50
+ if (!groupType)
51
+ failures.push('groupIdentify made no group type');
52
+ else {
53
+ const groups = await read(c, 'groups', '?group_type_index=' + groupType.group_type_index + '&group_key=sdk-harbourline');
54
+ if (groups.results[0]?.group_properties.name !== 'Harbourline SDK case')
55
+ failures.push('groupIdentify lost the group properties');
56
+ }
57
+ return failures;
58
+ },
59
+ },
60
+ {
61
+ id: 'identify-alias-and-person-properties',
62
+ asks: 'alias links an anonymous ID; set_once preserves its first value and unset removes the named profile property',
63
+ source: IDENTIFY + ' "set_once only sets the property if it has never been set before."',
64
+ async run(c) {
65
+ const sdk = await client(c);
66
+ sdk.capture({ distinctId: 'sdk-anonymous', event: 'sdk_pageview', properties: { $set: { source: 'invite' } } });
67
+ sdk.alias({ distinctId: 'sdk-member', alias: 'sdk-anonymous' });
68
+ sdk.identify({ distinctId: 'sdk-member', properties: { email: 'sdk@harbourline.test' } });
69
+ sdk.setPersonProperties({ distinctId: 'sdk-member', properties: { plan: 'trial' }, propertiesOnce: { first_plan: 'trial' } });
70
+ sdk.setPersonProperties({ distinctId: 'sdk-member', properties: { plan: 'paid' }, propertiesOnce: { first_plan: 'paid' } });
71
+ sdk.unsetPersonProperties({ distinctId: 'sdk-member', properties: ['plan'] });
72
+ await sdk.flush();
73
+ await sdk.shutdown();
74
+ const rows = (await read(c, 'persons', '?distinct_id=sdk-member')).results;
75
+ const person = rows[0];
76
+ if (rows.length !== 1 || !person.distinct_ids.includes('sdk-anonymous'))
77
+ return ['the alias did not link the two IDs to one person'];
78
+ return person.properties.source === 'invite' && person.properties.email === 'sdk@harbourline.test'
79
+ && person.properties.first_plan === 'trial' && !('plan' in person.properties) ? [] : ['profile updates lost the anonymous properties, overwrote set_once, or retained the unset property'];
80
+ },
81
+ },
82
+ {
83
+ id: 'transient-retry-preserves-request-and-event-uuid',
84
+ asks: 'a transient503 is retried with the same event UUID and accepted once by the twin',
85
+ source: CORE + ' "return err.status === 408 || err.status === 429 || err.status >= 500"',
86
+ async run(c) {
87
+ const requests = [];
88
+ const forwarded = (async (url, init) => {
89
+ const request = new Request(url, init);
90
+ if (request.method !== 'POST')
91
+ return c.fetch(request);
92
+ requests.push(request.clone());
93
+ if (requests.length === 1)
94
+ return new Response('service unavailable', { status: 503 });
95
+ return c.fetch(request);
96
+ });
97
+ const sdk = await client(c, { fetch: forwarded });
98
+ await sdk.captureImmediate({ distinctId: 'sdk-retry-member', event: 'sdk_retry_capture' });
99
+ await sdk.shutdown();
100
+ const failures = [];
101
+ if (requests.length !== 2)
102
+ failures.push('one transient503 did not cause exactly one retry');
103
+ else {
104
+ const first = new Uint8Array(await requests[0].arrayBuffer()), second = new Uint8Array(await requests[1].arrayBuffer());
105
+ if (first.length !== second.length || first.some((v, i) => v !== second[i]))
106
+ failures.push('the retry changed the prepared payload and event UUID');
107
+ }
108
+ const events = await read(c, 'events', '?event=sdk_retry_capture');
109
+ if (events.results.length !== 1 || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(events.results[0]?.id ?? ''))
110
+ failures.push('the retried capture is not one stored event with a UUID');
111
+ return failures;
112
+ },
113
+ },
114
+ {
115
+ id: 'nonretryable401-is-a-typed-http-error',
116
+ asks: 'capture with a personal key is rejected as401 once; the official client exposes PostHogFetchHttpError and its status',
117
+ source: CORE + ' "class PostHogFetchHttpError extends Error"',
118
+ async run(c) {
119
+ let attempts = 0;
120
+ const sdk = await client(c, { key: c.env.POSTHOG_PERSONAL_API_KEY, fetch: (async (url, init) => { attempts++; return c.fetch(url, init); }) });
121
+ let error;
122
+ try {
123
+ await sdk.captureImmediate({ distinctId: 'sdk-refusal', event: 'sdk_refused_capture' });
124
+ }
125
+ catch (caught) {
126
+ error = caught;
127
+ }
128
+ await sdk.shutdown();
129
+ const typed = error;
130
+ return attempts === 1 && typed?.name === 'PostHogFetchHttpError' && typed.status === 401 ? [] : ['the401 was retried or did not expose the SDK HTTP error and status'];
131
+ },
132
+ },
133
+ {
134
+ id: 'flags-empty-account-through-node-client',
135
+ asks: 'getAllFlags interprets the v2 response with person/group overrides and requested keys for this account with no flags',
136
+ source: 'https://unpkg.com/posthog-node@5.46.1/src/client.ts "Promise that resolves to a record of flag keys and their values"',
137
+ async run(c) {
138
+ const sdk = await client(c);
139
+ const flags = await sdk.getAllFlags('sdk-member', { groups: { organization: 'sdk-harbourline' },
140
+ personProperties: { email: 'sdk@harbourline.test' }, groupProperties: { organization: { name: 'Harbourline SDK case' } }, flagKeys: ['new-booking-flow'] });
141
+ await sdk.shutdown();
142
+ return Object.keys(flags).length === 0 ? [] : ['the empty account returned invented flags'];
143
+ },
144
+ },
145
+ ];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // The vendor's client cases, run in Node as an application runs its SDK: `node run.ts <job.json>`. The job names the
10
+ // vendor, the served port, the host, the issued credentials and the scenario file; each case's failures are printed as
11
+ // one JSON line, `{ id, asks, failures }`.
12
+ import { readFileSync } from 'node:fs';
13
+ import { clientContext, worldProxy } from "./harness.js";
14
+ const job = JSON.parse(readFileSync(process.argv[2], 'utf8'));
15
+ const { cases } = (await import(__rewriteRelativeImportExtension(`./${job.vendor}.ts`)));
16
+ // every connection a case makes goes through the World's proxy, as an application's does in a World
17
+ const context = clientContext(await worldProxy(job.port), job.host, job.env, job.scenarioPath);
18
+ for (const c of cases) {
19
+ const failures = await Promise.race([
20
+ c.run(context).catch((e) => [`the case threw: ${e.message}`]),
21
+ new Promise((r) => setTimeout(() => r(['no outcome in 30 s']), 30_000)),
22
+ ]);
23
+ process.stdout.write(`${JSON.stringify({ id: c.id, asks: c.asks, failures })}\n`);
24
+ }
25
+ process.exit(0);
@@ -0,0 +1,2 @@
1
+ import type { ClientCase } from './harness.js';
2
+ export declare const cases: ClientCase[];