kadi-deploy 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/.prettierrc +6 -0
- package/README.md +589 -0
- package/agent.json +23 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/quick-command.txt +92 -0
- package/scripts/preflight.js +458 -0
- package/scripts/preflight.sh +300 -0
- package/src/cli/bid-selector.ts +222 -0
- package/src/cli/colors.ts +216 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/prompts.ts +190 -0
- package/src/cli/spinners.ts +165 -0
- package/src/commands/deploy-local.ts +475 -0
- package/src/commands/deploy.ts +1342 -0
- package/src/commands/down.ts +679 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/lock.ts +571 -0
- package/src/config/agent-loader.ts +177 -0
- package/src/config/index.ts +9 -0
- package/src/display/deployment-info.ts +220 -0
- package/src/display/pricing.ts +137 -0
- package/src/display/resources.ts +234 -0
- package/src/enhanced-registry-manager.ts +892 -0
- package/src/index.ts +307 -0
- package/src/infrastructure/registry.ts +269 -0
- package/src/schemas/profiles.ts +529 -0
- package/src/secrets/broker-urls.ts +109 -0
- package/src/secrets/handshake.ts +407 -0
- package/src/secrets/index.ts +69 -0
- package/src/secrets/inject-env.ts +171 -0
- package/src/secrets/nonce.ts +31 -0
- package/src/secrets/normalize.ts +204 -0
- package/src/secrets/prepare.ts +152 -0
- package/src/secrets/validate.ts +243 -0
- package/src/secrets/vault.ts +80 -0
- package/src/types/akash.ts +116 -0
- package/src/types/container-registry-ability.d.ts +158 -0
- package/src/types/external.ts +49 -0
- package/src/types.ts +211 -0
- package/src/utils/akt-price.ts +74 -0
- package/tests/agent-loader.test.ts +239 -0
- package/tests/autonomous.test.ts +244 -0
- package/tests/down.test.ts +1143 -0
- package/tests/lock.test.ts +1148 -0
- package/tests/nonce.test.ts +34 -0
- package/tests/normalize.test.ts +270 -0
- package/tests/secrets-schema.test.ts +301 -0
- package/tests/types.test.ts +198 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Configuration Loader Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for loading, parsing, and querying agent.json configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
// Use dynamic import to avoid ESM issues with mocking
|
|
13
|
+
let loadAgentConfig: typeof import('../src/config/agent-loader.js').loadAgentConfig;
|
|
14
|
+
let getDeployProfiles: typeof import('../src/config/agent-loader.js').getDeployProfiles;
|
|
15
|
+
let hasProfile: typeof import('../src/config/agent-loader.js').hasProfile;
|
|
16
|
+
let getProfile: typeof import('../src/config/agent-loader.js').getProfile;
|
|
17
|
+
let getFirstProfile: typeof import('../src/config/agent-loader.js').getFirstProfile;
|
|
18
|
+
let AgentConfigError: typeof import('../src/config/agent-loader.js').AgentConfigError;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
const mod = await import('../src/config/agent-loader.js');
|
|
22
|
+
loadAgentConfig = mod.loadAgentConfig;
|
|
23
|
+
getDeployProfiles = mod.getDeployProfiles;
|
|
24
|
+
hasProfile = mod.hasProfile;
|
|
25
|
+
getProfile = mod.getProfile;
|
|
26
|
+
getFirstProfile = mod.getFirstProfile;
|
|
27
|
+
AgentConfigError = mod.AgentConfigError;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────
|
|
31
|
+
// Fixtures
|
|
32
|
+
// ─────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const MINIMAL_AGENT = {
|
|
35
|
+
name: 'test-agent',
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const AGENT_WITH_PROFILES = {
|
|
40
|
+
name: 'test-agent',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
deploy: {
|
|
43
|
+
local: {
|
|
44
|
+
target: 'local',
|
|
45
|
+
engine: 'docker',
|
|
46
|
+
services: {
|
|
47
|
+
app: {
|
|
48
|
+
image: 'nginx:alpine',
|
|
49
|
+
expose: [{ port: 80, as: 80 }],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
production: {
|
|
54
|
+
target: 'akash',
|
|
55
|
+
network: 'mainnet',
|
|
56
|
+
services: {
|
|
57
|
+
web: {
|
|
58
|
+
image: 'nginx:alpine',
|
|
59
|
+
expose: [{ port: 80, as: 80, to: [{ global: true }] }],
|
|
60
|
+
resources: { cpu: 0.5, memory: '512Mi', storage: '1Gi' },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const AGENT_WITH_AUTONOMOUS = {
|
|
68
|
+
name: 'auto-agent',
|
|
69
|
+
version: '2.0.0',
|
|
70
|
+
deploy: {
|
|
71
|
+
production: {
|
|
72
|
+
target: 'akash',
|
|
73
|
+
network: 'testnet',
|
|
74
|
+
services: {
|
|
75
|
+
api: {
|
|
76
|
+
image: 'my-api:latest',
|
|
77
|
+
expose: [{ port: 3000, as: 80, to: [{ global: true }] }],
|
|
78
|
+
resources: { cpu: 1, memory: '1Gi', storage: '2Gi' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
secrets: {
|
|
82
|
+
vault: 'global',
|
|
83
|
+
required: ['API_KEY'],
|
|
84
|
+
optional: ['DEBUG_TOKEN'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
brokers: {
|
|
89
|
+
default: 'wss://broker.kadi.build/kadi',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ─────────────────────────────────────────────────────────
|
|
94
|
+
// Tests: loadAgentConfig
|
|
95
|
+
// ─────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe('loadAgentConfig', () => {
|
|
98
|
+
let tmpDir: string;
|
|
99
|
+
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'kadi-test-'));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(async () => {
|
|
105
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('loads a valid agent.json', async () => {
|
|
109
|
+
await fs.writeFile(
|
|
110
|
+
path.join(tmpDir, 'agent.json'),
|
|
111
|
+
JSON.stringify(MINIMAL_AGENT)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const config = await loadAgentConfig(tmpDir);
|
|
115
|
+
expect(config.name).toBe('test-agent');
|
|
116
|
+
expect(config.version).toBe('1.0.0');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('loads agent.json with deploy profiles', async () => {
|
|
120
|
+
await fs.writeFile(
|
|
121
|
+
path.join(tmpDir, 'agent.json'),
|
|
122
|
+
JSON.stringify(AGENT_WITH_PROFILES)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const config = await loadAgentConfig(tmpDir);
|
|
126
|
+
expect(config.deploy).toBeDefined();
|
|
127
|
+
expect(Object.keys(config.deploy!)).toContain('local');
|
|
128
|
+
expect(Object.keys(config.deploy!)).toContain('production');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('throws AgentConfigError for missing agent.json', async () => {
|
|
132
|
+
await expect(loadAgentConfig(tmpDir)).rejects.toThrow(AgentConfigError);
|
|
133
|
+
await expect(loadAgentConfig(tmpDir)).rejects.toThrow('agent.json not found');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('throws AgentConfigError for invalid JSON', async () => {
|
|
137
|
+
await fs.writeFile(path.join(tmpDir, 'agent.json'), '{ invalid json');
|
|
138
|
+
await expect(loadAgentConfig(tmpDir)).rejects.toThrow(AgentConfigError);
|
|
139
|
+
await expect(loadAgentConfig(tmpDir)).rejects.toThrow('invalid JSON');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('throws AgentConfigError for missing name field', async () => {
|
|
143
|
+
await fs.writeFile(
|
|
144
|
+
path.join(tmpDir, 'agent.json'),
|
|
145
|
+
JSON.stringify({ version: '1.0.0' })
|
|
146
|
+
);
|
|
147
|
+
await expect(loadAgentConfig(tmpDir)).rejects.toThrow(AgentConfigError);
|
|
148
|
+
await expect(loadAgentConfig(tmpDir)).rejects.toThrow('missing required field');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─────────────────────────────────────────────────────────
|
|
153
|
+
// Tests: getDeployProfiles
|
|
154
|
+
// ─────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
describe('getDeployProfiles', () => {
|
|
157
|
+
it('returns empty array when no deploy profiles', () => {
|
|
158
|
+
expect(getDeployProfiles(MINIMAL_AGENT)).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('returns profile names from deploy config', () => {
|
|
162
|
+
const profiles = getDeployProfiles(AGENT_WITH_PROFILES);
|
|
163
|
+
expect(profiles).toContain('local');
|
|
164
|
+
expect(profiles).toContain('production');
|
|
165
|
+
expect(profiles).toHaveLength(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('filters out services and networks keys', () => {
|
|
169
|
+
const config = {
|
|
170
|
+
name: 'test',
|
|
171
|
+
version: '1.0.0',
|
|
172
|
+
deploy: {
|
|
173
|
+
production: { target: 'akash' },
|
|
174
|
+
services: { web: {} },
|
|
175
|
+
networks: { default: {} },
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const profiles = getDeployProfiles(config);
|
|
179
|
+
expect(profiles).toEqual(['production']);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────
|
|
184
|
+
// Tests: hasProfile
|
|
185
|
+
// ─────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe('hasProfile', () => {
|
|
188
|
+
it('returns true for existing profile', () => {
|
|
189
|
+
expect(hasProfile(AGENT_WITH_PROFILES, 'local')).toBe(true);
|
|
190
|
+
expect(hasProfile(AGENT_WITH_PROFILES, 'production')).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns false for non-existent profile', () => {
|
|
194
|
+
expect(hasProfile(AGENT_WITH_PROFILES, 'staging')).toBe(false);
|
|
195
|
+
expect(hasProfile(MINIMAL_AGENT, 'local')).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ─────────────────────────────────────────────────────────
|
|
200
|
+
// Tests: getFirstProfile
|
|
201
|
+
// ─────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
describe('getFirstProfile', () => {
|
|
204
|
+
it('returns first profile name', () => {
|
|
205
|
+
const first = getFirstProfile(AGENT_WITH_PROFILES);
|
|
206
|
+
expect(first).toBeDefined();
|
|
207
|
+
expect(['local', 'production']).toContain(first);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('returns undefined when no profiles', () => {
|
|
211
|
+
expect(getFirstProfile(MINIMAL_AGENT)).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ─────────────────────────────────────────────────────────
|
|
216
|
+
// Tests: getProfile with autonomous config
|
|
217
|
+
// ─────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe('getProfile', () => {
|
|
220
|
+
it('returns undefined for non-existent profile', () => {
|
|
221
|
+
expect(getProfile(AGENT_WITH_PROFILES, 'nonexistent')).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('returns validated profile for existing profile', () => {
|
|
225
|
+
const profile = getProfile(AGENT_WITH_PROFILES, 'production');
|
|
226
|
+
expect(profile).toBeDefined();
|
|
227
|
+
expect(profile!.target).toBe('akash');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('loads profile with secrets config (for autonomous deployment)', () => {
|
|
231
|
+
const profile = getProfile(AGENT_WITH_AUTONOMOUS, 'production');
|
|
232
|
+
expect(profile).toBeDefined();
|
|
233
|
+
expect(profile!.target).toBe('akash');
|
|
234
|
+
expect((profile as any).secrets).toBeDefined();
|
|
235
|
+
expect((profile as any).secrets.vault).toBe('global');
|
|
236
|
+
expect((profile as any).secrets.required).toContain('API_KEY');
|
|
237
|
+
expect((profile as any).secrets.optional).toContain('DEBUG_TOKEN');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous Deployment Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the autonomous deployment flow, CLI flag handling,
|
|
5
|
+
* SecretsProvider shim, and bid filter construction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import type {
|
|
10
|
+
AutonomousDeploymentConfig,
|
|
11
|
+
BidFilterCriteria,
|
|
12
|
+
SecretsProvider,
|
|
13
|
+
} from '@kadi.build/deploy-ability/akash';
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────
|
|
16
|
+
// SecretsProvider shim (mirrors createCliSecretsProvider logic)
|
|
17
|
+
// ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Recreation of the CLI secrets provider for isolated testing
|
|
21
|
+
* (The real one shells out to `kadi secret get`)
|
|
22
|
+
*/
|
|
23
|
+
function createMockSecretsProvider(secrets: Record<string, string>): SecretsProvider {
|
|
24
|
+
return {
|
|
25
|
+
async getMnemonic(): Promise<string> {
|
|
26
|
+
const mnemonic = secrets['AKASH_WALLET'];
|
|
27
|
+
if (!mnemonic) {
|
|
28
|
+
throw new Error('Wallet mnemonic not found');
|
|
29
|
+
}
|
|
30
|
+
return mnemonic;
|
|
31
|
+
},
|
|
32
|
+
async getCertificate() {
|
|
33
|
+
const certJson = secrets['akash-tls-certificate'];
|
|
34
|
+
if (!certJson) return null;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(certJson);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async storeCertificate(_cert) {
|
|
42
|
+
// no-op in mock
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('SecretsProvider shim', () => {
|
|
48
|
+
it('returns mnemonic from vault', async () => {
|
|
49
|
+
const provider = createMockSecretsProvider({
|
|
50
|
+
AKASH_WALLET: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const mnemonic = await provider.getMnemonic();
|
|
54
|
+
expect(mnemonic).toContain('abandon');
|
|
55
|
+
expect(mnemonic.split(' ')).toHaveLength(12);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('throws when mnemonic is missing', async () => {
|
|
59
|
+
const provider = createMockSecretsProvider({});
|
|
60
|
+
await expect(provider.getMnemonic()).rejects.toThrow('not found');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns null for missing certificate', async () => {
|
|
64
|
+
const provider = createMockSecretsProvider({ AKASH_WALLET: 'test' });
|
|
65
|
+
const cert = await provider.getCertificate!();
|
|
66
|
+
expect(cert).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('parses stored certificate JSON', async () => {
|
|
70
|
+
const certData = {
|
|
71
|
+
privateKey: 'pk',
|
|
72
|
+
publicKey: 'pub',
|
|
73
|
+
cert: 'cert-pem',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const provider = createMockSecretsProvider({
|
|
77
|
+
AKASH_WALLET: 'test',
|
|
78
|
+
'akash-tls-certificate': JSON.stringify(certData),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const cert = await provider.getCertificate!();
|
|
82
|
+
expect(cert).toEqual(certData);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns null for malformed certificate JSON', async () => {
|
|
86
|
+
const provider = createMockSecretsProvider({
|
|
87
|
+
AKASH_WALLET: 'test',
|
|
88
|
+
'akash-tls-certificate': 'not-json',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const cert = await provider.getCertificate!();
|
|
92
|
+
expect(cert).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─────────────────────────────────────────────────────────
|
|
97
|
+
// AutonomousDeploymentConfig construction
|
|
98
|
+
// ─────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe('AutonomousDeploymentConfig construction from CLI flags', () => {
|
|
101
|
+
it('builds config with cheapest strategy (default)', () => {
|
|
102
|
+
const provider = createMockSecretsProvider({ AKASH_WALLET: 'test' });
|
|
103
|
+
|
|
104
|
+
const config: AutonomousDeploymentConfig = {
|
|
105
|
+
secrets: provider,
|
|
106
|
+
bidStrategy: 'cheapest',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
expect(config.bidStrategy).toBe('cheapest');
|
|
110
|
+
expect(config.bidFilter).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('builds config with bid price filter', () => {
|
|
114
|
+
const provider = createMockSecretsProvider({ AKASH_WALLET: 'test' });
|
|
115
|
+
|
|
116
|
+
const bidFilter: BidFilterCriteria = {
|
|
117
|
+
maxPricePerBlock: 500,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const config: AutonomousDeploymentConfig = {
|
|
121
|
+
secrets: provider,
|
|
122
|
+
bidStrategy: 'balanced',
|
|
123
|
+
bidFilter,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
expect(config.bidFilter?.maxPricePerBlock).toBe(500);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('builds config with audited provider requirement', () => {
|
|
130
|
+
const provider = createMockSecretsProvider({ AKASH_WALLET: 'test' });
|
|
131
|
+
|
|
132
|
+
const config: AutonomousDeploymentConfig = {
|
|
133
|
+
secrets: provider,
|
|
134
|
+
bidStrategy: 'most-reliable',
|
|
135
|
+
bidFilter: {
|
|
136
|
+
requireAudited: true,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
expect(config.bidFilter?.requireAudited).toBe(true);
|
|
141
|
+
expect(config.bidStrategy).toBe('most-reliable');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('builds config with all filter options', () => {
|
|
145
|
+
const provider = createMockSecretsProvider({ AKASH_WALLET: 'test' });
|
|
146
|
+
|
|
147
|
+
const config: AutonomousDeploymentConfig = {
|
|
148
|
+
secrets: provider,
|
|
149
|
+
bidStrategy: 'balanced',
|
|
150
|
+
bidFilter: {
|
|
151
|
+
maxPricePerBlock: 1000,
|
|
152
|
+
requireAudited: true,
|
|
153
|
+
minUptime: { value: 0.95, period: '7d' },
|
|
154
|
+
preferredRegions: ['us-east'],
|
|
155
|
+
requireOnline: true,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
expect(config.bidFilter?.maxPricePerBlock).toBe(1000);
|
|
160
|
+
expect(config.bidFilter?.minUptime?.value).toBe(0.95);
|
|
161
|
+
expect(config.bidFilter?.preferredRegions).toContain('us-east');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ─────────────────────────────────────────────────────────
|
|
166
|
+
// Bid filter construction (mirrors deploy.ts logic)
|
|
167
|
+
// ─────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
describe('bid filter construction from CLI flags', () => {
|
|
170
|
+
interface MockFlags {
|
|
171
|
+
bidMaxPrice?: number;
|
|
172
|
+
requireAudited?: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildBidFilter(flags: MockFlags): BidFilterCriteria | undefined {
|
|
176
|
+
const bidFilter: Record<string, unknown> = {};
|
|
177
|
+
if (flags.bidMaxPrice) {
|
|
178
|
+
bidFilter.maxPricePerBlock = flags.bidMaxPrice;
|
|
179
|
+
}
|
|
180
|
+
if (flags.requireAudited) {
|
|
181
|
+
bidFilter.requireAudited = true;
|
|
182
|
+
}
|
|
183
|
+
return Object.keys(bidFilter).length > 0
|
|
184
|
+
? (bidFilter as BidFilterCriteria)
|
|
185
|
+
: undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
it('returns undefined when no filter flags set', () => {
|
|
189
|
+
expect(buildBidFilter({})).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('builds filter with maxPricePerBlock', () => {
|
|
193
|
+
const filter = buildBidFilter({ bidMaxPrice: 500 });
|
|
194
|
+
expect(filter?.maxPricePerBlock).toBe(500);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('builds filter with requireAudited', () => {
|
|
198
|
+
const filter = buildBidFilter({ requireAudited: true });
|
|
199
|
+
expect(filter?.requireAudited).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('builds filter with both flags', () => {
|
|
203
|
+
const filter = buildBidFilter({ bidMaxPrice: 1000, requireAudited: true });
|
|
204
|
+
expect(filter?.maxPricePerBlock).toBe(1000);
|
|
205
|
+
expect(filter?.requireAudited).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ─────────────────────────────────────────────────────────
|
|
210
|
+
// Tunnel service validation (mirrors deploy.ts logic)
|
|
211
|
+
// ─────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
describe('tunnel service validation', () => {
|
|
214
|
+
const validTunnelServices = ['kadi', 'ngrok', 'serveo', 'localtunnel'] as const;
|
|
215
|
+
|
|
216
|
+
function validateTunnelService(service: string): boolean {
|
|
217
|
+
return validTunnelServices.includes(service as any);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
it('accepts kadi (default)', () => {
|
|
221
|
+
expect(validateTunnelService('kadi')).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('accepts ngrok', () => {
|
|
225
|
+
expect(validateTunnelService('ngrok')).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('accepts serveo', () => {
|
|
229
|
+
expect(validateTunnelService('serveo')).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('accepts localtunnel', () => {
|
|
233
|
+
expect(validateTunnelService('localtunnel')).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('rejects bore (removed)', () => {
|
|
237
|
+
expect(validateTunnelService('bore')).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('rejects invalid services', () => {
|
|
241
|
+
expect(validateTunnelService('cloudflare')).toBe(false);
|
|
242
|
+
expect(validateTunnelService('')).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|