@antasphere/chassis-sdk 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DEFAULT_DOWNLOAD_TIMEOUT_MS, DEFAULT_TIMEOUT_MS } from '../src/index.js';
3
+
4
+ /**
5
+ * The default deadlines, as literals (PRDCT-2530, verifier M19). The timeout
6
+ * tests compare a call's signal against these same constants, so a changed
7
+ * value moves with them; a number a person waits on is pinned by its value.
8
+ */
9
+ describe('chassis client default deadlines', () => {
10
+ it('30 s a request, 10 min a download', () => {
11
+ expect(DEFAULT_TIMEOUT_MS).toBe(30_000);
12
+ expect(DEFAULT_DOWNLOAD_TIMEOUT_MS).toBe(600_000);
13
+ });
14
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ChassisClient, PlatformApiError } from '../src/index.js';
3
+
4
+ /**
5
+ * The demo pass redeem is an endpoint of the sign-in library
6
+ * (`/api/v1/auth/demo/redeem`), so it has no route contract and the route
7
+ * coverage guard cannot see it (the same stance as `signInEmail`). This pins
8
+ * its method, its path and its body by hand.
9
+ */
10
+
11
+ function recordingClient(answer: Response) {
12
+ const calls: Array<{ method: string; path: string; body: unknown; credentials: unknown }> = [];
13
+ const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
14
+ const url = new URL(typeof input === 'string' ? input : input.toString(), 'http://x');
15
+ calls.push({
16
+ method: (init?.method ?? 'GET').toUpperCase(),
17
+ path: url.pathname,
18
+ body: typeof init?.body === 'string' ? JSON.parse(init.body) : undefined,
19
+ credentials: init?.credentials
20
+ });
21
+ return answer;
22
+ }) as typeof globalThis.fetch;
23
+ return { client: new ChassisClient<'things:read'>({ fetch }), calls };
24
+ }
25
+
26
+ const json = (status: number, body: unknown) =>
27
+ new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
28
+
29
+ describe('redeemDemoPass', () => {
30
+ it('posts the secret as `pass` to the sign-in library, same-origin, and answers the person', async () => {
31
+ const answer = {
32
+ user: { id: 'u1', email: 'a@example.com', name: 'A' },
33
+ path: '/decks',
34
+ expiresAt: '2026-09-28T00:00:00.000Z'
35
+ };
36
+ const { client, calls } = recordingClient(json(200, answer));
37
+ await expect(client.redeemDemoPass('s3cr3t')).resolves.toEqual(answer);
38
+ expect(calls).toEqual([
39
+ {
40
+ method: 'POST',
41
+ path: '/api/v1/auth/demo/redeem',
42
+ body: { pass: 's3cr3t' },
43
+ credentials: 'same-origin'
44
+ }
45
+ ]);
46
+ });
47
+
48
+ it('turns the one refusal into a PlatformApiError carrying its code', async () => {
49
+ const { client } = recordingClient(
50
+ json(401, { error: { code: 'invalid_demo_pass', message: 'This demo link is not valid' } })
51
+ );
52
+ const refused = await client.redeemDemoPass('nope').catch((err: unknown) => err);
53
+ expect(refused).toBeInstanceOf(PlatformApiError);
54
+ expect(refused).toMatchObject({ status: 401, code: 'invalid_demo_pass' });
55
+ });
56
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ChassisClient, PlatformApiError } from '../src/index.js';
3
+
4
+ /**
5
+ * PRDCT-2426: a download is a request like any other, so it carries the
6
+ * active workspace. A browser anchor cannot send `X-Workspace-Id`, which is
7
+ * why a dashboard downloads through these methods: each of them must put the
8
+ * header on the wire, or a file of a non-default workspace answers 404.
9
+ * (The generic downloads; the tool SDK judges its own on its own class.)
10
+ */
11
+
12
+ const FILE = '33333333-3333-3333-3333-333333333333';
13
+ const WORKSPACE = '44444444-4444-4444-4444-444444444444';
14
+ type Client = ChassisClient<'things:read'>;
15
+
16
+ function recordingClient() {
17
+ const calls: { url: string; init: RequestInit }[] = [];
18
+ const fetchImpl = (async (input: unknown, init?: RequestInit) => {
19
+ calls.push({ url: String(input), init: init ?? {} });
20
+ return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
21
+ }) as typeof globalThis.fetch;
22
+ const client: Client = new ChassisClient({ baseUrl: 'http://x', fetch: fetchImpl, workspaceId: WORKSPACE });
23
+ return { client, calls };
24
+ }
25
+
26
+ const DOWNLOADS: Record<string, [(c: Client) => Promise<Response>, string]> = {
27
+ downloadFileContent: [(c) => c.downloadFileContent(FILE), `/api/v1/files/${FILE}/content`],
28
+ downloadExport: [(c) => c.downloadExport(), '/api/v1/workspace/export']
29
+ };
30
+
31
+ describe('chassis client downloads carry the active workspace', () => {
32
+ it.each(Object.entries(DOWNLOADS))('%s', async (_name, [call, path]) => {
33
+ const { client, calls } = recordingClient();
34
+ await call(client);
35
+ expect(calls).toHaveLength(1);
36
+ expect(calls[0]!.url).toBe(`http://x${path}`);
37
+ expect((calls[0]!.init.headers as Record<string, string>)['x-workspace-id']).toBe(WORKSPACE);
38
+ });
39
+
40
+ it('percent-encodes the file id: no path or query break-out', async () => {
41
+ const { client, calls } = recordingClient();
42
+ await client.downloadFileContent('../f?x#1');
43
+ expect(calls[0]!.url).toBe('http://x/api/v1/files/..%2Ff%3Fx%231/content');
44
+ });
45
+
46
+ it('follows setWorkspace, and sends no header once it is cleared', async () => {
47
+ const { client, calls } = recordingClient();
48
+ client.setWorkspace('other');
49
+ await client.downloadFileContent(FILE);
50
+ client.setWorkspace(null);
51
+ await client.downloadFileContent(FILE);
52
+ expect((calls[0]!.init.headers as Record<string, string>)['x-workspace-id']).toBe('other');
53
+ expect((calls[1]!.init.headers as Record<string, string>)['x-workspace-id']).toBeUndefined();
54
+ });
55
+
56
+ it('a refusal throws the wire error instead of handing back a body to save', async () => {
57
+ const fetchImpl = (async () =>
58
+ new Response(JSON.stringify({ error: { code: 'not_found', message: 'File not found' } }), {
59
+ status: 404,
60
+ headers: { 'content-type': 'application/json' }
61
+ })) as typeof globalThis.fetch;
62
+ const client: Client = new ChassisClient({ baseUrl: 'http://x', fetch: fetchImpl });
63
+ for (const call of [() => client.downloadFileContent(FILE), () => client.downloadExport()]) {
64
+ const error = await call().then(
65
+ () => null,
66
+ (e: unknown) => e
67
+ );
68
+ expect(error).toBeInstanceOf(PlatformApiError);
69
+ expect(error).toMatchObject({ name: 'PlatformApiError', status: 404, code: 'not_found' });
70
+ }
71
+ });
72
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ChassisClient, type ClientOptions } from '../src/index.js';
3
+
4
+ /**
5
+ * `headers` rides on every call, and it never stands in for a credential:
6
+ * a client given no key sends no `authorization` at all (the CLI's owner
7
+ * session holds a cookie and must never look like a machine caller), and a
8
+ * client given a key keeps its own bearer whatever the caller passed.
9
+ */
10
+
11
+ function recordingClient(options: ClientOptions = {}) {
12
+ const sent: Array<Record<string, string>> = [];
13
+ const fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
14
+ sent.push({ ...(init?.headers as Record<string, string>) });
15
+ return new Response(JSON.stringify({ passes: [] }), {
16
+ status: 200,
17
+ headers: { 'content-type': 'application/json' }
18
+ });
19
+ }) as typeof globalThis.fetch;
20
+ return { client: new ChassisClient<'things:read'>({ ...options, fetch }), sent };
21
+ }
22
+
23
+ describe('ClientOptions.headers', () => {
24
+ it('sends the given headers on every call, and no authorization when no key is given', async () => {
25
+ const { client, sent } = recordingClient({
26
+ baseUrl: 'http://x',
27
+ headers: { cookie: 'a.session_token=v', origin: 'http://x' }
28
+ });
29
+ await client.demoPasses();
30
+ await client.mintDemoPass({ email: 'ada@example.com' });
31
+ expect(sent).toHaveLength(2);
32
+ for (const headers of sent) {
33
+ expect(headers['cookie']).toBe('a.session_token=v');
34
+ expect(headers['origin']).toBe('http://x');
35
+ expect(headers).not.toHaveProperty('authorization');
36
+ }
37
+ });
38
+
39
+ it('never lets a given header replace the key', async () => {
40
+ const { client, sent } = recordingClient({
41
+ apiKey: 'k_1_s',
42
+ headers: { authorization: 'Bearer forged', 'x-extra': '1' }
43
+ });
44
+ await client.demoPasses();
45
+ expect(sent[0]!['authorization']).toBe('Bearer k_1_s');
46
+ expect(sent[0]!['x-extra']).toBe('1');
47
+ });
48
+ });
@@ -0,0 +1,174 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defineChassisContract } from '@antasphere/chassis-contract';
3
+ import * as chassisRoutes from '@antasphere/chassis-contract/routes';
4
+ import { ChassisClient } from '../src/index.js';
5
+
6
+ /**
7
+ * Chassis client ↔ chassis contract drift guard, the generic half of the
8
+ * tool SDK's own route-coverage test. Request/response SHAPES already fail
9
+ * typecheck; this closes the remaining gap: the method + path string literals
10
+ * inside each client method.
11
+ *
12
+ * The scope-carrying routes are instantiated with scopes that are NOT any real
13
+ * tool's (`things:*`), so the chassis stays honest about never spelling one.
14
+ *
15
+ * Better Auth wrapper methods (session/signInEmail/signOut) call /auth/* and
16
+ * are intentionally NOT in the contract; they are excluded below.
17
+ */
18
+
19
+ const contract = defineChassisContract({ scopes: ['things:read', 'things:write'] });
20
+ const scopedRoutes = chassisRoutes.defineChassisRoutes(
21
+ contract,
22
+ { cliKeyScopesLabel: 'things:read+write', scopes: { dataExport: 'things:export' } },
23
+ { fileInUseOpenApi: 'file_in_use: referenced by a thing' }
24
+ );
25
+ type ThingsScope = 'things:read' | 'things:write';
26
+ type Client = ChassisClient<ThingsScope>;
27
+
28
+ const SAMPLE_ID = '11111111-1111-1111-1111-111111111111';
29
+ const SAMPLE_TOKEN = 'x'.repeat(24);
30
+
31
+ /** contract route → a client call. Keyed by `METHOD path` (contract shape). */
32
+ /** A Better Auth user id: text, never a uuid. */
33
+ const SAMPLE_USER_ID = 'usr_projects_coverage';
34
+ const SAMPLE_TEAM_ID = '7b0c3d1e-2f4a-4b6c-8d9e-0f1a2b3c4d5e';
35
+
36
+ const INVOKERS: Record<string, (c: Client) => Promise<unknown>> = {
37
+ 'GET /instance': (c) => c.instance(),
38
+ 'POST /setup': (c) =>
39
+ c.setup({
40
+ instanceName: 'X',
41
+ owner: { email: 'a@b.co', name: 'A', password: 'x'.repeat(12) }
42
+ }),
43
+ 'GET /me': (c) => c.me(),
44
+ 'POST /me/onboarding/dismiss': (c) => c.dismissOnboarding(),
45
+ 'PUT /me/default-workspace': (c) => c.setDefaultWorkspace(SAMPLE_ID),
46
+ 'POST /workspaces': (c) => c.createWorkspace('Second'),
47
+ 'POST /sso/logout': (c) => c.ssoLogout(),
48
+ 'POST /cli/auth/request': (c) => c.cliAuthRequest({ email: 'a@b.co' }),
49
+ 'POST /cli/auth/complete': (c) => c.cliAuthComplete({ email: 'a@b.co', otp: '123456' }),
50
+ 'DELETE /cli/auth/key': (c) => c.cliAuthRevoke(),
51
+ 'POST /sso/cli-connect': (c) => c.ssoCliConnect({ token: SAMPLE_TOKEN }),
52
+ 'GET /members': (c) => c.members(),
53
+ 'PATCH /members/{id}': (c) => c.updateMember(SAMPLE_ID, { role: 'admin' }),
54
+ 'DELETE /members/{id}': (c) => c.deleteMember(SAMPLE_ID),
55
+ 'POST /members/{id}/remove': (c) => c.removeMember(SAMPLE_ID),
56
+ 'POST /members/{id}/reset-link': (c) => c.createMemberResetLink(SAMPLE_ID),
57
+ 'POST /members/{id}/change-email-link': (c) =>
58
+ c.createMemberChangeEmailLink(SAMPLE_ID, { newEmail: 'a@b.co' }),
59
+ 'GET /projects': (c) => c.projects(),
60
+ 'POST /projects': (c) => c.createProject({ name: 'P' }),
61
+ 'GET /projects/{id}': (c) => c.project(SAMPLE_ID),
62
+ 'PATCH /projects/{id}': (c) => c.updateProject(SAMPLE_ID, { name: 'P' }),
63
+ 'POST /projects/{id}/archive': (c) => c.archiveProject(SAMPLE_ID),
64
+ 'POST /projects/{id}/unarchive': (c) => c.unarchiveProject(SAMPLE_ID),
65
+ 'GET /projects/{id}/members': (c) => c.projectMembers(SAMPLE_ID),
66
+ 'POST /projects/{id}/members': (c) => c.addProjectMember(SAMPLE_ID, { email: 'a@b.co', role: 'viewer' }),
67
+ 'PATCH /projects/{id}/members/{userId}': (c) => c.setProjectMemberRole(SAMPLE_ID, SAMPLE_USER_ID, 'editor'),
68
+ 'DELETE /projects/{id}/members/{userId}': (c) => c.removeProjectMember(SAMPLE_ID, SAMPLE_USER_ID),
69
+ 'PATCH /projects/{id}/teams/{teamId}': (c) => c.setProjectTeamRole(SAMPLE_ID, SAMPLE_TEAM_ID, 'editor'),
70
+ 'DELETE /projects/{id}/teams/{teamId}': (c) => c.removeProjectTeam(SAMPLE_ID, SAMPLE_TEAM_ID),
71
+ 'GET /teams': (c) => c.teams(),
72
+ 'GET /teams/{id}': (c) => c.team(SAMPLE_ID),
73
+ 'POST /teams': (c) => c.createTeam({ name: 'Design' }),
74
+ 'PATCH /teams/{id}': (c) => c.updateTeam(SAMPLE_ID, { name: 'Design' }),
75
+ 'DELETE /teams/{id}': (c) => c.deleteTeam(SAMPLE_ID),
76
+ 'GET /teams/{id}/members': (c) => c.teamMembers(SAMPLE_ID),
77
+ 'POST /teams/{id}/members': (c) => c.addTeamMember(SAMPLE_ID, { email: 'a@b.co' }),
78
+ 'DELETE /teams/{id}/members/{userId}': (c) => c.removeTeamMember(SAMPLE_ID, SAMPLE_USER_ID),
79
+ 'GET /demo/passes': (c) => c.demoPasses(),
80
+ 'POST /demo/passes': (c) => c.mintDemoPass({ email: 'a@example.com' }),
81
+ 'DELETE /demo/passes/{id}': (c) => c.revokeDemoPass(SAMPLE_ID),
82
+ 'GET /api-keys': (c) => c.apiKeys(),
83
+ 'POST /api-keys': (c) => c.createApiKey({ name: 'k', scopes: ['things:read'] }),
84
+ 'DELETE /api-keys/{id}': (c) => c.revokeApiKey(SAMPLE_ID),
85
+ 'PATCH /workspace': (c) => c.updateWorkspace({ name: 'Acme' }),
86
+ 'GET /invitations': (c) => c.invitations(),
87
+ 'POST /invitations': (c) => c.createInvitation({ email: 'a@b.co', role: 'member' }),
88
+ 'DELETE /invitations/{id}': (c) => c.revokeInvitation(SAMPLE_ID),
89
+ 'GET /invitations/lookup': (c) => c.lookupInvitation(SAMPLE_TOKEN),
90
+ 'POST /invitations/accept': (c) => c.acceptInvitation({ token: SAMPLE_TOKEN }),
91
+ 'POST /admin/break-glass/claim-ownership': (c) => c.breakGlassClaimOwnership(),
92
+ 'POST /admin/break-glass/reset-2fa': (c) => c.breakGlassResetTwoFactor({ userId: SAMPLE_ID }),
93
+ 'GET /audit': (c) => c.audit(),
94
+ 'GET /workspace/export': (c) => c.downloadExport(),
95
+ 'GET /files': (c) => c.files(),
96
+ 'GET /files/{id}': (c) => c.file(SAMPLE_ID),
97
+ 'POST /files': (c) => c.uploadFile('n.txt', new Uint8Array([1])),
98
+ 'DELETE /files/{id}': (c) => c.deleteFile(SAMPLE_ID)
99
+ };
100
+
101
+ interface ContractRoute {
102
+ method: string;
103
+ path: string;
104
+ }
105
+
106
+ function contractRoutes(): Array<{ key: string; method: string; path: string }> {
107
+ // Both halves of the chassis routes: the static ones, then the ones
108
+ // instantiated with the test tool's scopes.
109
+ return [
110
+ ...Object.values(chassisRoutes as Record<string, unknown>),
111
+ ...Object.values(scopedRoutes as Record<string, unknown>)
112
+ ]
113
+ .filter((r): r is ContractRoute => {
114
+ const o = r as Partial<ContractRoute>;
115
+ return typeof o?.method === 'string' && typeof o?.path === 'string';
116
+ })
117
+ .map((r) => ({
118
+ key: `${r.method.toUpperCase()} ${r.path}`,
119
+ method: r.method.toUpperCase(),
120
+ path: r.path
121
+ }));
122
+ }
123
+
124
+ /** Records the (method, pathname) of the single request a client call makes. */
125
+ function recordingClient(): { client: Client; calls: Array<{ method: string; path: string }> } {
126
+ const calls: Array<{ method: string; path: string }> = [];
127
+ const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
128
+ const url = new URL(typeof input === 'string' ? input : input.toString(), 'http://x');
129
+ calls.push({ method: (init?.method ?? 'GET').toUpperCase(), path: url.pathname });
130
+ return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } });
131
+ }) as typeof globalThis.fetch;
132
+ return { client: new ChassisClient<ThingsScope>({ fetch }), calls };
133
+ }
134
+
135
+ /** The contract path with every param substituted, to compare against the client's real path. */
136
+ function expectedPath(contractPath: string): string {
137
+ return `/api/v1${contractPath
138
+ .replace('{id}', SAMPLE_ID)
139
+ .replace('{userId}', SAMPLE_USER_ID)
140
+ .replace('{teamId}', SAMPLE_TEAM_ID)}`;
141
+ }
142
+
143
+ describe('chassis client route coverage (contract drift guard)', () => {
144
+ const all = contractRoutes();
145
+
146
+ it('sees both halves of the chassis routes', () => {
147
+ expect(all.length).toBe(Object.keys(INVOKERS).length);
148
+ expect(all.map((r) => r.key)).toContain('GET /me');
149
+ expect(all.map((r) => r.key)).toContain('GET /instance');
150
+ });
151
+
152
+ it('maps every chassis route to a client method', () => {
153
+ const missing = all.filter((r) => !(r.key in INVOKERS)).map((r) => r.key);
154
+ expect(missing, `contract routes without a client method: ${missing.join(', ')}`).toEqual([]);
155
+ });
156
+
157
+ it('has no stale mappings (every mapping targets a real contract route)', () => {
158
+ const keys = new Set(all.map((r) => r.key));
159
+ const stale = Object.keys(INVOKERS).filter((k) => !keys.has(k));
160
+ expect(stale, `INVOKERS entries with no matching contract route: ${stale.join(', ')}`).toEqual([]);
161
+ });
162
+
163
+ for (const route of contractRoutes()) {
164
+ it(`${route.key} hits the right method + path`, async () => {
165
+ const invoke = INVOKERS[route.key];
166
+ if (!invoke) return; // coverage asserted separately
167
+ const { client, calls } = recordingClient();
168
+ await invoke(client);
169
+ expect(calls).toHaveLength(1);
170
+ expect(calls[0]!.method).toBe(route.method);
171
+ expect(calls[0]!.path).toBe(expectedPath(route.path));
172
+ });
173
+ }
174
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ChassisClient, DEFAULT_DOWNLOAD_TIMEOUT_MS, DEFAULT_TIMEOUT_MS } from '../src/index.js';
3
+
4
+ /**
5
+ * PRDCT-1353 / CLI-10 — `fetch` has no default timeout anywhere. Without an
6
+ * AbortSignal a silent (or deliberately stalling) instance parks the caller
7
+ * forever. Every call the chassis client makes must carry one. (The tool SDK
8
+ * keeps the same cases on its own class; these judge the generic half alone.)
9
+ */
10
+
11
+ function recordingClient(overrides: { timeoutMs?: number; downloadTimeoutMs?: number } = {}) {
12
+ const inits: RequestInit[] = [];
13
+ const fetchImpl = (async (_input: unknown, init?: RequestInit) => {
14
+ inits.push(init ?? {});
15
+ return new Response(JSON.stringify({ ok: true }), {
16
+ status: 200,
17
+ headers: { 'content-type': 'application/json' }
18
+ });
19
+ }) as typeof globalThis.fetch;
20
+ const client = new ChassisClient<'things:read'>({
21
+ baseUrl: 'http://x',
22
+ apiKey: 'thk_k_s',
23
+ fetch: fetchImpl,
24
+ ...overrides
25
+ });
26
+ return { client, inits };
27
+ }
28
+
29
+ describe('chassis client request deadlines', () => {
30
+ it('puts an AbortSignal on the JSON calls', async () => {
31
+ const { client, inits } = recordingClient();
32
+ await client.instance();
33
+ expect(inits).toHaveLength(1);
34
+ expect(inits[0]!.signal).toBeInstanceOf(AbortSignal);
35
+ expect(inits[0]!.signal!.aborted).toBe(false);
36
+ });
37
+
38
+ it('puts an AbortSignal on the streaming download calls', async () => {
39
+ const { client, inits } = recordingClient();
40
+ await client.downloadExport();
41
+ await client.downloadFileContent('file');
42
+ expect(inits).toHaveLength(2);
43
+ for (const init of inits) expect(init.signal).toBeInstanceOf(AbortSignal);
44
+ });
45
+
46
+ it('puts an AbortSignal on the upload call', async () => {
47
+ const { client, inits } = recordingClient();
48
+ await client.uploadFile('a.txt', new Uint8Array([1]), 'text/plain');
49
+ expect(inits).toHaveLength(1);
50
+ expect(inits[0]!.signal).toBeInstanceOf(AbortSignal);
51
+ });
52
+
53
+ it('actually aborts once the deadline passes', async () => {
54
+ const stalling = (async (_input: unknown, init?: RequestInit) =>
55
+ new Promise<Response>((_resolve, reject) => {
56
+ init?.signal?.addEventListener('abort', () => reject(new Error('aborted')));
57
+ })) as typeof globalThis.fetch;
58
+ const client = new ChassisClient<'things:read'>({ baseUrl: 'http://x', fetch: stalling, timeoutMs: 20 });
59
+ await expect(client.instance()).rejects.toThrow(/abort/i);
60
+ });
61
+
62
+ it('lets an explicit 0 opt out, per kind of call', async () => {
63
+ const { client, inits } = recordingClient({ timeoutMs: 0 });
64
+ await client.instance();
65
+ await client.downloadExport();
66
+ expect(inits[0]!.signal).toBeUndefined();
67
+ expect(inits[1]!.signal).toBeInstanceOf(AbortSignal);
68
+
69
+ const downloads = recordingClient({ downloadTimeoutMs: 0 });
70
+ await downloads.client.downloadExport();
71
+ await downloads.client.instance();
72
+ expect(downloads.inits[0]!.signal).toBeUndefined();
73
+ expect(downloads.inits[1]!.signal).toBeInstanceOf(AbortSignal);
74
+ });
75
+
76
+ it('documents its defaults', () => {
77
+ expect(DEFAULT_TIMEOUT_MS).toBeGreaterThan(0);
78
+ expect(DEFAULT_DOWNLOAD_TIMEOUT_MS).toBeGreaterThan(DEFAULT_TIMEOUT_MS);
79
+ });
80
+ });
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.