@volter/twin-standard 1.0.30 → 1.0.31
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.
- package/dist/src/archive-source.d.ts +13 -0
- package/dist/src/archive-source.js +176 -0
- package/dist/src/check-sources.d.ts +19 -2
- package/dist/src/check-sources.js +95 -37
- package/dist/src/cli.js +52 -9
- package/dist/src/clients/ai-gateway.d.ts +1 -0
- package/dist/src/clients/ai-gateway.js +3 -0
- package/dist/src/clients/anthropic-messages.d.ts +10 -0
- package/dist/src/clients/anthropic-messages.js +52 -0
- package/dist/src/clients/anthropic.d.ts +1 -0
- package/dist/src/clients/anthropic.js +3 -0
- package/dist/src/clients/aws.d.ts +2 -0
- package/dist/src/clients/aws.js +137 -0
- package/dist/src/clients/cerebras.d.ts +1 -0
- package/dist/src/clients/cerebras.js +3 -0
- package/dist/src/clients/clerk.d.ts +2 -0
- package/dist/src/clients/clerk.js +99 -0
- package/dist/src/clients/cloudflare.d.ts +2 -0
- package/dist/src/clients/cloudflare.js +86 -0
- package/dist/src/clients/elevenlabs.d.ts +2 -0
- package/dist/src/clients/elevenlabs.js +39 -0
- package/dist/src/clients/gemini.d.ts +2 -0
- package/dist/src/clients/gemini.js +92 -0
- package/dist/src/clients/github.d.ts +2 -0
- package/dist/src/clients/github.js +71 -0
- package/dist/src/clients/groq.d.ts +1 -0
- package/dist/src/clients/groq.js +3 -0
- package/dist/src/clients/harness.d.ts +36 -0
- package/dist/src/clients/harness.js +74 -0
- package/dist/src/clients/merge.d.ts +1 -0
- package/dist/src/clients/merge.js +8 -0
- package/dist/src/clients/openai-compatible.d.ts +9 -0
- package/dist/src/clients/openai-compatible.js +51 -0
- package/dist/src/clients/openai.d.ts +2 -0
- package/dist/src/clients/openai.js +65 -0
- package/dist/src/clients/openrouter.d.ts +1 -0
- package/dist/src/clients/openrouter.js +3 -0
- package/dist/src/clients/perplexity.d.ts +1 -0
- package/dist/src/clients/perplexity.js +3 -0
- package/dist/src/clients/planetscale.d.ts +2 -0
- package/dist/src/clients/planetscale.js +109 -0
- package/dist/src/clients/posthog.d.ts +2 -0
- package/dist/src/clients/posthog.js +145 -0
- package/dist/src/clients/run.d.ts +1 -0
- package/dist/src/clients/run.js +25 -0
- package/dist/src/clients/sentry.d.ts +2 -0
- package/dist/src/clients/sentry.js +167 -0
- package/dist/src/clients/slack.d.ts +2 -0
- package/dist/src/clients/slack.js +107 -0
- package/dist/src/clients/stripe.d.ts +2 -0
- package/dist/src/clients/stripe.js +161 -0
- package/dist/src/clients/tinybird.d.ts +2 -0
- package/dist/src/clients/tinybird.js +71 -0
- package/dist/src/clients/tunnel.d.ts +2 -0
- package/dist/src/clients/tunnel.js +99 -0
- package/dist/src/clients/upstash.d.ts +2 -0
- package/dist/src/clients/upstash.js +143 -0
- package/dist/src/clients/volteridentity.d.ts +2 -0
- package/dist/src/clients/volteridentity.js +128 -0
- package/dist/src/clients/xai.d.ts +2 -0
- package/dist/src/clients/xai.js +23 -0
- package/dist/src/conformance.d.ts +33 -0
- package/dist/src/conformance.js +520 -0
- package/dist/src/derive.js +113 -22
- package/dist/src/gate.js +22 -16
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +5 -3
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/p3-rules.js +0 -2
- package/dist/src/protocol-3.d.ts +1 -0
- package/dist/src/protocol-3.js +76 -30
- package/dist/src/published.d.ts +6 -0
- package/dist/src/published.js +65 -11
- package/dist/src/spec-documents.js +2 -1
- package/dist/src/spec-ir-client.d.ts +5 -1
- package/dist/src/spec-ir-client.js +12 -2
- package/dist/src/spec-ir.d.ts +7 -2
- package/dist/src/spec-ir.js +42 -10
- package/dist/src/verdict.d.ts +14 -0
- package/dist/src/verdict.js +30 -0
- package/dist/src/walk/behavior-journey.d.ts +47 -0
- package/dist/src/walk/behavior-journey.js +1138 -0
- package/dist/src/walk/git-client.d.ts +26 -0
- package/dist/src/walk/git-client.js +139 -0
- package/dist/src/walk/journey-kit.d.ts +445 -0
- package/dist/src/walk/journey-kit.js +743 -0
- package/dist/src/walk/shape.d.ts +54 -0
- package/dist/src/walk/shape.js +210 -0
- package/dist/src/walk/sse-example.d.ts +2 -0
- package/dist/src/walk/sse-example.js +32 -0
- package/dist/src/walk/vendor-fetch.d.ts +20 -0
- package/dist/src/walk/vendor-fetch.js +92 -0
- package/dist/src/walk/walk-database.d.ts +12 -0
- package/dist/src/walk/walk-database.js +96 -0
- package/dist/src/walk/xml-view.d.ts +2 -0
- package/dist/src/walk/xml-view.js +55 -0
- package/package.json +39 -3
- package/src/archive-source.ts +141 -0
- package/src/check-sources.ts +83 -33
- package/src/cli.ts +40 -8
- package/src/clients/ai-gateway.ts +4 -0
- package/src/clients/anthropic-messages.ts +60 -0
- package/src/clients/anthropic.ts +4 -0
- package/src/clients/aws.ts +119 -0
- package/src/clients/cerebras.ts +4 -0
- package/src/clients/clerk.ts +97 -0
- package/src/clients/cloudflare.ts +80 -0
- package/src/clients/elevenlabs.ts +38 -0
- package/src/clients/gemini.ts +89 -0
- package/src/clients/github.ts +67 -0
- package/src/clients/groq.ts +4 -0
- package/src/clients/harness.ts +101 -0
- package/src/clients/merge.ts +9 -0
- package/src/clients/openai-compatible.ts +59 -0
- package/src/clients/openai.ts +66 -0
- package/src/clients/openrouter.ts +4 -0
- package/src/clients/perplexity.ts +4 -0
- package/src/clients/planetscale.ts +98 -0
- package/src/clients/posthog.ts +132 -0
- package/src/clients/run.ts +18 -0
- package/src/clients/sentry.ts +157 -0
- package/src/clients/slack.ts +98 -0
- package/src/clients/stripe.ts +129 -0
- package/src/clients/tinybird.ts +69 -0
- package/src/clients/tunnel.ts +79 -0
- package/src/clients/upstash.ts +137 -0
- package/src/clients/volteridentity.ts +108 -0
- package/src/clients/xai.ts +25 -0
- package/src/conformance.ts +446 -0
- package/src/derive.ts +87 -16
- package/src/gate.ts +20 -14
- package/src/index.ts +5 -3
- package/src/lanes.ts +7 -0
- package/src/p3-rules.ts +0 -2
- package/src/protocol-3.ts +71 -27
- package/src/published.ts +59 -11
- package/src/spec-documents.ts +2 -1
- package/src/spec-ir-client.ts +15 -4
- package/src/spec-ir.ts +43 -5
- package/src/verdict.ts +28 -0
- package/src/walk/behavior-journey.ts +806 -0
- package/src/walk/git-client.ts +138 -0
- package/src/walk/journey-kit.ts +858 -0
- package/src/walk/shape.ts +208 -0
- package/src/walk/sse-example.ts +25 -0
- package/src/walk/vendor-fetch.ts +80 -0
- package/src/walk/walk-database.ts +97 -0
- package/src/walk/xml-view.ts +44 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const PUT = 'https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html';
|
|
2
|
+
const COMPLETE = 'https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html';
|
|
3
|
+
const SECRET = 'https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html';
|
|
4
|
+
const options = (c) => ({
|
|
5
|
+
region: 'us-east-1',
|
|
6
|
+
credentials: { accessKeyId: c.env.AWS_ACCESS_KEY_ID ?? '', secretAccessKey: c.env.AWS_SECRET_ACCESS_KEY ?? '' },
|
|
7
|
+
requestHandler: { httpsAgent: c.agent, requestTimeout: 10_000 },
|
|
8
|
+
maxAttempts: 1,
|
|
9
|
+
});
|
|
10
|
+
const failed = (what) => (e) => new Error(`${what} failed: ${e.name}: ${e.message}`);
|
|
11
|
+
export const cases = [
|
|
12
|
+
{
|
|
13
|
+
id: 's3-object-round-trip',
|
|
14
|
+
asks: 'an object put with its type and metadata into a bucket the client made is found by HeadObject with them, listed under its prefix, and read back byte for byte, the client checking the CRC32 it sent',
|
|
15
|
+
source: `${PUT} "Adds an object to a bucket."`,
|
|
16
|
+
async run(c) {
|
|
17
|
+
const s3 = await import('@aws-sdk/client-s3');
|
|
18
|
+
const client = new s3.S3Client(options(c));
|
|
19
|
+
const body = new TextEncoder().encode('{"madeAt":"2026-01-06T02:00:00.000Z","files":{}}');
|
|
20
|
+
try {
|
|
21
|
+
await client.send(new s3.CreateBucketCommand({ Bucket: 'client-case-backups' })).catch((e) => { throw failed('CreateBucket')(e); });
|
|
22
|
+
await client.send(new s3.PutObjectCommand({ Bucket: 'client-case-backups', Key: 'platform-state/one.json', Body: body, ContentType: 'application/json', Metadata: { source: 'client-case' } }))
|
|
23
|
+
.catch((e) => { throw failed('PutObject')(e); });
|
|
24
|
+
const out = [];
|
|
25
|
+
const head = await client.send(new s3.HeadObjectCommand({ Bucket: 'client-case-backups', Key: 'platform-state/one.json' })).catch((e) => { throw failed('HeadObject')(e); });
|
|
26
|
+
if (head.ContentType !== 'application/json')
|
|
27
|
+
out.push(`HeadObject answered the type ${String(head.ContentType)}, not the one put`);
|
|
28
|
+
if (head.Metadata?.source !== 'client-case')
|
|
29
|
+
out.push('HeadObject answered no x-amz-meta-source');
|
|
30
|
+
if (head.ContentLength !== body.length)
|
|
31
|
+
out.push(`HeadObject answered ${String(head.ContentLength)} bytes, not ${body.length}`);
|
|
32
|
+
const listed = await client.send(new s3.ListObjectsV2Command({ Bucket: 'client-case-backups', Prefix: 'platform-state/' })).catch((e) => { throw failed('ListObjectsV2')(e); });
|
|
33
|
+
if (!(listed.Contents ?? []).some((o) => o.Key === 'platform-state/one.json'))
|
|
34
|
+
out.push('ListObjectsV2 does not list the object put under its prefix');
|
|
35
|
+
const got = await client.send(new s3.GetObjectCommand({ Bucket: 'client-case-backups', Key: 'platform-state/one.json' })).catch((e) => { throw failed('GetObject')(e); });
|
|
36
|
+
const back = await got.Body.transformToByteArray().catch((e) => { throw failed('reading GetObject\'s body')(e); });
|
|
37
|
+
if (back.length !== body.length || back.some((b, i) => b !== body[i]))
|
|
38
|
+
out.push(`GetObject answered ${back.length} bytes, not the ${body.length} put`);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
return [e.message];
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 's3-multipart-upload',
|
|
48
|
+
asks: 'an upload of two parts, the first 5 MiB, completes into one object whose ETag names its two parts and whose bytes are the parts in order',
|
|
49
|
+
source: `${COMPLETE} "Completes a multipart upload by assembling previously uploaded parts."`,
|
|
50
|
+
async run(c) {
|
|
51
|
+
const s3 = await import('@aws-sdk/client-s3');
|
|
52
|
+
const client = new s3.S3Client(options(c));
|
|
53
|
+
const first = new Uint8Array(5 * 1024 * 1024).fill(97);
|
|
54
|
+
const second = new TextEncoder().encode('the recording\'s last bytes');
|
|
55
|
+
try {
|
|
56
|
+
await client.send(new s3.CreateBucketCommand({ Bucket: 'client-case-recordings' })).catch((e) => { throw failed('CreateBucket')(e); });
|
|
57
|
+
const made = await client.send(new s3.CreateMultipartUploadCommand({ Bucket: 'client-case-recordings', Key: 'calls/call.m4a', ContentType: 'audio/mp4' }))
|
|
58
|
+
.catch((e) => { throw failed('CreateMultipartUpload')(e); });
|
|
59
|
+
const UploadId = made.UploadId;
|
|
60
|
+
if (!UploadId)
|
|
61
|
+
return ['CreateMultipartUpload answered no UploadId'];
|
|
62
|
+
const one = await client.send(new s3.UploadPartCommand({ Bucket: 'client-case-recordings', Key: 'calls/call.m4a', UploadId, PartNumber: 1, Body: first })).catch((e) => { throw failed('UploadPart 1')(e); });
|
|
63
|
+
const two = await client.send(new s3.UploadPartCommand({ Bucket: 'client-case-recordings', Key: 'calls/call.m4a', UploadId, PartNumber: 2, Body: second })).catch((e) => { throw failed('UploadPart 2')(e); });
|
|
64
|
+
const done = await client.send(new s3.CompleteMultipartUploadCommand({ Bucket: 'client-case-recordings', Key: 'calls/call.m4a', UploadId,
|
|
65
|
+
MultipartUpload: { Parts: [{ PartNumber: 1, ETag: one.ETag }, { PartNumber: 2, ETag: two.ETag }] } })).catch((e) => { throw failed('CompleteMultipartUpload')(e); });
|
|
66
|
+
const out = [];
|
|
67
|
+
if (!/-2"$/.test(String(done.ETag)))
|
|
68
|
+
out.push(`the completed object's ETag ${String(done.ETag)} does not name its two parts`);
|
|
69
|
+
const got = await client.send(new s3.GetObjectCommand({ Bucket: 'client-case-recordings', Key: 'calls/call.m4a' })).catch((e) => { throw failed('GetObject')(e); });
|
|
70
|
+
const back = await got.Body.transformToByteArray();
|
|
71
|
+
if (back.length !== first.length + second.length || back[0] !== 97 || back[back.length - 1] !== second[second.length - 1])
|
|
72
|
+
out.push(`GetObject answered ${back.length} bytes, not the parts joined (${first.length + second.length})`);
|
|
73
|
+
if (got.ContentType !== 'audio/mp4')
|
|
74
|
+
out.push(`the object's type is ${String(got.ContentType)}, not the one its upload began with`);
|
|
75
|
+
const left = await client.send(new s3.ListMultipartUploadsCommand({ Bucket: 'client-case-recordings' })).catch((e) => { throw failed('ListMultipartUploads')(e); });
|
|
76
|
+
if ((left.Uploads ?? []).some((u) => u.UploadId === UploadId))
|
|
77
|
+
out.push('the completed upload is still listed in progress');
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return [e.message];
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'secrets-manager-create-and-read',
|
|
87
|
+
asks: 'a secret stored with its value is read back as the current version, labelled AWSCURRENT',
|
|
88
|
+
source: `${SECRET} "Retrieves the contents of the encrypted fields SecretString or SecretBinary from the specified version of a secret"`,
|
|
89
|
+
async run(c) {
|
|
90
|
+
const sm = await import('@aws-sdk/client-secrets-manager');
|
|
91
|
+
const client = new sm.SecretsManagerClient(options(c));
|
|
92
|
+
const SecretString = '{"username":"api","password":"client-case-password"}';
|
|
93
|
+
try {
|
|
94
|
+
const made = await client.send(new sm.CreateSecretCommand({ Name: 'client-case/db', SecretString })).catch((e) => { throw failed('CreateSecret')(e); });
|
|
95
|
+
const got = await client.send(new sm.GetSecretValueCommand({ SecretId: 'client-case/db' })).catch((e) => { throw failed('GetSecretValue')(e); });
|
|
96
|
+
const out = [];
|
|
97
|
+
if (got.SecretString !== SecretString)
|
|
98
|
+
out.push('GetSecretValue answered another value than the one stored');
|
|
99
|
+
if (got.VersionId !== made.VersionId)
|
|
100
|
+
out.push(`GetSecretValue answered version ${String(got.VersionId)}, not the ${String(made.VersionId)} CreateSecret made`);
|
|
101
|
+
if (!(got.VersionStages ?? []).includes('AWSCURRENT'))
|
|
102
|
+
out.push('the version read is not labelled AWSCURRENT');
|
|
103
|
+
if (!String(made.ARN).startsWith('arn:aws:secretsmanager:us-east-1:'))
|
|
104
|
+
out.push(`CreateSecret answered the ARN ${String(made.ARN)}, not one in us-east-1, the Region it was called in`);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return [e.message];
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 's3-backup-bucket-created-when-missing',
|
|
114
|
+
asks: 'HeadBucket rejects with HTTP 404 for a missing backup bucket; the backup job catches it and creates the bucket, which then answers HTTP 200',
|
|
115
|
+
source: 'https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html "The specified bucket does not exist."',
|
|
116
|
+
async run(c) {
|
|
117
|
+
const s3 = await import('@aws-sdk/client-s3');
|
|
118
|
+
const client = new s3.S3Client({ ...options(c), forcePathStyle: true });
|
|
119
|
+
const Bucket = 'client-case-backup-new-bucket';
|
|
120
|
+
try {
|
|
121
|
+
const missing = await client.send(new s3.HeadBucketCommand({ Bucket })).then(() => undefined, (e) => e);
|
|
122
|
+
if (missing?.$metadata?.httpStatusCode !== 404)
|
|
123
|
+
return ['HeadBucket did not reject the missing bucket with HTTP 404'];
|
|
124
|
+
// apps/platform/src/backup.ts catches the SDK rejection before creating the bucket.
|
|
125
|
+
await client.send(new s3.CreateBucketCommand({ Bucket }));
|
|
126
|
+
const found = await client.send(new s3.HeadBucketCommand({ Bucket }));
|
|
127
|
+
return found.$metadata.httpStatusCode === 200 ? [] : ['HeadBucket did not find the newly created backup bucket'];
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return [e.message];
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
client.destroy();
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const cases: import("./harness.js").ClientCase[];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const ORG_MEMBERSHIPS = 'https://clerk.com/docs/references/backend/organization/get-organization-membership-list';
|
|
2
|
+
const USER_MEMBERSHIPS = 'https://clerk.com/docs/references/backend/user/get-organization-membership-list';
|
|
3
|
+
const OVERVIEW = 'https://clerk.com/docs/references/backend/overview';
|
|
4
|
+
const VERIFY = 'https://clerk.com/docs/references/backend/verify-token';
|
|
5
|
+
async function sdk(c) {
|
|
6
|
+
globalThis.fetch = c.fetch;
|
|
7
|
+
return await import('@clerk/backend');
|
|
8
|
+
}
|
|
9
|
+
async function client(c) {
|
|
10
|
+
const { createClerkClient } = await sdk(c);
|
|
11
|
+
return createClerkClient({ secretKey: c.env.CLERK_SECRET_KEY });
|
|
12
|
+
}
|
|
13
|
+
/** A person and an organization they created (Organizations turned on first, as an application's instance has them). */
|
|
14
|
+
async function founded(c, who) {
|
|
15
|
+
const clerk = await client(c);
|
|
16
|
+
await clerk.instance.updateOrganizationSettings({ enabled: true });
|
|
17
|
+
const user = await clerk.users.createUser({ emailAddress: [`${who}@sdk-case.example`], firstName: who, skipPasswordRequirement: true });
|
|
18
|
+
const org = await clerk.organizations.createOrganization({ name: `${who}'s team`, createdBy: user.id });
|
|
19
|
+
return { clerk, user, org };
|
|
20
|
+
}
|
|
21
|
+
export const cases = [
|
|
22
|
+
{
|
|
23
|
+
id: 'organization-membership-list-paginated',
|
|
24
|
+
asks: "the organization's memberships come back as { data, totalCount }, each membership's public user data and creation time read as the SDK's objects",
|
|
25
|
+
source: `${ORG_MEMBERSHIPS} "Returns a PaginatedResourceResponse object with a data property containing an array of OrganizationMembership objects and a totalCount property"`,
|
|
26
|
+
async run(c) {
|
|
27
|
+
const { clerk, user, org } = await founded(c, 'marta');
|
|
28
|
+
const { data, totalCount } = await clerk.organizations.getOrganizationMembershipList({ organizationId: org.id, limit: 100 });
|
|
29
|
+
const failures = [];
|
|
30
|
+
if (totalCount !== 1 || data.length !== 1)
|
|
31
|
+
failures.push(`the creator's organization lists ${data.length} memberships of ${totalCount}, not its one`);
|
|
32
|
+
const m = data[0];
|
|
33
|
+
if (m?.publicUserData?.userId !== user.id)
|
|
34
|
+
failures.push('the membership names no publicUserData.userId of its member');
|
|
35
|
+
if (m?.publicUserData?.identifier !== 'marta@sdk-case.example')
|
|
36
|
+
failures.push('the membership lost its member\'s identifier');
|
|
37
|
+
if (typeof m?.createdAt !== 'number' || Number.isNaN(new Date(m.createdAt).getTime()))
|
|
38
|
+
failures.push('the membership\'s createdAt is no time');
|
|
39
|
+
return failures;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'user-membership-list-names-organization-and-permissions',
|
|
44
|
+
asks: "a user's memberships come back as { data, totalCount }, each naming its organization and the permissions its role grants",
|
|
45
|
+
source: `${USER_MEMBERSHIPS} "Returns a PaginatedResourceResponse object with a data property that contains an array of OrganizationMembership objects"`,
|
|
46
|
+
async run(c) {
|
|
47
|
+
const { clerk, user, org } = await founded(c, 'rui');
|
|
48
|
+
const { data, totalCount } = await clerk.users.getOrganizationMembershipList({ userId: user.id, limit: 100 });
|
|
49
|
+
const failures = [];
|
|
50
|
+
const m = data.find((x) => x.organization?.id === org.id);
|
|
51
|
+
if (totalCount !== 1 || !m)
|
|
52
|
+
failures.push(`the creator's memberships (${totalCount}) do not name the organization made`);
|
|
53
|
+
else if (!Array.isArray(m.permissions) || !m.permissions.length)
|
|
54
|
+
failures.push(`the creator's membership (${m.role}) carries no permissions`);
|
|
55
|
+
return failures;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'api-error-carries-status-and-code',
|
|
60
|
+
asks: "a refused call throws ClerkAPIResponseError carrying the HTTP status and the errors Clerk answered: an unknown user's memberships are a 404, which runhuman's isMember reads as no membership (packages/api/src/services/clerk/clerk-org.api.ts:51-63)",
|
|
61
|
+
source: `${OVERVIEW} "The clerkClient methods throw errors ( ClerkAPIResponseError ) when something goes wrong."`,
|
|
62
|
+
async run(c) {
|
|
63
|
+
const clerk = await client(c);
|
|
64
|
+
await clerk.instance.updateOrganizationSettings({ enabled: true });
|
|
65
|
+
try {
|
|
66
|
+
await clerk.users.getOrganizationMembershipList({ userId: 'user_2missing0000000000000000', limit: 100 });
|
|
67
|
+
return ["an unknown user's memberships were answered"];
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
const error = e;
|
|
71
|
+
const failures = [];
|
|
72
|
+
if (error.status !== 404)
|
|
73
|
+
failures.push(`the error's status is ${error.status}, not 404`);
|
|
74
|
+
if (error.errors?.[0]?.code !== 'resource_not_found')
|
|
75
|
+
failures.push(`the error's first code is ${error.errors?.[0]?.code}, not resource_not_found`);
|
|
76
|
+
return failures;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'session-token-verified-against-jwks',
|
|
82
|
+
asks: "a session's token verifies with the secret key alone, the SDK fetching the instance's JWKS from the Backend API",
|
|
83
|
+
source: `${VERIFY} "Otherwise, performs a network call to retrieve the JWKS from the Backend API"`,
|
|
84
|
+
async run(c) {
|
|
85
|
+
const { verifyToken } = await sdk(c);
|
|
86
|
+
const clerk = await client(c);
|
|
87
|
+
const user = await clerk.users.createUser({ emailAddress: ['ines@sdk-case.example'], skipPasswordRequirement: true });
|
|
88
|
+
const session = await clerk.sessions.createSession({ userId: user.id });
|
|
89
|
+
const { jwt } = await clerk.sessions.getToken(session.id);
|
|
90
|
+
const payload = await verifyToken(jwt, { secretKey: c.env.CLERK_SECRET_KEY });
|
|
91
|
+
const failures = [];
|
|
92
|
+
if (payload.sub !== user.id)
|
|
93
|
+
failures.push(`the verified token's sub is ${String(payload.sub)}, not the user`);
|
|
94
|
+
if (payload.sid !== session.id)
|
|
95
|
+
failures.push(`the verified token's sid is ${String(payload.sid)}, not the session`);
|
|
96
|
+
return failures;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const HOSTNAMES = 'https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/';
|
|
2
|
+
const WIDGETS = 'https://developers.cloudflare.com/turnstile/get-started/widget-management/api/';
|
|
3
|
+
const AWS4FETCH = 'https://developers.cloudflare.com/r2/examples/aws/aws4fetch/';
|
|
4
|
+
async function cloudflare(c) {
|
|
5
|
+
const { default: Cloudflare } = await import('cloudflare');
|
|
6
|
+
return new Cloudflare({ apiToken: c.env.CLOUDFLARE_API_TOKEN ?? '', fetch: c.fetch, maxRetries: 0, timeout: 10_000 });
|
|
7
|
+
}
|
|
8
|
+
const failed = (what) => (e) => new Error(`${what} failed: ${e.message}`);
|
|
9
|
+
export const cases = [
|
|
10
|
+
{
|
|
11
|
+
id: 'custom-hostname-pending-and-found',
|
|
12
|
+
asks: 'a custom hostname registered on a zone is pending, with the ownership verification its customer publishes, and is found again by its hostname',
|
|
13
|
+
source: `${HOSTNAMES} "validate hostname ownership and affect the custom hostname"`,
|
|
14
|
+
async run(c) {
|
|
15
|
+
const cf = await cloudflare(c);
|
|
16
|
+
const account = c.env.CLOUDFLARE_ACCOUNT_ID ?? '';
|
|
17
|
+
try {
|
|
18
|
+
const zone = await cf.zones.create({ account: { id: account }, name: 'client-case.example', type: 'full' }).catch((e) => { throw failed('zones.create')(e); });
|
|
19
|
+
const made = await cf.customHostnames.create({ zone_id: zone.id, hostname: 'shop.customer-case.example', ssl: { method: 'txt', type: 'dv' } })
|
|
20
|
+
.catch((e) => { throw failed('customHostnames.create')(e); });
|
|
21
|
+
const out = [];
|
|
22
|
+
if (made.status !== 'pending')
|
|
23
|
+
out.push(`a new custom hostname is ${String(made.status)}, not pending`);
|
|
24
|
+
if (!made.ownership_verification?.name || !made.ownership_verification?.value)
|
|
25
|
+
out.push('a new custom hostname names no ownership verification record');
|
|
26
|
+
const page = await cf.customHostnames.list({ zone_id: zone.id, hostname: 'shop.customer-case.example' }).catch((e) => { throw failed('customHostnames.list')(e); });
|
|
27
|
+
const found = page.getPaginatedItems();
|
|
28
|
+
if (found.length !== 1 || found[0]?.id !== made.id)
|
|
29
|
+
out.push(`the hostname filter found ${found.length} hostname(s), not the one made`);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return [e.message];
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'turnstile-widget-secret-at-detail',
|
|
39
|
+
asks: "a widget made through the API answers its sitekey and secret, and its detail answers the same secret",
|
|
40
|
+
source: `${WIDGETS} "Turnstile Widget Details"`,
|
|
41
|
+
async run(c) {
|
|
42
|
+
const cf = await cloudflare(c);
|
|
43
|
+
const account_id = c.env.CLOUDFLARE_ACCOUNT_ID ?? '';
|
|
44
|
+
try {
|
|
45
|
+
const widget = await cf.turnstile.widgets.create({ account_id, domains: ['client-case.example'], mode: 'managed', name: 'client case sign-up' })
|
|
46
|
+
.catch((e) => { throw failed('turnstile.widgets.create')(e); });
|
|
47
|
+
if (!widget.sitekey || !widget.secret)
|
|
48
|
+
return ['the new widget answers no sitekey or no secret'];
|
|
49
|
+
const detail = await cf.turnstile.widgets.get(widget.sitekey, { account_id }).catch((e) => { throw failed('turnstile.widgets.get')(e); });
|
|
50
|
+
return detail.secret === widget.secret ? [] : ['the widget\'s detail answers another secret than its creation'];
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return [e.message];
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'r2-object-round-trip-aws4fetch',
|
|
59
|
+
asks: "an object put through aws4fetch at the account's R2 endpoint is listed by ListObjectsV2 and read back byte for byte",
|
|
60
|
+
source: `${AWS4FETCH} "ListObjectsV2Result"`,
|
|
61
|
+
async run(c) {
|
|
62
|
+
const { AwsClient } = await import('aws4fetch');
|
|
63
|
+
const r2 = new AwsClient({ accessKeyId: c.env.R2_ACCESS_KEY_ID ?? '', secretAccessKey: c.env.R2_SECRET_ACCESS_KEY ?? '', service: 's3', region: 'auto' });
|
|
64
|
+
const base = c.env.R2_ENDPOINT ?? '';
|
|
65
|
+
const send = async (url, init) => c.fetch(await r2.sign(url, init));
|
|
66
|
+
const bytes = new TextEncoder().encode('a recording, as RH2 imports it\n');
|
|
67
|
+
const made = await send(`${base}/client-case-recordings`, { method: 'PUT' });
|
|
68
|
+
if (!made.ok)
|
|
69
|
+
return [`CreateBucket answered ${made.status}`];
|
|
70
|
+
const put = await send(`${base}/client-case-recordings/rooms/r1.txt`, { method: 'PUT', body: bytes, headers: { 'content-type': 'text/plain' } });
|
|
71
|
+
if (!put.ok)
|
|
72
|
+
return [`PutObject answered ${put.status}`];
|
|
73
|
+
const out = [];
|
|
74
|
+
const listed = await (await send(`${base}/client-case-recordings?list-type=2`)).text();
|
|
75
|
+
if (!listed.includes('<Key>rooms/r1.txt</Key>'))
|
|
76
|
+
out.push('ListObjectsV2 does not list the object put');
|
|
77
|
+
const got = await send(`${base}/client-case-recordings/rooms/r1.txt`);
|
|
78
|
+
const back = new Uint8Array(await got.arrayBuffer());
|
|
79
|
+
if (got.status !== 200 || back.length !== bytes.length || back.some((b, i) => b !== bytes[i]))
|
|
80
|
+
out.push(`GetObject answered ${got.status} with ${back.length} bytes, not the ${bytes.length} put`);
|
|
81
|
+
if (got.headers.get('content-type') !== 'text/plain')
|
|
82
|
+
out.push(`GetObject answered the type ${String(got.headers.get('content-type'))}, not the one put`);
|
|
83
|
+
return out;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const SOCKET = 'https://elevenlabs.io/docs/agents-platform/api-reference/agents-platform/websocket';
|
|
2
|
+
async function client(c) {
|
|
3
|
+
const { ElevenLabsClient } = await import('@elevenlabs/elevenlabs-js');
|
|
4
|
+
return new ElevenLabsClient({ apiKey: c.env.ELEVENLABS_API_KEY ?? '', baseUrl: `https://${c.host}`, fetch: c.fetch, maxRetries: 0, timeoutInSeconds: 10 });
|
|
5
|
+
}
|
|
6
|
+
export const cases = [
|
|
7
|
+
{
|
|
8
|
+
id: 'agent-conversation-opens',
|
|
9
|
+
asks: "an agent's conversation opened at its signed URL answers the client's initiation data with conversation_initiation_metadata",
|
|
10
|
+
source: `${SOCKET} "conversation_initiation_client_data"`,
|
|
11
|
+
async run(c) {
|
|
12
|
+
const el = await client(c);
|
|
13
|
+
const agent = await el.conversationalAi.agents.create({ name: 'Conformance', conversationConfig: { agent: { firstMessage: 'Hello.', prompt: { prompt: 'Answer briefly.' } } } }).catch((e) => e);
|
|
14
|
+
if (agent instanceof Error)
|
|
15
|
+
return [`agents.create failed: ${agent.message}`];
|
|
16
|
+
const signed = await el.conversationalAi.conversations.getSignedUrl({ agentId: agent.agentId }).catch((e) => e);
|
|
17
|
+
if (signed instanceof Error)
|
|
18
|
+
return [`conversations.getSignedUrl failed: ${signed.message}`];
|
|
19
|
+
const url = signed.signedUrl;
|
|
20
|
+
if (!url.startsWith('wss://'))
|
|
21
|
+
return [`the signed URL is not ElevenLabs' secure socket: ${url.slice(0, 60)}`];
|
|
22
|
+
const { WebSocket } = await import('ws');
|
|
23
|
+
const first = await new Promise((done) => {
|
|
24
|
+
const ws = new WebSocket(url, { createConnection: c.connect });
|
|
25
|
+
const timer = setTimeout(() => { ws.close(); done('no event in 10 s'); }, 10_000);
|
|
26
|
+
// the client's first frame, as ElevenLabs' SDKs send it: its initiation data
|
|
27
|
+
ws.on('open', () => ws.send(JSON.stringify({ type: 'conversation_initiation_client_data' })));
|
|
28
|
+
ws.on('message', (d) => { clearTimeout(timer); ws.close(); try {
|
|
29
|
+
done(String(JSON.parse(String(d)).type));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
done(`a frame that is not JSON: ${String(d).slice(0, 40)}`);
|
|
33
|
+
} });
|
|
34
|
+
ws.on('error', (e) => { clearTimeout(timer); done(`the socket failed: ${e.message}`); });
|
|
35
|
+
});
|
|
36
|
+
return first === 'conversation_initiation_metadata' ? [] : [`the conversation's first event was ${first}`];
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const REFERENCE = 'https://ai.google.dev/api/generate-content';
|
|
2
|
+
const MODEL = 'gemini-3.8-flash';
|
|
3
|
+
const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZlSAAAAAASUVORK5CYII=';
|
|
4
|
+
async function routed(c, run) {
|
|
5
|
+
const previous = globalThis.fetch;
|
|
6
|
+
globalThis.fetch = c.fetch;
|
|
7
|
+
try {
|
|
8
|
+
return await run();
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
globalThis.fetch = previous;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function chat(c, model = MODEL) {
|
|
15
|
+
const { GoogleGenerativeAI } = await import('@google/generative-ai');
|
|
16
|
+
return new GoogleGenerativeAI(c.env.GOOGLE_API_KEY ?? '').getGenerativeModel({ model }, { baseUrl: `https://${c.host}` });
|
|
17
|
+
}
|
|
18
|
+
export const cases = [
|
|
19
|
+
{
|
|
20
|
+
id: 'generative-ai-stream-aggregate',
|
|
21
|
+
asks: 'the chat SDK decodes SSE chunks and aggregates their text into the final response',
|
|
22
|
+
source: `${REFERENCE} "a stream of GenerateContentResponse instances"`,
|
|
23
|
+
async run(c) {
|
|
24
|
+
return routed(c, async () => {
|
|
25
|
+
c.scenario({ handlers: [{ id: 'words', on: { userTextIncludes: 'workshop' }, respond: { text: 'A scripted workshop reply.' } }] });
|
|
26
|
+
const result = await (await chat(c)).generateContentStream('Plan the workshop.');
|
|
27
|
+
let text = '';
|
|
28
|
+
for await (const chunk of result.stream)
|
|
29
|
+
text += chunk.text();
|
|
30
|
+
const response = await result.response;
|
|
31
|
+
return text === 'A scripted workshop reply.' && response.text() === text && response.candidates?.[0]?.finishReason === 'STOP'
|
|
32
|
+
? [] : ['SSE text, aggregate response or final finish reason did not survive the SDK'];
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'generative-ai-function-continuation',
|
|
38
|
+
asks: 'the SDK exposes function calls and sends their result as the next chat turn',
|
|
39
|
+
source: `${REFERENCE} "Function Calling"`,
|
|
40
|
+
async run(c) {
|
|
41
|
+
return routed(c, async () => {
|
|
42
|
+
const { SchemaType } = await import('@google/generative-ai');
|
|
43
|
+
c.scenario({ handlers: [
|
|
44
|
+
{ id: 'returned', on: { toolResultFor: 'lookup_company', lastMessageIsToolResult: true }, respond: { text: 'Tidewater renewal is in October.' } },
|
|
45
|
+
{ id: 'lookup', on: { hasTool: 'lookup_company' }, respond: { functionCalls: [{ name: 'lookup_company', args: { company: 'Tidewater' } }] } },
|
|
46
|
+
] });
|
|
47
|
+
const session = (await chat(c)).startChat({ tools: [{ functionDeclarations: [{ name: 'lookup_company', parameters: { type: SchemaType.OBJECT, properties: { company: { type: SchemaType.STRING } }, required: ['company'] } }] }] });
|
|
48
|
+
const first = await session.sendMessage('Find the company renewal.');
|
|
49
|
+
const call = first.response.functionCalls()?.[0];
|
|
50
|
+
if (call?.name !== 'lookup_company' || call.args.company !== 'Tidewater')
|
|
51
|
+
return ['the SDK lost the function name or arguments'];
|
|
52
|
+
const next = await session.sendMessage([{ functionResponse: { name: call.name, response: { renewal: 'October' } } }]);
|
|
53
|
+
return next.response.text() === 'Tidewater renewal is in October.' ? [] : ['the SDK chat history/function response failed to continue the turn'];
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'generative-ai-typed-refusal',
|
|
59
|
+
asks: 'a missing model becomes the chat SDK HTTP error with its status and Google error details',
|
|
60
|
+
source: 'https://cloud.google.com/apis/design/errors "NOT_FOUND"',
|
|
61
|
+
async run(c) {
|
|
62
|
+
return routed(c, async () => {
|
|
63
|
+
const { GoogleGenerativeAIFetchError } = await import('@google/generative-ai');
|
|
64
|
+
try {
|
|
65
|
+
await (await chat(c, 'missing-model')).generateContent('Hello');
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return error instanceof GoogleGenerativeAIFetchError && error.status === 404 ? [] : ['missing model was not the SDK 404 error'];
|
|
69
|
+
}
|
|
70
|
+
return ['missing model unexpectedly generated content'];
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'genai-image-parts',
|
|
76
|
+
asks: 'the image SDK reads generated text and inline image bytes from response parts',
|
|
77
|
+
source: `${REFERENCE} "inlineData"`,
|
|
78
|
+
async run(c) {
|
|
79
|
+
return routed(c, async () => {
|
|
80
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
81
|
+
c.scenario({ handlers: [{ id: 'picture', on: { userTextIncludes: 'square illustration' }, respond: {
|
|
82
|
+
text: 'A synthetic square illustration.', parts: [{ inlineData: { mimeType: 'image/png', data: PNG } }],
|
|
83
|
+
} }] });
|
|
84
|
+
const ai = new GoogleGenAI({ apiKey: c.env.GEMINI_API_KEY ?? '', httpOptions: { baseUrl: `https://${c.host}` } });
|
|
85
|
+
const response = await ai.models.generateContent({ model: 'gemini-3.1-flash-image', contents: 'Make a square illustration.', config: { responseModalities: ['TEXT', 'IMAGE'] } });
|
|
86
|
+
const image = response.candidates?.[0]?.content?.parts?.find((p) => p.inlineData)?.inlineData;
|
|
87
|
+
return response.text === 'A synthetic square illustration.' && image?.mimeType === 'image/png' && image.data === PNG
|
|
88
|
+
? [] : ['the image SDK lost text, MIME type or fixture bytes'];
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const SCRIPTING = 'https://docs.github.com/en/rest/guides/scripting-with-the-rest-api-and-javascript';
|
|
2
|
+
const ISSUES = 'https://docs.github.com/en/rest/issues/issues';
|
|
3
|
+
const COMMENTS = 'https://docs.github.com/en/rest/issues/comments';
|
|
4
|
+
async function octokit(c) {
|
|
5
|
+
const { Octokit } = await import('@octokit/rest');
|
|
6
|
+
return new Octokit({ auth: c.env.GITHUB_TOKEN ?? '', request: { fetch: c.fetch } });
|
|
7
|
+
}
|
|
8
|
+
/** A public repository of the token's own account, made as `gh repo create` makes one (GraphQL's createRepository). */
|
|
9
|
+
async function repository(client, name) {
|
|
10
|
+
const made = await client.graphql('mutation($name: String!) { createRepository(input: { name: $name, visibility: PUBLIC }) { repository { nameWithOwner } } }', { name });
|
|
11
|
+
const [owner, repo] = made.createRepository.repository.nameWithOwner.split('/');
|
|
12
|
+
return { owner, repo };
|
|
13
|
+
}
|
|
14
|
+
export const cases = [
|
|
15
|
+
{
|
|
16
|
+
id: 'on-call-triage',
|
|
17
|
+
asks: 'the on-call recipe reads a new issue by its number and answers on it, through Octokit: the issue as filed, the comment as written, and the issue counting it',
|
|
18
|
+
source: `${ISSUES} "Any user with pull access to a repository can create an issue."; ${COMMENTS} "You can use the REST API to list comments on issues and pull requests. Every pull request is an issue, but not every issue is a pull request."`,
|
|
19
|
+
async run(c) {
|
|
20
|
+
try {
|
|
21
|
+
const client = await octokit(c);
|
|
22
|
+
const { owner, repo } = await repository(client, 'storefront');
|
|
23
|
+
const filed = (await client.rest.issues.create({ owner, repo, title: 'Checkout fails on Safari', body: 'The pay button does nothing on Safari 18.' })).data;
|
|
24
|
+
const issue = (await client.rest.issues.get({ owner, repo, issue_number: filed.number })).data;
|
|
25
|
+
const out = [];
|
|
26
|
+
if (issue.number !== filed.number)
|
|
27
|
+
out.push(`issues.get answered issue ${issue.number}, not ${filed.number}`);
|
|
28
|
+
if (issue.title !== 'Checkout fails on Safari')
|
|
29
|
+
out.push(`the issue's title is ${JSON.stringify(issue.title)}`);
|
|
30
|
+
if (issue.state !== 'open')
|
|
31
|
+
out.push(`the issue is ${issue.state}, not open`);
|
|
32
|
+
const body = 'Tracked as OPS-1. A bug: the checkout button on Safari.';
|
|
33
|
+
const comment = (await client.rest.issues.createComment({ owner, repo, issue_number: issue.number, body })).data;
|
|
34
|
+
if (comment.body !== body)
|
|
35
|
+
out.push(`the comment's body is ${JSON.stringify(comment.body)}`);
|
|
36
|
+
if (comment.user?.login !== owner)
|
|
37
|
+
out.push(`the comment is ${String(comment.user?.login)}'s, not the token's own ${owner}`);
|
|
38
|
+
const after = (await client.rest.issues.get({ owner, repo, issue_number: issue.number })).data;
|
|
39
|
+
if (after.comments !== 1)
|
|
40
|
+
out.push(`the issue counts ${after.comments} comments, not the one made`);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return [`the recipe's calls failed: ${e.name}: ${e.message}`];
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'missing-issue-raises',
|
|
50
|
+
asks: "reading an issue the repository does not have raises Octokit's HttpError with the response's status, 404",
|
|
51
|
+
source: `${SCRIPTING} "If an API error occurs even after retries, Octokit.js throws an error that includes the HTTP status code of the response ( response.status ) and the response headers ( response.headers )."`,
|
|
52
|
+
async run(c) {
|
|
53
|
+
try {
|
|
54
|
+
const client = await octokit(c);
|
|
55
|
+
const { owner, repo } = await repository(client, 'pager');
|
|
56
|
+
const raised = await client.rest.issues.get({ owner, repo, issue_number: 4242 }).then(() => undefined, (e) => e);
|
|
57
|
+
if (!raised)
|
|
58
|
+
return ['reading an issue the repository does not have answered one'];
|
|
59
|
+
const out = [];
|
|
60
|
+
if (raised.name !== 'HttpError')
|
|
61
|
+
out.push(`it raised ${String(raised.name)}, not Octokit's HttpError`);
|
|
62
|
+
if (raised.status !== 404)
|
|
63
|
+
out.push(`its status is ${String(raised.status)}, not 404`);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return [`the case's calls failed: ${e.name}: ${e.message}`];
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const cases: import("./harness.js").ClientCase[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Agent as HttpsAgent } from 'node:https';
|
|
2
|
+
import { type Socket } from 'node:net';
|
|
3
|
+
/** What a case is given. */
|
|
4
|
+
export type ClientContext = {
|
|
5
|
+
/** the vendor's API host, as its client addresses it (the descriptor's first host) */
|
|
6
|
+
host: string;
|
|
7
|
+
/** the credentials the pack's credential door issued, by the environment variable a World sets each in */
|
|
8
|
+
env: Record<string, string>;
|
|
9
|
+
/** fetch as a World routes it: a URL on the vendor's host reaches the served pack, its Host kept */
|
|
10
|
+
fetch: typeof fetch;
|
|
11
|
+
/** a TCP connection to the served pack, for a socket client (the ws library's `createConnection`): the client keeps
|
|
12
|
+
* the vendor's URL and Host, and its TLS-less connection lands on the served port, as the World's interception does */
|
|
13
|
+
connect: () => Socket;
|
|
14
|
+
/** an HTTPS agent as a World routes it, for a client that takes an agent (Slack's, Stripe's): its connections reach the
|
|
15
|
+
* served port, the client's URL and Host unchanged */
|
|
16
|
+
agent: HttpsAgent;
|
|
17
|
+
/** writes the World's scenario file (read at the pack's next turn) */
|
|
18
|
+
scenario: (doc: unknown) => void;
|
|
19
|
+
};
|
|
20
|
+
/** One expectation of the vendor's documented behaviour, driven through its client. */
|
|
21
|
+
export type ClientCase = {
|
|
22
|
+
id: string;
|
|
23
|
+
/** what the vendor documents, as the case holds the pack to it */
|
|
24
|
+
asks: string;
|
|
25
|
+
/** the page that documents it: `<url> "<words>"` */
|
|
26
|
+
source: string;
|
|
27
|
+
/** what fails, each named; empty when the pack holds it */
|
|
28
|
+
run: (c: ClientContext) => Promise<string[]>;
|
|
29
|
+
};
|
|
30
|
+
/** The World's proxy in front of a pack served on `served`: it ends the caller's TLS (here, none was begun) and forwards
|
|
31
|
+
* each request and socket upgrade to the pack with `x-forwarded-proto: https`, the vendor's Host unchanged, as a World's
|
|
32
|
+
* proxy forwards a caller of the vendor's https host (twin-fetch.ts, twinPublicBase: the links and socket URLs the pack
|
|
33
|
+
* answers are then the vendor's secure ones). Answers the proxy's port. */
|
|
34
|
+
export declare function worldProxy(served: number): Promise<number>;
|
|
35
|
+
/** The context for a pack reached at `port` of this machine (the World's proxy in front of it). */
|
|
36
|
+
export declare function clientContext(port: number, host: string, env: Record<string, string>, scenarioPath: string): ClientContext;
|