lyra-ai-agent 0.1.9 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lyra-ai-agent",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "lyra CLI: a Sui-native, policy-bound AI finance agent. Real on-chain work gated by deterministic policy, simulation, and approval",
6
6
  "license": "MIT",
@@ -35,11 +35,11 @@
35
35
  "@mysten/sui": "^1.45.2",
36
36
  "@opentui/core": "^0.1.97",
37
37
  "@opentui/solid": "^0.1.97",
38
- "lyra-core": "^0.1.2",
39
- "lyra-gateway": "^0.1.6",
40
- "lyra-plugin-onchain": "^0.1.9",
41
- "lyra-plugin-system": "^0.1.2",
42
- "lyra-plugin-telegram": "^0.1.2",
38
+ "lyra-core": "^0.1.10",
39
+ "lyra-gateway": "^0.1.10",
40
+ "lyra-plugin-onchain": "^0.1.10",
41
+ "lyra-plugin-system": "^0.1.10",
42
+ "lyra-plugin-telegram": "^0.1.10",
43
43
  "picocolors": "^1.1.1",
44
44
  "qrcode-terminal": "^0.12.0",
45
45
  "solid-js": "^1.9.12"
@@ -264,9 +264,9 @@ export async function runChat(opts?: { cwd?: string; yolo?: boolean }): Promise<
264
264
  )
265
265
  // Onchain side-band ctx: the Sui client + agent keypair drive every PTB, and
266
266
  // the deterministic policy (mirrored on-chain by lyra::policy) bounds writes.
267
- let onchain: ReturnType<typeof buildOnchainContext> | undefined
267
+ let onchain: Awaited<ReturnType<typeof buildOnchainContext>> | undefined
268
268
  if (pluginNames.includes('onchain')) {
269
- onchain = buildOnchainContext({
269
+ onchain = await buildOnchainContext({
270
270
  agent,
271
271
  network: config.network,
272
272
  agentDir: paths.dir,
@@ -278,11 +278,13 @@ export async function runChat(opts?: { cwd?: string; yolo?: boolean }): Promise<
278
278
  // works regardless of which plugins loaded.
279
279
  const balanceClient =
280
280
  onchain?.client ??
281
- buildOnchainContext({
282
- agent,
283
- network: config.network,
284
- agentDir: paths.dir,
285
- }).client
281
+ (
282
+ await buildOnchainContext({
283
+ agent,
284
+ network: config.network,
285
+ agentDir: paths.dir,
286
+ })
287
+ ).client
286
288
  // Phase 12: telegram side-band ctx. We build the runtime context now (before
287
289
  // brain.init) so the plugin can register its listener via ctx.registerListener,
288
290
  // but the dispatch callback is deferred — the slot's `.current` is null until
@@ -68,7 +68,7 @@ export async function runDemo(opts: DemoOpts = {}): Promise<void> {
68
68
 
69
69
  console.log(`lyra demo — agent ${agent.address} on ${config.network}\n`)
70
70
 
71
- const onchain = buildOnchainContext({
71
+ const onchain = await buildOnchainContext({
72
72
  agent,
73
73
  network: config.network,
74
74
  agentDir: found.path,
@@ -9,7 +9,7 @@
9
9
 
10
10
  /** Deployed `lyra::policy` Move package on mainnet (on-chain receipts). */
11
11
  export const DEFAULT_PACKAGE_ID =
12
- '0x8e984145d636037cebf5c402ac4b338567411ba6dd275948d7ff593b1ed01a04'
12
+ '0x8ffdbda0bec2e3604757d435c567d52451317ed5752cef8fc5321a1050872cbf'
13
13
 
14
14
  /** Default Sui network. */
15
15
  export const DEFAULT_NETWORK = 'mainnet' as const
@@ -20,6 +20,7 @@ import {
20
20
  keypairFromSecret,
21
21
  makeSuiClient,
22
22
  policyFromEnv,
23
+ resolveVaultForAgent,
23
24
  } from 'lyra-plugin-onchain'
24
25
  import { resolvePackageId, resolvePolicyEnv } from '../config/defaults'
25
26
 
@@ -97,11 +98,16 @@ export interface BuildOnchainOpts {
97
98
  * wrote (`LYRA_PACKAGE_ID` / `LYRA_POLICY_OBJECT_ID`); the off-chain policy
98
99
  * mirror comes from `policyFromEnv()` (the `LYRA_POLICY_*` vars).
99
100
  */
100
- export function buildOnchainContext(opts: BuildOnchainOpts): OnchainRuntimeContext {
101
+ export async function buildOnchainContext(opts: BuildOnchainOpts): Promise<OnchainRuntimeContext> {
101
102
  // packageId: explicit opt wins, then LYRA_PACKAGE_ID, then the deployed
102
103
  // mainnet default — so on-chain receipts work with zero env config.
103
104
  const packageId = opts.packageId ?? resolvePackageId()
104
- const policyObjectId = opts.policyObjectId ?? process.env.LYRA_POLICY_OBJECT_ID ?? undefined
105
+ // Auto-resolve this agent's treasury vault (if provisioned) so DeFi tools draw
106
+ // from the vault via the policy-gated vault_spend; falls back to the agent's own
107
+ // SUI when there's none. Owner-agnostic (found by the agent tag on-chain).
108
+ const vault = await resolveVaultForAgent(opts.agent.address, opts.network).catch(() => null)
109
+ const policyObjectId =
110
+ opts.policyObjectId ?? process.env.LYRA_POLICY_OBJECT_ID ?? vault?.policyId ?? undefined
105
111
  return {
106
112
  client: makeSuiClient(opts.network),
107
113
  keypair: opts.agent.keypair,
@@ -112,6 +118,9 @@ export function buildOnchainContext(opts: BuildOnchainOpts): OnchainRuntimeConte
112
118
  policy: policyFromEnv(resolvePolicyEnv()),
113
119
  packageId: packageId || undefined,
114
120
  policyObjectId: policyObjectId || undefined,
121
+ vaultId: vault?.vaultId,
122
+ vaultMist: vault?.vaultMist,
123
+ ownerAddress: vault?.owner,
115
124
  agentDir: opts.agentDir,
116
125
  brainProvider: opts.brainProvider ?? null,
117
126
  brainModel: opts.brainModel ?? null,
@@ -1,104 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
- import { mkdtempSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
6
- import { type LoginDeps, deviceLink, resolveWebBase } from './login'
7
-
8
- // Isolate the agent dir so writeAgentKey() (called on approval) doesn't touch
9
- // the real ~/.lyra.
10
- let prevRoot: string | undefined
11
- beforeEach(() => {
12
- prevRoot = process.env.LYRA_ROOT
13
- process.env.LYRA_ROOT = mkdtempSync(join(tmpdir(), 'lyra-login-'))
14
- })
15
- afterEach(() => {
16
- if (prevRoot === undefined) Reflect.deleteProperty(process.env, 'LYRA_ROOT')
17
- else process.env.LYRA_ROOT = prevRoot
18
- })
19
-
20
- /** Build a mock fetch over a scripted sequence of poll responses. */
21
- function mockFetch(start: unknown, polls: unknown[]): { fetchImpl: typeof fetch; calls: string[] } {
22
- const calls: string[] = []
23
- let pollIdx = 0
24
- const fetchImpl = (async (url: string | URL | Request) => {
25
- const u = String(url)
26
- calls.push(u)
27
- if (u.includes('/api/cli/login/start')) {
28
- return new Response(JSON.stringify(start), { status: 200 })
29
- }
30
- if (u.includes('/api/cli/login/poll')) {
31
- const body = polls[Math.min(pollIdx, polls.length - 1)]
32
- pollIdx += 1
33
- return new Response(JSON.stringify(body), { status: 200 })
34
- }
35
- return new Response('not found', { status: 404 })
36
- }) as unknown as typeof fetch
37
- return { fetchImpl, calls }
38
- }
39
-
40
- const baseDeps = (fetchImpl: typeof fetch): LoginDeps => ({
41
- base: 'https://example.test',
42
- fetchImpl,
43
- sleep: async () => {},
44
- log: () => {},
45
- openBrowser: () => {},
46
- })
47
-
48
- describe('resolveWebBase', () => {
49
- test('defaults to lyraai.space, env override wins', () => {
50
- expect(resolveWebBase({})).toBe('https://lyraai.space')
51
- expect(resolveWebBase({ LYRA_WEB_URL: 'http://localhost:3000' })).toBe('http://localhost:3000')
52
- })
53
- })
54
-
55
- describe('deviceLink contract', () => {
56
- test('calls start then poll, writes key on approved, returns address', async () => {
57
- const kp = new Ed25519Keypair()
58
- const agentKey = kp.getSecretKey()
59
- const owner = `0x${'a'.repeat(64)}`
60
- const { fetchImpl, calls } = mockFetch(
61
- { code: 'ABCD', pollToken: 'tok-1', verifyUrl: 'https://example.test/cli', expiresInSec: 60 },
62
- [{ status: 'pending' }, { status: 'approved', owner, agentKey }],
63
- )
64
- const res = await deviceLink(baseDeps(fetchImpl))
65
- expect(res.owner).toBe(owner)
66
- expect(res.address).toBe(kp.toSuiAddress())
67
- // start first, then at least two polls.
68
- expect(calls[0]).toContain('/api/cli/login/start')
69
- expect(calls[1]).toContain('/api/cli/login/poll?pollToken=tok-1')
70
- expect(calls.length).toBeGreaterThanOrEqual(3)
71
- })
72
-
73
- test('throws a friendly error on expired', async () => {
74
- const { fetchImpl } = mockFetch(
75
- { code: 'X', pollToken: 't', verifyUrl: 'https://example.test/cli', expiresInSec: 60 },
76
- [{ status: 'expired' }],
77
- )
78
- await expect(deviceLink(baseDeps(fetchImpl))).rejects.toThrow(/expired/i)
79
- })
80
-
81
- test('throws on not_found', async () => {
82
- const { fetchImpl } = mockFetch(
83
- { code: 'X', pollToken: 't', verifyUrl: 'https://example.test/cli', expiresInSec: 60 },
84
- [{ status: 'not_found' }],
85
- )
86
- await expect(deviceLink(baseDeps(fetchImpl))).rejects.toThrow(/not found/i)
87
- })
88
-
89
- test('times out when expiresInSec elapses without approval', async () => {
90
- const { fetchImpl } = mockFetch(
91
- { code: 'X', pollToken: 't', verifyUrl: 'https://example.test/cli', expiresInSec: 4 },
92
- [{ status: 'pending' }],
93
- )
94
- // Advancing clock: each read jumps 1s, so a 4s window closes after a few
95
- // polls without ever hitting the wall clock.
96
- let t = 0
97
- const now = (): number => {
98
- t += 1000
99
- return t
100
- }
101
- const deps: LoginDeps = { ...baseDeps(fetchImpl), now }
102
- await expect(deviceLink(deps)).rejects.toThrow(/timed out/i)
103
- })
104
- })
@@ -1,88 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
- import { parsePairingArgs } from './pairing'
3
-
4
- describe('parsePairingArgs', () => {
5
- it('errors on no args', () => {
6
- const r = parsePairingArgs([])
7
- expect('error' in r).toBe(true)
8
- })
9
-
10
- it('errors on unknown subcommand', () => {
11
- const r = parsePairingArgs(['quack'])
12
- expect('error' in r).toBe(true)
13
- })
14
-
15
- it('parses `list`', () => {
16
- const r = parsePairingArgs(['list']) as Exclude<
17
- ReturnType<typeof parsePairingArgs>,
18
- { error: string }
19
- >
20
- expect(r.sub).toBe('list')
21
- })
22
-
23
- it('parses `list telegram` with platform filter', () => {
24
- const r = parsePairingArgs(['list', 'telegram']) as Exclude<
25
- ReturnType<typeof parsePairingArgs>,
26
- { error: string }
27
- >
28
- expect(r.sub).toBe('list')
29
- expect(r.platform).toBe('telegram')
30
- })
31
-
32
- it('parses `approve telegram ABCDEFGH`', () => {
33
- const r = parsePairingArgs(['approve', 'telegram', 'ABCDEFGH']) as Exclude<
34
- ReturnType<typeof parsePairingArgs>,
35
- { error: string }
36
- >
37
- expect(r.sub).toBe('approve')
38
- expect(r.platform).toBe('telegram')
39
- expect(r.code).toBe('ABCDEFGH')
40
- })
41
-
42
- it('errors on `approve` without arguments', () => {
43
- const r = parsePairingArgs(['approve'])
44
- expect('error' in r).toBe(true)
45
- })
46
-
47
- it('errors on `approve telegram` without code', () => {
48
- const r = parsePairingArgs(['approve', 'telegram'])
49
- expect('error' in r).toBe(true)
50
- })
51
-
52
- it('parses `revoke telegram 12345`', () => {
53
- const r = parsePairingArgs(['revoke', 'telegram', '12345']) as Exclude<
54
- ReturnType<typeof parsePairingArgs>,
55
- { error: string }
56
- >
57
- expect(r.sub).toBe('revoke')
58
- expect(r.platform).toBe('telegram')
59
- expect(r.userId).toBe('12345')
60
- })
61
-
62
- it('parses `clear-pending` and `clear-pending telegram`', () => {
63
- const a = parsePairingArgs(['clear-pending']) as Exclude<
64
- ReturnType<typeof parsePairingArgs>,
65
- { error: string }
66
- >
67
- expect(a.sub).toBe('clear-pending')
68
- expect(a.platform).toBeUndefined()
69
- const b = parsePairingArgs(['clear-pending', 'telegram']) as Exclude<
70
- ReturnType<typeof parsePairingArgs>,
71
- { error: string }
72
- >
73
- expect(b.platform).toBe('telegram')
74
- })
75
-
76
- it('extracts --yes / -y flag', () => {
77
- const r = parsePairingArgs(['revoke', 'telegram', '12345', '--yes']) as Exclude<
78
- ReturnType<typeof parsePairingArgs>,
79
- { error: string }
80
- >
81
- expect(r.yes).toBe(true)
82
- const s = parsePairingArgs(['clear-pending', '-y']) as Exclude<
83
- ReturnType<typeof parsePairingArgs>,
84
- { error: string }
85
- >
86
- expect(s.yes).toBe(true)
87
- })
88
- })
@@ -1,50 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
- import { parseTelegramArgs } from './telegram'
3
-
4
- describe('parseTelegramArgs', () => {
5
- it('errors on missing subcommand', () => {
6
- const r = parseTelegramArgs([])
7
- expect('error' in r).toBe(true)
8
- if ('error' in r) expect(r.error).toContain('usage')
9
- })
10
-
11
- it('errors on unknown subcommand', () => {
12
- const r = parseTelegramArgs(['nuke'])
13
- expect('error' in r).toBe(true)
14
- if ('error' in r) expect(r.error).toContain('nuke')
15
- })
16
-
17
- it('parses setup', () => {
18
- const r = parseTelegramArgs(['setup'])
19
- expect('error' in r).toBe(false)
20
- if (!('error' in r)) expect(r.sub).toBe('setup')
21
- })
22
-
23
- it('parses status', () => {
24
- const r = parseTelegramArgs(['status'])
25
- if (!('error' in r)) expect(r.sub).toBe('status')
26
- })
27
-
28
- it('parses remove without --yes', () => {
29
- const r = parseTelegramArgs(['remove'])
30
- if (!('error' in r)) {
31
- expect(r.sub).toBe('remove')
32
- expect(r.yes).toBeFalsy()
33
- }
34
- })
35
-
36
- it('parses remove --yes', () => {
37
- const r = parseTelegramArgs(['remove', '--yes'])
38
- if (!('error' in r)) {
39
- expect(r.sub).toBe('remove')
40
- expect(r.yes).toBe(true)
41
- }
42
- })
43
-
44
- it('parses remove -y', () => {
45
- const r = parseTelegramArgs(['remove', '-y'])
46
- if (!('error' in r)) {
47
- expect(r.yes).toBe(true)
48
- }
49
- })
50
- })
@@ -1,95 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import {
3
- DEFAULT_ALLOWED_COINS,
4
- DEFAULT_ALLOWED_PROTOCOLS,
5
- DEFAULT_AUTO_MAX_SUI,
6
- DEFAULT_LLM_BASE_URL,
7
- DEFAULT_LLM_MODEL,
8
- DEFAULT_MAX_PER_TX_SUI,
9
- DEFAULT_MAX_SLIPPAGE_BPS,
10
- DEFAULT_NETWORK,
11
- DEFAULT_PACKAGE_ID,
12
- defaultPolicyEnv,
13
- hasNoPolicyEnv,
14
- resolveLlmBaseUrl,
15
- resolveLlmModel,
16
- resolveNetwork,
17
- resolvePackageId,
18
- resolvePolicyEnv,
19
- } from './defaults'
20
-
21
- describe('config defaults (zero-env-var)', () => {
22
- test('exported constant values match the shared contract', () => {
23
- expect(DEFAULT_PACKAGE_ID).toBe(
24
- '0x8e984145d636037cebf5c402ac4b338567411ba6dd275948d7ff593b1ed01a04',
25
- )
26
- expect(DEFAULT_NETWORK).toBe('mainnet')
27
- expect(DEFAULT_LLM_BASE_URL).toBe('https://api.openai.com/v1')
28
- expect(DEFAULT_LLM_MODEL).toBe('gpt-4o-mini')
29
- expect(DEFAULT_MAX_PER_TX_SUI).toBe(1)
30
- expect(DEFAULT_AUTO_MAX_SUI).toBe(0.1)
31
- expect(DEFAULT_MAX_SLIPPAGE_BPS).toBe(100)
32
- expect(DEFAULT_ALLOWED_COINS).toEqual(['0x2::sui::SUI'])
33
- expect(DEFAULT_ALLOWED_PROTOCOLS).toEqual([
34
- 'transfer',
35
- 'swap',
36
- 'stake',
37
- 'borrow',
38
- 'deepbook',
39
- 'scallop',
40
- 'navi',
41
- 'suilend',
42
- 'walrus',
43
- ])
44
- })
45
-
46
- test('resolvePackageId: default when env unset, env wins when set', () => {
47
- expect(resolvePackageId({})).toBe(DEFAULT_PACKAGE_ID)
48
- expect(resolvePackageId({ LYRA_PACKAGE_ID: '0xabc' })).toBe('0xabc')
49
- // Blank/whitespace env falls back to the default.
50
- expect(resolvePackageId({ LYRA_PACKAGE_ID: ' ' })).toBe(DEFAULT_PACKAGE_ID)
51
- })
52
-
53
- test('resolveNetwork: default mainnet, env override, ignores garbage', () => {
54
- expect(resolveNetwork({})).toBe('mainnet')
55
- expect(resolveNetwork({ LYRA_NETWORK: 'testnet' })).toBe('testnet')
56
- expect(resolveNetwork({ LYRA_NETWORK: 'devnet' })).toBe('mainnet')
57
- })
58
-
59
- test('resolveLlmBaseUrl / resolveLlmModel: default then env override', () => {
60
- expect(resolveLlmBaseUrl({})).toBe(DEFAULT_LLM_BASE_URL)
61
- expect(resolveLlmBaseUrl({ LYRA_LLM_BASE_URL: 'https://gw' })).toBe('https://gw')
62
- expect(resolveLlmModel({})).toBe(DEFAULT_LLM_MODEL)
63
- expect(resolveLlmModel({ LYRA_LLM_MODEL: 'gpt-4o' })).toBe('gpt-4o')
64
- })
65
-
66
- test('defaultPolicyEnv produces a bounded LYRA_POLICY_* record', () => {
67
- const env = defaultPolicyEnv()
68
- expect(env.LYRA_POLICY_MAX_PER_TX_SUI).toBe('1')
69
- expect(env.LYRA_POLICY_AUTO_MAX_SUI).toBe('0.1')
70
- expect(env.LYRA_POLICY_MAX_SLIPPAGE_BPS).toBe('100')
71
- expect(env.LYRA_POLICY_ALLOWED_COINS).toBe('0x2::sui::SUI')
72
- expect(env.LYRA_POLICY_ALLOWED_PROTOCOLS).toBe(
73
- 'transfer,swap,stake,borrow,deepbook,scallop,navi,suilend,walrus',
74
- )
75
- })
76
-
77
- test('hasNoPolicyEnv: true with no policy vars, false when any set', () => {
78
- expect(hasNoPolicyEnv({})).toBe(true)
79
- expect(hasNoPolicyEnv({ OPENAI_API_KEY: 'x' })).toBe(true)
80
- expect(hasNoPolicyEnv({ LYRA_POLICY_MAX_PER_TX_SUI: '5' })).toBe(false)
81
- })
82
-
83
- test('resolvePolicyEnv: seeds defaults only when no policy var is set', () => {
84
- // No policy env → defaults applied.
85
- const seeded = resolvePolicyEnv({})
86
- expect(seeded.LYRA_POLICY_MAX_PER_TX_SUI).toBe('1')
87
- expect(seeded.LYRA_POLICY_ALLOWED_PROTOCOLS).toContain('deepbook')
88
-
89
- // Any policy env present → caller's env passed through untouched (no merge).
90
- const userEnv = { LYRA_POLICY_MAX_PER_TX_SUI: '5' }
91
- const passed = resolvePolicyEnv(userEnv)
92
- expect(passed.LYRA_POLICY_MAX_PER_TX_SUI).toBe('5')
93
- expect(passed.LYRA_POLICY_ALLOWED_PROTOCOLS).toBeUndefined()
94
- })
95
- })
@@ -1,96 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import type { LyraConfig } from 'lyra-core'
3
- import { renderConfigTs } from './render'
4
-
5
- const baseConfig: LyraConfig = {
6
- identity: { operator: null, agent: null },
7
- network: 'mainnet',
8
- storage: { network: 'mainnet' },
9
- brain: { provider: null, model: null },
10
- plugins: ['system'],
11
- tools: {},
12
- imports: { claudeCode: true },
13
- }
14
-
15
- describe('renderConfigTs sandbox block', () => {
16
- test('fresh config (no sandbox set) emits default + commented examples for each tier', () => {
17
- const out = renderConfigTs(baseConfig)
18
- // Active default
19
- expect(out).toContain(`sandbox: { mode: 'none' }`)
20
- // Commented OPTION 2 (os)
21
- expect(out).toContain('OPTION 2: os')
22
- expect(out).toContain(`// sandbox: { mode: 'os' }`)
23
- // Commented OPTION 3 (docker)
24
- expect(out).toContain('OPTION 3: docker')
25
- expect(out).toContain(`// mode: 'docker'`)
26
- expect(out).toContain(`// dockerImage: 'nikolaik/python-nodejs:python3.11-nodejs20'`)
27
- expect(out).toContain('// dockerMountWorkspace: false')
28
- // LYRA_SANDBOX_MODE override hint
29
- expect(out).toContain('LYRA_SANDBOX_MODE')
30
- })
31
-
32
- test('config with sandbox.mode="os" already set emits the chosen value, not the template', () => {
33
- const out = renderConfigTs({ ...baseConfig, sandbox: { mode: 'os' } })
34
- expect(out).toContain(`"mode": "os"`)
35
- // No verbose template when operator already chose
36
- expect(out).not.toContain('OPTION 2: os')
37
- expect(out).not.toContain('OPTION 3: docker')
38
- })
39
-
40
- test('config with sandbox.mode="docker" + image emits the full chosen object', () => {
41
- const out = renderConfigTs({
42
- ...baseConfig,
43
- sandbox: {
44
- mode: 'docker',
45
- dockerImage: 'custom/img:tag',
46
- dockerMountWorkspace: true,
47
- },
48
- })
49
- expect(out).toContain(`"mode": "docker"`)
50
- expect(out).toContain(`"dockerImage": "custom/img:tag"`)
51
- expect(out).toContain(`"dockerMountWorkspace": true`)
52
- expect(out).not.toContain('OPTION')
53
- })
54
-
55
- test('annotated template documents resource caps with hermes default values', () => {
56
- const out = renderConfigTs(baseConfig)
57
- expect(out).toContain('// dockerCpu: 1')
58
- expect(out).toContain('// dockerMemoryMb: 5120')
59
- expect(out).toContain('// dockerDiskMb: 51200')
60
- expect(out).toContain('// dockerNoNetwork: true')
61
- })
62
-
63
- test('config with sandbox docker + resource caps emits chosen numeric values', () => {
64
- const out = renderConfigTs({
65
- ...baseConfig,
66
- sandbox: {
67
- mode: 'docker',
68
- dockerCpu: 2,
69
- dockerMemoryMb: 4096,
70
- dockerNoNetwork: true,
71
- },
72
- })
73
- expect(out).toContain(`"dockerCpu": 2`)
74
- expect(out).toContain(`"dockerMemoryMb": 4096`)
75
- expect(out).toContain(`"dockerNoNetwork": true`)
76
- expect(out).not.toContain('OPTION')
77
- })
78
-
79
- test('output is valid TypeScript (parses as a default-export module)', async () => {
80
- const out = renderConfigTs(baseConfig)
81
- const { writeFile, rm, mkdtemp } = await import('node:fs/promises')
82
- const { tmpdir } = await import('node:os')
83
- const { join } = await import('node:path')
84
- const dir = await mkdtemp(join(tmpdir(), 'lyra-render-test-'))
85
- const path = join(dir, 'config.ts')
86
- try {
87
- await writeFile(path, out, 'utf8')
88
- const mod = await import(path)
89
- expect(mod.default).toBeDefined()
90
- expect(mod.default.sandbox).toEqual({ mode: 'none' })
91
- expect(mod.default.network).toBe('mainnet')
92
- } finally {
93
- await rm(dir, { recursive: true, force: true })
94
- }
95
- })
96
- })
@@ -1,132 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
- import { summarizeApprovalSubject } from './approval-summary'
3
-
4
- describe('summarizeApprovalSubject', () => {
5
- it('renders chain.send native with amount + recipient', () => {
6
- expect(
7
- summarizeApprovalSubject({
8
- kind: 'chain.send',
9
- amount: '0.001',
10
- recipient: '0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec',
11
- token: 'SUI',
12
- reason: 'SUI transfer',
13
- }),
14
- ).toBe('send 0.001 SUI to 0xC635…87Ec')
15
- })
16
-
17
- it('renders chain.send coin with explicit token symbol', () => {
18
- expect(
19
- summarizeApprovalSubject({
20
- kind: 'chain.send',
21
- amount: '0.5',
22
- recipient: '0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec',
23
- token: 'USDC',
24
- reason: 'coin transfer',
25
- }),
26
- ).toBe('send 0.5 USDC to 0xC635…87Ec')
27
- })
28
-
29
- it('renders chain.send native fallback label when token omitted', () => {
30
- expect(
31
- summarizeApprovalSubject({
32
- kind: 'chain.send',
33
- amount: '0.001',
34
- recipient: '0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec',
35
- reason: 'SUI transfer',
36
- }),
37
- ).toBe('send 0.001 SUI to 0xC635…87Ec')
38
- })
39
-
40
- it('renders the arrow form (no recipient noise)', () => {
41
- expect(
42
- summarizeApprovalSubject({
43
- kind: 'chain.send',
44
- amount: '0.01',
45
- token: 'SUI→USDC',
46
- reason: 'wrap',
47
- }),
48
- ).toBe('0.01 SUI→USDC')
49
- })
50
-
51
- it('renders chain.swap with token-pair encoding', () => {
52
- expect(
53
- summarizeApprovalSubject({
54
- kind: 'chain.swap',
55
- amount: '0.005',
56
- token: 'SUI→USDC',
57
- reason: 'Cetus swap execution',
58
- }),
59
- ).toBe('swap 0.005 SUI→USDC')
60
- })
61
-
62
- it('renders chain.swap with empty amt + tok', () => {
63
- expect(
64
- summarizeApprovalSubject({
65
- kind: 'chain.swap',
66
- reason: 'Cetus swap execution',
67
- }),
68
- ).toBe('swap')
69
- })
70
-
71
- it('renders chain.write with signature + recipient + value', () => {
72
- expect(
73
- summarizeApprovalSubject({
74
- kind: 'chain.write',
75
- recipient: '0x9e71d79f06f956d4d2666b5c93dafab721c84721',
76
- command: 'transfer(address,uint256)',
77
- amount: '1 wei',
78
- reason: 'arbitrary state-changing call',
79
- }),
80
- ).toBe('transfer(address,uint256) (value: 1 wei) on 0x9e71…4721')
81
- })
82
-
83
- it('renders chain.write with no recipient (Aave command) without a trailing "on"', () => {
84
- expect(
85
- summarizeApprovalSubject({
86
- kind: 'chain.write',
87
- command: 'aave.borrow 100 USDC',
88
- reason: 'borrow from Aave V3 (leverage)',
89
- }),
90
- ).toBe('aave.borrow 100 USDC')
91
- })
92
-
93
- it('renders chain.write with no value', () => {
94
- expect(
95
- summarizeApprovalSubject({
96
- kind: 'chain.write',
97
- recipient: '0x9e71d79f06f956d4d2666b5c93dafab721c84721',
98
- command: 'totalSupply()',
99
- reason: 'arbitrary state-changing call',
100
- }),
101
- ).toBe('totalSupply() on 0x9e71…4721')
102
- })
103
-
104
- it('falls back to command for shell.run', () => {
105
- expect(
106
- summarizeApprovalSubject({
107
- kind: 'shell.run',
108
- command: 'rm -rf /tmp/foo',
109
- reason: 'shell command execution',
110
- }),
111
- ).toBe('rm -rf /tmp/foo')
112
- })
113
-
114
- it('falls back to path for fs.write', () => {
115
- expect(
116
- summarizeApprovalSubject({
117
- kind: 'fs.write',
118
- path: '/tmp/x.txt',
119
- reason: 'fs.write request',
120
- }),
121
- ).toBe('/tmp/x.txt')
122
- })
123
-
124
- it('falls back to (unspecified) when nothing usable', () => {
125
- expect(
126
- summarizeApprovalSubject({
127
- kind: 'fs.patch',
128
- reason: 'fs.patch request',
129
- }),
130
- ).toBe('(unspecified)')
131
- })
132
- })