@partylayer/conformance-runner 0.2.8 → 0.2.10

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/src/index.ts DELETED
@@ -1,116 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * PartyLayer Conformance Runner CLI
4
- *
5
- * Runs adapter conformance tests and generates reports.
6
- */
7
-
8
- import { program } from 'commander';
9
- import { readFileSync, writeFileSync, existsSync } from 'fs';
10
- import { loadAdapter } from './loader';
11
- import { runConformanceTests, createMockContext } from './tests';
12
- import { generateReport, formatReportText, type ConformanceReport } from './report';
13
-
14
- program
15
- .name('partylayer-conformance')
16
- .description('Run conformance tests for PartyLayer wallet adapters')
17
- .version('0.1.0');
18
-
19
- program
20
- .command('run')
21
- .description('Run conformance tests for an adapter')
22
- .requiredOption('--adapter <packageOrPath>', 'Adapter package name or path')
23
- .option('--network <network>', 'Network to use', 'devnet')
24
- .option('--registryUrl <url>', 'Registry URL (optional)')
25
- .option('--output <path>', 'Output JSON report path', 'conformance-report.json')
26
- .action(async (options) => {
27
- try {
28
- console.log(`Loading adapter: ${options.adapter}`);
29
- const adapter = await loadAdapter(options.adapter);
30
-
31
- console.log(`Running conformance tests for: ${adapter.name} (${adapter.walletId})`);
32
- const context = createMockContext();
33
- context.network = options.network as 'devnet' | 'testnet' | 'mainnet';
34
-
35
- const startTime = Date.now();
36
- const testResults = await runConformanceTests(adapter, context);
37
-
38
- const report = generateReport(
39
- {
40
- walletId: adapter.walletId,
41
- name: adapter.name,
42
- },
43
- testResults,
44
- startTime
45
- );
46
-
47
- // Write JSON report
48
- writeFileSync(options.output, JSON.stringify(report, null, 2));
49
- console.log(`\nReport written to: ${options.output}`);
50
-
51
- // Print human-readable summary
52
- console.log('\n' + formatReportText(report));
53
-
54
- // Exit with error code if tests failed
55
- if (report.results.failed > 0) {
56
- process.exit(1);
57
- }
58
- } catch (err) {
59
- console.error('Error running conformance tests:', err);
60
- process.exit(1);
61
- }
62
- });
63
-
64
- program
65
- .command('report')
66
- .description('Format and display a conformance report')
67
- .requiredOption('--input <path>', 'Input JSON report path')
68
- .action((options) => {
69
- try {
70
- if (!existsSync(options.input)) {
71
- console.error(`Report file not found: ${options.input}`);
72
- process.exit(1);
73
- }
74
-
75
- const reportJson = readFileSync(options.input, 'utf-8');
76
- const report = JSON.parse(reportJson) as ConformanceReport;
77
-
78
- console.log(formatReportText(report));
79
-
80
- if (report.results.failed > 0) {
81
- process.exit(1);
82
- }
83
- } catch (err) {
84
- console.error('Error reading report:', err);
85
- process.exit(1);
86
- }
87
- });
88
-
89
- program
90
- .command('run-cip0103')
91
- .description('Run CIP-0103 conformance tests against a Provider')
92
- .option('--output <path>', 'Output JSON report path', 'cip0103-report.json')
93
- .action(async (options) => {
94
- try {
95
- const { runCIP0103ConformanceTests, formatCIP0103Report } = await import('./cip0103-tests');
96
- const { PartyLayerProvider } = await import('@partylayer/provider');
97
-
98
- console.log('Running CIP-0103 conformance tests...');
99
-
100
- const provider = new PartyLayerProvider();
101
- const report = await runCIP0103ConformanceTests(provider);
102
-
103
- writeFileSync(options.output, JSON.stringify(report, null, 2));
104
- console.log(`\nReport written to: ${options.output}`);
105
- console.log('\n' + formatCIP0103Report(report));
106
-
107
- if (report.failed > 0) {
108
- process.exit(1);
109
- }
110
- } catch (err) {
111
- console.error('Error running CIP-0103 conformance tests:', err);
112
- process.exit(1);
113
- }
114
- });
115
-
116
- program.parse();
package/src/loader.ts DELETED
@@ -1,78 +0,0 @@
1
- /**
2
- * Adapter Module Loader
3
- *
4
- * Dynamically loads adapter modules (ESM/CJS safe)
5
- */
6
-
7
- import { createRequire } from 'module';
8
- import { resolve } from 'path';
9
- import { pathToFileURL } from 'url';
10
-
11
- import type { WalletAdapter } from '@partylayer/core';
12
-
13
- // This package builds to ESM (type: module), where the `require` global is
14
- // undefined — a bare `require(...)`/`require.resolve(...)` throws at runtime
15
- // (and would hit esbuild's `__require` shim in bundled consumers). Use a real
16
- // Node require created from this module's URL instead.
17
- const nodeRequire = createRequire(import.meta.url);
18
-
19
- /**
20
- * Load adapter from package or path
21
- */
22
- export async function loadAdapter(
23
- packageNameOrPath: string
24
- ): Promise<WalletAdapter> {
25
- let adapterPath: string;
26
-
27
- // Check if it's a path
28
- if (packageNameOrPath.startsWith('.') || packageNameOrPath.startsWith('/')) {
29
- adapterPath = resolve(packageNameOrPath);
30
- } else {
31
- // Try to resolve as package
32
- try {
33
- adapterPath = nodeRequire.resolve(packageNameOrPath);
34
- } catch {
35
- throw new Error(`Cannot resolve adapter: ${packageNameOrPath}`);
36
- }
37
- }
38
-
39
- // Try to load as ESM first
40
- try {
41
- const moduleUrl = pathToFileURL(adapterPath).href;
42
- const module = await import(moduleUrl);
43
-
44
- // Look for default export or named export
45
- const adapter = module.default || module[Object.keys(module)[0]];
46
-
47
- if (!adapter) {
48
- throw new Error('No adapter export found');
49
- }
50
-
51
- // If it's a class, instantiate it
52
- if (typeof adapter === 'function') {
53
- return new adapter();
54
- }
55
-
56
- return adapter as WalletAdapter;
57
- } catch (err) {
58
- // Fallback to CJS
59
- try {
60
- const module = nodeRequire(adapterPath);
61
- const adapter = module.default || module[Object.keys(module)[0]];
62
-
63
- if (!adapter) {
64
- throw new Error('No adapter export found');
65
- }
66
-
67
- if (typeof adapter === 'function') {
68
- return new adapter();
69
- }
70
-
71
- return adapter as WalletAdapter;
72
- } catch (cjsErr) {
73
- throw new Error(
74
- `Failed to load adapter: ${err instanceof Error ? err.message : String(err)}`
75
- );
76
- }
77
- }
78
- }
package/src/report.ts DELETED
@@ -1,107 +0,0 @@
1
- /**
2
- * Conformance Report Generation
3
- */
4
-
5
- import type { TestResult } from './tests';
6
-
7
- /**
8
- * Conformance report
9
- */
10
- export interface ConformanceReport {
11
- adapter: {
12
- walletId: string;
13
- name: string;
14
- version?: string;
15
- };
16
- environment: {
17
- nodeVersion: string;
18
- platform: string;
19
- timestamp: string;
20
- };
21
- results: {
22
- total: number;
23
- passed: number;
24
- failed: number;
25
- duration: number;
26
- };
27
- tests: TestResult[];
28
- }
29
-
30
- /**
31
- * Generate conformance report
32
- */
33
- export function generateReport(
34
- adapter: { walletId: string; name: string; version?: string },
35
- tests: TestResult[],
36
- startTime: number
37
- ): ConformanceReport {
38
- const passed = tests.filter((t) => t.passed).length;
39
- const failed = tests.filter((t) => !t.passed).length;
40
-
41
- return {
42
- adapter,
43
- environment: {
44
- nodeVersion: process.version,
45
- platform: process.platform,
46
- timestamp: new Date().toISOString(),
47
- },
48
- results: {
49
- total: tests.length,
50
- passed,
51
- failed,
52
- duration: Date.now() - startTime,
53
- },
54
- tests,
55
- };
56
- }
57
-
58
- /**
59
- * Format report as human-readable text
60
- */
61
- export function formatReportText(report: ConformanceReport): string {
62
- const lines: string[] = [];
63
-
64
- lines.push('='.repeat(60));
65
- lines.push('PartyLayer Adapter Conformance Report');
66
- lines.push('='.repeat(60));
67
- lines.push('');
68
- lines.push(`Adapter: ${report.adapter.name} (${report.adapter.walletId})`);
69
- if (report.adapter.version) {
70
- lines.push(`Version: ${report.adapter.version}`);
71
- }
72
- lines.push(`Environment: Node ${report.environment.nodeVersion} on ${report.environment.platform}`);
73
- lines.push(`Timestamp: ${report.environment.timestamp}`);
74
- lines.push('');
75
- lines.push('Summary:');
76
- lines.push(` Total Tests: ${report.results.total}`);
77
- lines.push(` Passed: ${report.results.passed}`);
78
- lines.push(` Failed: ${report.results.failed}`);
79
- lines.push(` Duration: ${report.results.duration}ms`);
80
- lines.push('');
81
- lines.push('Test Results:');
82
- lines.push('-'.repeat(60));
83
-
84
- report.tests.forEach((test, index) => {
85
- const status = test.passed ? '✓' : '✗';
86
- lines.push(`${index + 1}. ${status} ${test.name}`);
87
- if (test.details) {
88
- Object.entries(test.details).forEach(([key, value]) => {
89
- lines.push(` ${key}: ${JSON.stringify(value)}`);
90
- });
91
- }
92
- if (test.error) {
93
- lines.push(` Error: ${test.error}`);
94
- }
95
- });
96
-
97
- lines.push('-'.repeat(60));
98
- lines.push('');
99
-
100
- if (report.results.failed === 0) {
101
- lines.push('✓ All tests passed!');
102
- } else {
103
- lines.push(`✗ ${report.results.failed} test(s) failed`);
104
- }
105
-
106
- return lines.join('\n');
107
- }
package/src/tests.test.ts DELETED
@@ -1,74 +0,0 @@
1
- /**
2
- * Conformance: network-truthfulness contract.
3
- *
4
- * An adapter's connect() must surface the wallet's EFFECTIVE network in
5
- * session.network, not merely echo the requested ctx.network.
6
- */
7
- import { describe, it, expect } from 'vitest';
8
- import {
9
- toWalletId,
10
- toPartyId,
11
- type AdapterConnectResult,
12
- type AdapterContext,
13
- type AdapterDetectResult,
14
- type CapabilityKey,
15
- type WalletAdapter,
16
- } from '@partylayer/core';
17
- import { createMockContext, checkNetworkTruthfulness } from './tests';
18
-
19
- /** Base fixture adapter; subclasses decide what session.network they report. */
20
- abstract class FixtureAdapter implements WalletAdapter {
21
- readonly walletId = toWalletId('fixture');
22
- readonly name = 'Fixture';
23
- getCapabilities(): CapabilityKey[] {
24
- return ['connect'];
25
- }
26
- async detectInstalled(): Promise<AdapterDetectResult> {
27
- return { installed: true };
28
- }
29
- abstract connect(ctx: AdapterContext): Promise<AdapterConnectResult>;
30
- async disconnect(): Promise<void> {}
31
- }
32
-
33
- /** WRONG: echoes the requested ctx.network — fails the contract. */
34
- class EchoAdapter extends FixtureAdapter {
35
- async connect(ctx: AdapterContext): Promise<AdapterConnectResult> {
36
- return {
37
- partyId: toPartyId('party::echo'),
38
- session: { walletId: this.walletId, network: ctx.network, createdAt: 0 },
39
- capabilities: ['connect'],
40
- };
41
- }
42
- }
43
-
44
- /** RIGHT: reports the wallet's actual network — passes the contract. */
45
- class TruthfulAdapter extends FixtureAdapter {
46
- constructor(private readonly walletNetwork: string) {
47
- super();
48
- }
49
- async connect(): Promise<AdapterConnectResult> {
50
- return {
51
- partyId: toPartyId('party::truth'),
52
- session: { walletId: this.walletId, network: this.walletNetwork, createdAt: 0 },
53
- capabilities: ['connect'],
54
- };
55
- }
56
- }
57
-
58
- describe('conformance: network-truthfulness contract', () => {
59
- // The wallet is on mainnet; the dApp requested devnet.
60
- const ctx = (): AdapterContext => ({ ...createMockContext(), network: 'devnet' });
61
- const WALLET_NETWORK = 'canton:da-mainnet';
62
-
63
- it('FAILS an adapter that echoes ctx.network', async () => {
64
- const result = await checkNetworkTruthfulness(new EchoAdapter(), ctx(), WALLET_NETWORK);
65
- expect(result.passed).toBe(false);
66
- expect(result.error).toMatch(/echoed context\.network/);
67
- });
68
-
69
- it('PASSES an adapter that reports the wallet network', async () => {
70
- const result = await checkNetworkTruthfulness(new TruthfulAdapter(WALLET_NETWORK), ctx(), WALLET_NETWORK);
71
- expect(result.passed).toBe(true);
72
- expect(result.details).toMatchObject({ expected: WALLET_NETWORK, got: WALLET_NETWORK });
73
- });
74
- });
package/src/tests.ts DELETED
@@ -1,277 +0,0 @@
1
- /**
2
- * Adapter Conformance Tests
3
- *
4
- * Standard contract tests that all adapters must pass.
5
- */
6
-
7
- import type {
8
- WalletAdapter,
9
- AdapterContext,
10
- CapabilityKey,
11
- Session,
12
- } from '@partylayer/core';
13
- import {
14
- CapabilityNotSupportedError,
15
- } from '@partylayer/core';
16
-
17
- /**
18
- * Test result
19
- */
20
- export interface TestResult {
21
- name: string;
22
- passed: boolean;
23
- error?: string;
24
- details?: Record<string, unknown>;
25
- }
26
-
27
- /**
28
- * Conformance test suite
29
- */
30
- export interface ConformanceSuite {
31
- adapter: WalletAdapter;
32
- context: AdapterContext;
33
- results: TestResult[];
34
- }
35
-
36
- /**
37
- * Create mock adapter context
38
- */
39
- export function createMockContext(): AdapterContext {
40
- return {
41
- appName: 'Conformance Test',
42
- origin: 'https://test.example.com',
43
- network: 'devnet',
44
- logger: {
45
- debug: () => {},
46
- info: () => {},
47
- warn: () => {},
48
- error: () => {},
49
- },
50
- telemetry: undefined,
51
- registry: {
52
- getWallet: async () => ({}),
53
- },
54
- crypto: {
55
- encrypt: async (data: string) => data,
56
- decrypt: async (data: string) => data,
57
- generateKey: async () => 'test-key',
58
- },
59
- storage: {
60
- get: async () => null,
61
- set: async () => {},
62
- remove: async () => {},
63
- clear: async () => {},
64
- },
65
- timeout: async (ms: number) => {
66
- await new Promise((resolve) => setTimeout(resolve, ms));
67
- throw new Error('Timeout');
68
- },
69
- };
70
- }
71
-
72
- /**
73
- * Run conformance tests
74
- */
75
- export async function runConformanceTests(
76
- adapter: WalletAdapter,
77
- context: AdapterContext
78
- ): Promise<TestResult[]> {
79
- const results: TestResult[] = [];
80
-
81
- // Test 1: Adapter has required properties
82
- results.push({
83
- name: 'Adapter has walletId',
84
- passed: typeof adapter.walletId === 'string' && adapter.walletId.length > 0,
85
- details: { walletId: adapter.walletId },
86
- });
87
-
88
- results.push({
89
- name: 'Adapter has name',
90
- passed: typeof adapter.name === 'string' && adapter.name.length > 0,
91
- details: { name: adapter.name },
92
- });
93
-
94
- // Test 2: getCapabilities returns valid array
95
- let capabilities: CapabilityKey[];
96
- try {
97
- capabilities = adapter.getCapabilities();
98
- results.push({
99
- name: 'getCapabilities returns array',
100
- passed: Array.isArray(capabilities),
101
- details: { capabilities },
102
- });
103
-
104
- // Check for required capabilities
105
- const hasConnect = capabilities.includes('connect');
106
- results.push({
107
- name: 'Capabilities include "connect"',
108
- passed: hasConnect,
109
- details: { capabilities },
110
- });
111
- } catch (err) {
112
- results.push({
113
- name: 'getCapabilities throws error',
114
- passed: false,
115
- error: err instanceof Error ? err.message : String(err),
116
- });
117
- return results; // Can't continue without capabilities
118
- }
119
-
120
- // Test 3: detectInstalled returns valid result
121
- try {
122
- const detect = await adapter.detectInstalled();
123
- results.push({
124
- name: 'detectInstalled returns valid result',
125
- passed:
126
- typeof detect === 'object' &&
127
- typeof detect.installed === 'boolean' &&
128
- (detect.installed || typeof detect.reason === 'string'),
129
- details: { detect },
130
- });
131
- } catch (err) {
132
- results.push({
133
- name: 'detectInstalled throws error',
134
- passed: false,
135
- error: err instanceof Error ? err.message : String(err),
136
- });
137
- }
138
-
139
- // Test 4: If detectInstalled returns false, connect should throw WalletNotInstalledError
140
- // (This test requires mocking detectInstalled - skip for now or use mock adapter)
141
-
142
- // Test 5: Capability checks
143
- if (capabilities.includes('signMessage')) {
144
- if (typeof adapter.signMessage !== 'function') {
145
- results.push({
146
- name: 'signMessage capability declared but method missing',
147
- passed: false,
148
- error: 'signMessage capability declared but method not implemented',
149
- });
150
- }
151
- } else {
152
- // If signMessage not in capabilities, calling it should throw CapabilityNotSupportedError
153
- if (typeof adapter.signMessage === 'function') {
154
- try {
155
- await adapter.signMessage(context, {} as unknown as Session, { message: 'test' });
156
- results.push({
157
- name: 'signMessage throws CapabilityNotSupportedError when not supported',
158
- passed: false,
159
- error: 'signMessage did not throw error',
160
- });
161
- } catch (err) {
162
- const passed = err instanceof CapabilityNotSupportedError;
163
- results.push({
164
- name: 'signMessage throws CapabilityNotSupportedError when not supported',
165
- passed,
166
- error: passed ? undefined : err instanceof Error ? err.message : String(err),
167
- });
168
- }
169
- }
170
- }
171
-
172
- // Similar tests for signTransaction and submitTransaction
173
- if (capabilities.includes('signTransaction')) {
174
- if (typeof adapter.signTransaction !== 'function') {
175
- results.push({
176
- name: 'signTransaction capability declared but method missing',
177
- passed: false,
178
- error: 'signTransaction capability declared but method not implemented',
179
- });
180
- }
181
- }
182
-
183
- if (capabilities.includes('submitTransaction')) {
184
- if (typeof adapter.submitTransaction !== 'function') {
185
- results.push({
186
- name: 'submitTransaction capability declared but method missing',
187
- passed: false,
188
- error: 'submitTransaction capability declared but method not implemented',
189
- });
190
- }
191
- }
192
-
193
- // Test 5b: restore capability <-> method symmetry.
194
- //
195
- // We had a real bug where 3 of 5 adapters implemented restore() but did
196
- // not declare the 'restore' capability in getCapabilities(). The SDK
197
- // checks method presence at runtime so session persistence still worked,
198
- // but `wallet.capabilities.includes('restore')` returned false, which
199
- // misleads docs consumers and anyone programmatically inspecting caps.
200
- //
201
- // Enforce both directions:
202
- // - declared-but-missing: capability listed, no method → broken promise
203
- // - implemented-but-undeclared: method exists, capability missing →
204
- // silent drift that hides a feature from consumers
205
- {
206
- const hasRestoreMethod = typeof adapter.restore === 'function';
207
- const declaresRestore = capabilities.includes('restore');
208
- if (declaresRestore && !hasRestoreMethod) {
209
- results.push({
210
- name: 'restore capability declared but method missing',
211
- passed: false,
212
- error: `${adapter.name}: declares 'restore' capability but does not implement restore()`,
213
- });
214
- } else if (hasRestoreMethod && !declaresRestore) {
215
- results.push({
216
- name: 'restore method implemented but capability not declared',
217
- passed: false,
218
- error:
219
- `${adapter.name}: implements restore() but getCapabilities() does not include 'restore'. ` +
220
- `Add 'restore' to the returned array so consumers can discover the feature.`,
221
- });
222
- } else {
223
- results.push({
224
- name: 'restore capability / method symmetry',
225
- passed: true,
226
- details: { hasRestoreMethod, declaresRestore },
227
- });
228
- }
229
- }
230
-
231
- // Test 6: Error mapping
232
- // Verify adapter uses mapUnknownErrorToPartyLayerError
233
- // (This is a code inspection test - hard to test at runtime)
234
-
235
- return results;
236
- }
237
-
238
- /**
239
- * Network-truthfulness contract.
240
- *
241
- * An adapter's `connect()` MUST surface the wallet's EFFECTIVE network in
242
- * `session.network` (so the SDK can detect a wallet/dApp network mismatch) —
243
- * not merely echo the requested `context.network`.
244
- *
245
- * Drive it with a context whose `.network` differs from the network the wallet
246
- * actually reports (`walletReportedNetwork`); a compliant adapter returns
247
- * `session.network === walletReportedNetwork`. An adapter that echoes
248
- * `context.network` fails the contract.
249
- *
250
- * Adapters that genuinely cannot read the wallet's network must NOT claim to
251
- * (leave `session.network = context.network`) and must be recorded as
252
- * "network-reported: no" in the support matrix — they are out of scope for this
253
- * contract rather than silently passing it.
254
- */
255
- export async function checkNetworkTruthfulness(
256
- adapter: WalletAdapter,
257
- context: AdapterContext,
258
- walletReportedNetwork: string,
259
- ): Promise<TestResult> {
260
- const name = 'connect() session.network is the wallet effective network';
261
- try {
262
- const result = await adapter.connect(context);
263
- const got = result.session.network;
264
- const passed = got === walletReportedNetwork;
265
- return {
266
- name,
267
- passed,
268
- error: passed
269
- ? undefined
270
- : `expected session.network='${walletReportedNetwork}' (wallet-reported), got '${String(got)}'` +
271
- (got === context.network ? ' — merely echoed context.network' : ''),
272
- details: { expected: walletReportedNetwork, got, ctxNetwork: context.network },
273
- };
274
- } catch (err) {
275
- return { name, passed: false, error: err instanceof Error ? err.message : String(err) };
276
- }
277
- }
package/tsconfig.esm.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "module": "ESNext",
5
- "outDir": "./dist",
6
- "declaration": false,
7
- "declarationMap": false,
8
- "composite": false
9
- }
10
- }