@agfpd/iapeer 0.2.77 → 0.2.79

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.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/package.json +21 -2
  4. package/src/cli/index.ts +95 -78
  5. package/src/connect/index.ts +4 -4
  6. package/src/core/constants.ts +3 -3
  7. package/src/core/normalize.ts +2 -2
  8. package/src/create/index.ts +1 -1
  9. package/src/daemon/index.ts +5 -5
  10. package/src/daemon/main.ts +9 -9
  11. package/src/enable/index.ts +19 -18
  12. package/src/enable/provisionCommand.ts +10 -10
  13. package/src/identity/index.ts +2 -2
  14. package/src/identity/profileStandard.ts +8 -8
  15. package/src/init/index.ts +10 -10
  16. package/src/install/index.ts +2 -2
  17. package/src/install/signing.ts +3 -4
  18. package/src/launch/adapters/claude.ts +7 -7
  19. package/src/launch/adapters/codex.ts +5 -6
  20. package/src/launch/canary.ts +8 -8
  21. package/src/launch/cmdlog.ts +3 -3
  22. package/src/launch/composeSystemPrompt.ts +2 -1
  23. package/src/launch/index.ts +5 -6
  24. package/src/launch/invocation.ts +1 -1
  25. package/src/launch/launchd.ts +14 -15
  26. package/src/launch/launchdRun.ts +6 -6
  27. package/src/launch/nativeMemory.ts +9 -9
  28. package/src/launch/ptyHost.ts +1 -1
  29. package/src/launch/readyGateModel.ts +3 -3
  30. package/src/launch/types.ts +7 -7
  31. package/src/lifecycle/eventlog.ts +2 -2
  32. package/src/lifecycle/index.ts +21 -21
  33. package/src/lifecycle/queue.ts +1 -1
  34. package/src/onboard/index.ts +19 -13
  35. package/src/onboard/memory.ts +5 -5
  36. package/src/onboard/steps.ts +8 -9
  37. package/src/provision/index.ts +2 -3
  38. package/src/runtime/deploy.ts +2 -2
  39. package/src/runtime/index.ts +4 -4
  40. package/src/shadow/gate.ts +6 -6
  41. package/src/shadow/index.ts +2 -2
  42. package/src/status/index.ts +11 -11
  43. package/src/storage/index.ts +1 -1
  44. package/src/supervisor/daemon.ts +20 -13
  45. package/src/transport/index.ts +11 -11
  46. package/src/update/index.ts +4 -4
  47. package/src/cli/cli.test.ts +0 -603
  48. package/src/cli/listTui.test.ts +0 -71
  49. package/src/codec/codec.test.ts +0 -271
  50. package/src/connect/connect.test.ts +0 -163
  51. package/src/core/constants.test.ts +0 -32
  52. package/src/core/normalize.test.ts +0 -98
  53. package/src/create/create.test.ts +0 -143
  54. package/src/daemon/daemon-http.test.ts +0 -114
  55. package/src/daemon/daemon.test.ts +0 -267
  56. package/src/daemon/main.test.ts +0 -413
  57. package/src/enable/enable.test.ts +0 -95
  58. package/src/enable/provisionCommand.test.ts +0 -107
  59. package/src/identity/identity.test.ts +0 -279
  60. package/src/identity/profileStandard.test.ts +0 -204
  61. package/src/init/init.test.ts +0 -215
  62. package/src/init/runtime-resolve.test.ts +0 -49
  63. package/src/install/install.test.ts +0 -66
  64. package/src/install/signing.test.ts +0 -84
  65. package/src/launch/bootdeliver.test.ts +0 -108
  66. package/src/launch/bootstrap.test.ts +0 -56
  67. package/src/launch/canary.test.ts +0 -346
  68. package/src/launch/cmdlog.test.ts +0 -94
  69. package/src/launch/codexHooksTrust.test.ts +0 -248
  70. package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
  71. package/src/launch/composeSystemPrompt.test.ts +0 -145
  72. package/src/launch/hostComposerOccupied.test.ts +0 -63
  73. package/src/launch/invocation.test.ts +0 -174
  74. package/src/launch/launch.test.ts +0 -359
  75. package/src/launch/launchd.test.ts +0 -499
  76. package/src/launch/nativeMemory.test.ts +0 -193
  77. package/src/launch/ptyHost.test.ts +0 -38
  78. package/src/launch/readyGateModel.test.ts +0 -72
  79. package/src/launch/sockdir.test.ts +0 -74
  80. package/src/lifecycle/eventlog.test.ts +0 -114
  81. package/src/lifecycle/lifecycle.test.ts +0 -1056
  82. package/src/lifecycle/queue.test.ts +0 -185
  83. package/src/onboard/memory.test.ts +0 -157
  84. package/src/onboard/onboard.test.ts +0 -39
  85. package/src/onboard/steps.test.ts +0 -201
  86. package/src/provision/provision.test.ts +0 -288
  87. package/src/registry/registry.test.ts +0 -432
  88. package/src/runtime/runtime.test.ts +0 -226
  89. package/src/runtime/update.test.ts +0 -192
  90. package/src/shadow/enumerate-skip.test.ts +0 -67
  91. package/src/shadow/gate.test.ts +0 -286
  92. package/src/shadow/install.test.ts +0 -153
  93. package/src/shadow/shadow.test.ts +0 -42
  94. package/src/status/status.test.ts +0 -208
  95. package/src/storage/peers-home.test.ts +0 -34
  96. package/src/storage/rotatelog.test.ts +0 -44
  97. package/src/storage/storage.test.ts +0 -65
  98. package/src/supervisor/attached-marker.test.ts +0 -84
  99. package/src/supervisor/boot.test.ts +0 -66
  100. package/src/supervisor/client.attach.test.ts +0 -107
  101. package/src/supervisor/daemon.test.ts +0 -213
  102. package/src/supervisor/deliver.test.ts +0 -58
  103. package/src/supervisor/panelog.test.ts +0 -95
  104. package/src/supervisor/protocol.test.ts +0 -222
  105. package/src/supervisor/selfargv.test.ts +0 -22
  106. package/src/supervisor/spinner-strip.test.ts +0 -52
  107. package/src/transport/compactDone.test.ts +0 -144
  108. package/src/transport/composerQueue.test.ts +0 -204
  109. package/src/transport/hostControl.test.ts +0 -87
  110. package/src/transport/hostDeliver.test.ts +0 -137
  111. package/src/transport/hostLive.test.ts +0 -50
  112. package/src/transport/routesend-wake.test.ts +0 -357
  113. package/src/update/update.test.ts +0 -208
@@ -1,71 +0,0 @@
1
- // list TUI — the PURE render + key state-machine (the raw-mode loop is live-verified).
2
-
3
- import { describe, expect, test } from 'bun:test'
4
- import { filterRows, handleListKey, renderListPanel, type TuiState } from './listTui.ts'
5
- import type { PeerListing } from './index.ts'
6
-
7
- function row(over: Partial<PeerListing>): PeerListing {
8
- return {
9
- personality: 'p',
10
- default_runtime: 'claude',
11
- intelligence: 'artificial',
12
- description: '',
13
- cwd: '/tmp/p',
14
- runtimes: [{ runtime: 'claude', status: 'asleep' }],
15
- ...over,
16
- }
17
- }
18
- const ROWS: PeerListing[] = [
19
- row({ personality: 'arthur', default_runtime: 'telegram', description: 'owner' }),
20
- row({ personality: 'boris', runtimes: [{ runtime: 'claude', status: 'live' }], last_active_runtime: 'claude' }),
21
- row({ personality: 'doc' }),
22
- ]
23
- const S0: TuiState = { cursor: 0, filter: '', filterMode: false }
24
-
25
- describe('filterRows', () => {
26
- test('case-insensitive substring over personality + description', () => {
27
- expect(filterRows(ROWS, 'BOR').map(r => r.personality)).toEqual(['boris'])
28
- expect(filterRows(ROWS, 'owner').map(r => r.personality)).toEqual(['arthur']) // description match
29
- expect(filterRows(ROWS, '').length).toBe(3) // empty → all
30
- })
31
- })
32
-
33
- describe('renderListPanel', () => {
34
- test('selected row carries the ❯ caret + reverse-video; others do not', () => {
35
- const frame = renderListPanel(ROWS, { ...S0, cursor: 1 })
36
- expect(frame).toContain('\x1b[7m❯ boris') // reverse + caret on the cursor row
37
- expect(frame).toContain('arthur') // other rows present, no caret
38
- expect(frame).not.toContain('❯ arthur')
39
- expect(frame).toContain('● claude') // boris live glyph
40
- })
41
- test('filter narrows the rendered rows', () => {
42
- const frame = renderListPanel(ROWS, { ...S0, filter: 'doc' })
43
- expect(frame).toContain('doc')
44
- expect(frame).not.toContain('arthur')
45
- })
46
- })
47
-
48
- describe('handleListKey', () => {
49
- const visible = ROWS
50
- test('↓/↑ move the cursor, clamped to bounds', () => {
51
- expect(handleListKey('\x1b[B', S0, visible).state.cursor).toBe(1) // down
52
- expect(handleListKey('\x1b[A', S0, visible).state.cursor).toBe(0) // up clamped at 0
53
- expect(handleListKey('\x1b[B', { ...S0, cursor: 2 }, visible).state.cursor).toBe(2) // down clamped at last
54
- })
55
- test('Enter → attach action with the selected peer', () => {
56
- const r = handleListKey('\r', { ...S0, cursor: 1 }, visible)
57
- expect(r.action).toEqual({ type: 'attach', personality: 'boris' })
58
- })
59
- test('q / Ctrl-C → quit', () => {
60
- expect(handleListKey('q', S0, visible).action).toEqual({ type: 'quit' })
61
- expect(handleListKey('\x03', S0, visible).action).toEqual({ type: 'quit' })
62
- })
63
- test('/ enters filter mode; typing edits the filter; Enter leaves it', () => {
64
- const f = handleListKey('/', S0, visible)
65
- expect(f.state.filterMode).toBe(true)
66
- const typed = handleListKey('b', f.state, visible)
67
- expect(typed.state.filter).toBe('b')
68
- const left = handleListKey('\r', typed.state, visible)
69
- expect(left.state.filterMode).toBe(false)
70
- })
71
- })
@@ -1,271 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { buildEnvelope, decodeEnvelope, extractEnvelopes, type EnvelopeInput } from './index.ts'
3
-
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- // WITNESS: faithful copy of the OLD telegram-runtime decoder (cli.ts:867-935).
6
- // These reproduce the pre-fix behaviour so each adversarial test shows a real
7
- // delta: the witness FAILS the assertion the new codec PASSES. (Brief: "тест,
8
- // падающий ДО фикса и проходящий ПОСЛЕ".)
9
- // ─────────────────────────────────────────────────────────────────────────────
10
-
11
- function oldUnescapeAttr(value: string): string {
12
- return value
13
- .replace(/&quot;/g, '"')
14
- .replace(/&lt;/g, '<')
15
- .replace(/&gt;/g, '>')
16
- .replace(/&amp;/g, '&')
17
- }
18
- function oldDecodeCdata(inner: string): string {
19
- if (!inner.startsWith('<![CDATA[') || !inner.endsWith(']]>')) return inner
20
- return inner.slice('<![CDATA['.length, -']]>'.length).replaceAll(']]]]><![CDATA[>', ']]>')
21
- }
22
- function oldTagContent(xml: string, tag: string): string | undefined {
23
- const re = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`)
24
- const m = re.exec(xml)
25
- return m ? oldDecodeCdata(m[1]) : undefined
26
- }
27
- function oldParseMessage(xml: string): string | undefined {
28
- xml = xml.replace(/\r\n?/g, '\n') // the CR→LF fold the codec must NOT do
29
- return oldTagContent(xml, 'message')
30
- }
31
- function oldExtractEnvelopes(buffer: string): { envelopes: string[]; rest: string } {
32
- const envelopes: string[] = []
33
- let rest = buffer
34
- while (true) {
35
- const start = rest.indexOf('<iap ')
36
- if (start < 0) return { envelopes, rest: rest.slice(Math.max(0, rest.length - 8)) }
37
- if (start > 0) rest = rest.slice(start)
38
- const end = rest.indexOf('</iap>') // naive: not CDATA-aware
39
- if (end < 0) return { envelopes, rest }
40
- const envelopeEnd = end + '</iap>'.length
41
- envelopes.push(rest.slice(0, envelopeEnd))
42
- rest = rest.slice(envelopeEnd)
43
- }
44
- }
45
-
46
- const base: Omit<EnvelopeInput, 'message'> = {
47
- fromPersonality: 'arthur',
48
- fromRuntime: 'telegram',
49
- fromIntelligence: 'natural',
50
- topic: 'Ф0 фундамента',
51
- }
52
-
53
- function roundTripMessage(message: string, extra: Partial<EnvelopeInput> = {}): string {
54
- const xml = buildEnvelope({ ...base, ...extra, message })
55
- const { envelopes, rest } = extractEnvelopes(xml)
56
- expect(envelopes).toHaveLength(1)
57
- expect(rest).toBe('')
58
- return decodeEnvelope(envelopes[0]).message
59
- }
60
-
61
- // ─────────────────────────────────────────────────────────────────────────────
62
- // Basic round-trip
63
- // ─────────────────────────────────────────────────────────────────────────────
64
-
65
- describe('codec round-trip (field-semantic equivalence)', () => {
66
- test('preserves all fields', () => {
67
- const input: EnvelopeInput = {
68
- fromPersonality: 'boris',
69
- fromRuntime: 'claude',
70
- fromIntelligence: 'artificial',
71
- topic: 'checkpoint',
72
- attachments: ['/tmp/a.txt', '/tmp/b with space.png'],
73
- message: 'plain multi\nline\nmessage',
74
- }
75
- const decoded = decodeEnvelope(buildEnvelope(input))
76
- expect(decoded.fromPersonality).toBe(input.fromPersonality)
77
- expect(decoded.fromRuntime).toBe(input.fromRuntime)
78
- expect(decoded.fromIntelligence).toBe(input.fromIntelligence)
79
- expect(decoded.topic).toBe(input.topic)
80
- expect(decoded.attachments).toEqual([...input.attachments!])
81
- expect(decoded.message).toBe(input.message)
82
- })
83
-
84
- test('attr special chars (<, >, &, ") survive in personality and topic', () => {
85
- const xml = buildEnvelope({
86
- fromPersonality: 'arthur',
87
- fromRuntime: 'telegram',
88
- fromIntelligence: 'natural',
89
- topic: 'a < b & c > d "quoted"',
90
- message: 'hi',
91
- })
92
- const d = decodeEnvelope(xml)
93
- expect(d.topic).toBe('a < b & c > d "quoted"')
94
- })
95
-
96
- test('empty message round-trips', () => {
97
- expect(roundTripMessage('')).toBe('')
98
- })
99
- })
100
-
101
- // ─────────────────────────────────────────────────────────────────────────────
102
- // Adversarial: message contains </iap> → extractEnvelopes must not truncate
103
- // ─────────────────────────────────────────────────────────────────────────────
104
-
105
- describe('adversarial: </iap> in message', () => {
106
- const message = 'see the closing tag </iap> right here'
107
-
108
- test('NEW codec: single envelope, message intact', () => {
109
- expect(roundTripMessage(message)).toBe(message)
110
- })
111
-
112
- test('WITNESS: old naive indexOf truncates the envelope', () => {
113
- const xml = buildEnvelope({ ...base, message })
114
- const oldFirst = oldExtractEnvelopes(xml).envelopes[0]
115
- // old splits at the inner </iap>, producing a shorter, broken envelope
116
- expect(oldFirst.length).toBeLessThan(xml.length)
117
- // ...and the new extractor produces the full envelope
118
- expect(extractEnvelopes(xml).envelopes[0]).toBe(xml)
119
- })
120
-
121
- test('NEW codec: two real envelopes, first message holds a fake </iap>', () => {
122
- const e1 = buildEnvelope({ ...base, message: 'fake </iap> inside' })
123
- const e2 = buildEnvelope({ ...base, fromPersonality: 'darwin', message: 'second' })
124
- const { envelopes, rest } = extractEnvelopes(e1 + '\n' + e2)
125
- expect(envelopes).toHaveLength(2)
126
- expect(rest).toBe('')
127
- expect(decodeEnvelope(envelopes[0]).message).toBe('fake </iap> inside')
128
- expect(decodeEnvelope(envelopes[1]).message).toBe('second')
129
- // witness: old truncates the first envelope at the inner </iap>, so its
130
- // first fragment is NOT the real e1 (and no longer decodes to a message)
131
- const oldFirst = oldExtractEnvelopes(e1 + '\n' + e2).envelopes[0]
132
- expect(oldFirst).not.toBe(e1)
133
- expect(oldTagContent(oldFirst, 'message')).toBeUndefined()
134
- })
135
- })
136
-
137
- // ─────────────────────────────────────────────────────────────────────────────
138
- // Adversarial: message contains </message> → tag-content must not truncate
139
- // ─────────────────────────────────────────────────────────────────────────────
140
-
141
- describe('adversarial: </message> in message', () => {
142
- const message = 'a </message> b </message> c'
143
-
144
- test('NEW codec: message intact', () => {
145
- expect(roundTripMessage(message)).toBe(message)
146
- })
147
-
148
- test('WITNESS: old non-greedy regex truncates at inner </message>', () => {
149
- const xml = buildEnvelope({ ...base, message })
150
- expect(oldParseMessage(xml)).not.toBe(message)
151
- })
152
- })
153
-
154
- // ─────────────────────────────────────────────────────────────────────────────
155
- // Adversarial: message contains ]]> → CDATA split must reconstruct
156
- // ─────────────────────────────────────────────────────────────────────────────
157
-
158
- describe('adversarial: ]]> in message', () => {
159
- test('NEW codec: literal ]]> reconstructed', () => {
160
- expect(roundTripMessage('a]]>b')).toBe('a]]>b')
161
- expect(roundTripMessage('edge ]]> at ]]> several ]]> places')).toBe(
162
- 'edge ]]> at ]]> several ]]> places',
163
- )
164
- expect(roundTripMessage(']]>')).toBe(']]>')
165
- })
166
- })
167
-
168
- // ─────────────────────────────────────────────────────────────────────────────
169
- // Adversarial: literal CR → codec must NOT fold (transport's job)
170
- // ─────────────────────────────────────────────────────────────────────────────
171
-
172
- describe('adversarial: literal CR is preserved (no fold in codec)', () => {
173
- const message = 'line1\rline2\r\nline3'
174
-
175
- test('NEW codec: CR survives round-trip', () => {
176
- expect(roundTripMessage(message)).toBe(message)
177
- })
178
-
179
- test('WITNESS: old decoder folds CR→LF and loses it', () => {
180
- const xml = buildEnvelope({ ...base, message })
181
- const folded = oldParseMessage(xml)
182
- expect(folded).not.toBe(message)
183
- expect(folded).toBe('line1\nline2\nline3') // proof of the lossy fold
184
- })
185
- })
186
-
187
- // ─────────────────────────────────────────────────────────────────────────────
188
- // Headline: all four adversarial features at once
189
- // ─────────────────────────────────────────────────────────────────────────────
190
-
191
- describe('adversarial: combined </iap> </message> ]]> CR', () => {
192
- const message = 'X </iap> Y </message> Z ]]> W\rtail\nend'
193
-
194
- test('NEW codec: full round-trip by field semantics', () => {
195
- const input: EnvelopeInput = {
196
- ...base,
197
- attachments: ['/path/with ]]> weird', '/second </iap> path'],
198
- message,
199
- }
200
- const xml = buildEnvelope(input)
201
- const { envelopes, rest } = extractEnvelopes(xml)
202
- expect(envelopes).toHaveLength(1)
203
- expect(rest).toBe('')
204
- const d = decodeEnvelope(envelopes[0])
205
- expect(d.message).toBe(message)
206
- expect(d.attachments).toEqual(['/path/with ]]> weird', '/second </iap> path'])
207
- expect(d.fromPersonality).toBe('arthur')
208
- expect(d.fromIntelligence).toBe('natural')
209
- })
210
- })
211
-
212
- // ─────────────────────────────────────────────────────────────────────────────
213
- // extractEnvelopes streaming semantics
214
- // ─────────────────────────────────────────────────────────────────────────────
215
-
216
- describe('extractEnvelopes streaming', () => {
217
- test('incomplete envelope kept in rest, completed on next chunk', () => {
218
- const xml = buildEnvelope({ ...base, message: 'streamed' })
219
- const cut = Math.floor(xml.length / 2)
220
- const first = extractEnvelopes(xml.slice(0, cut))
221
- expect(first.envelopes).toHaveLength(0)
222
- const second = extractEnvelopes(first.rest + xml.slice(cut))
223
- expect(second.envelopes).toHaveLength(1)
224
- expect(decodeEnvelope(second.envelopes[0]).message).toBe('streamed')
225
- })
226
-
227
- test('mid-CDATA cut does not falsely close on inner </iap>', () => {
228
- const xml = buildEnvelope({ ...base, message: 'pre </iap> post' })
229
- // cut right after the inner </iap> but before the CDATA terminator
230
- const innerIap = xml.indexOf('</iap>')
231
- const cut = innerIap + 3
232
- const first = extractEnvelopes(xml.slice(0, cut))
233
- expect(first.envelopes).toHaveLength(0) // must NOT emit a truncated envelope
234
- const second = extractEnvelopes(first.rest + xml.slice(cut))
235
- expect(second.envelopes).toHaveLength(1)
236
- expect(decodeEnvelope(second.envelopes[0]).message).toBe('pre </iap> post')
237
- })
238
-
239
- test('leading noise before <iap is discarded', () => {
240
- const xml = buildEnvelope({ ...base, message: 'm' })
241
- const { envelopes } = extractEnvelopes('garbage text\n' + xml)
242
- expect(envelopes).toHaveLength(1)
243
- expect(decodeEnvelope(envelopes[0]).message).toBe('m')
244
- })
245
- })
246
-
247
- // ─────────────────────────────────────────────────────────────────────────────
248
- // Decoder error surface
249
- // ─────────────────────────────────────────────────────────────────────────────
250
-
251
- describe('decodeEnvelope errors', () => {
252
- test('missing <iap throws', () => {
253
- expect(() => decodeEnvelope('no envelope here')).toThrow()
254
- })
255
- test('missing message throws', () => {
256
- expect(() => decodeEnvelope('<iap from-personality="a" from-runtime="claude"></iap>')).toThrow()
257
- })
258
- test('unknown from-intelligence is dropped, not invented', () => {
259
- const xml =
260
- '<iap from-personality="a" from-runtime="claude" from-intelligence="bogus">\n<message><![CDATA[x]]></message>\n</iap>'
261
- const d = decodeEnvelope(xml)
262
- expect(d.fromIntelligence).toBeUndefined()
263
- expect(d.message).toBe('x')
264
- })
265
-
266
- test('READ-COMPAT: legacy from-intelligence="human" decodes to natural', () => {
267
- const xml =
268
- '<iap from-personality="arthur" from-runtime="telegram" from-intelligence="human">\n<message><![CDATA[hi]]></message>\n</iap>'
269
- expect(decodeEnvelope(xml).fromIntelligence).toBe('natural')
270
- })
271
- })
@@ -1,163 +0,0 @@
1
- // connect telegram — the one-flow channel attachment (design §(в)). Sandboxed:
2
- // IAPEER_ROOT temp dirs, injected telegram-runtime runner + router restart. The
3
- // runner stub mimics the package: `bot add` writes bots/<alias>/.env (stable ABI).
4
-
5
- import { afterEach, describe, expect, test } from 'bun:test'
6
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
7
- import { dirname } from 'path'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import { botEnvPath, connectTelegram, type RestartOutcome, type TgRunner } from './index.ts'
11
- import { writeRuntimeManifest } from '../runtime/index.ts'
12
- import { upsertPeer } from '../registry/index.ts'
13
-
14
- const roots: string[] = []
15
- function mkTmp(): string {
16
- const d = mkdtempSync(join(tmpdir(), 'iapeer-connect-'))
17
- roots.push(d)
18
- return d
19
- }
20
- afterEach(() => {
21
- while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
22
- })
23
-
24
- function envFor(root: string): NodeJS.ProcessEnv {
25
- return {
26
- IAPEER_ROOT: join(root, 'iapeer'),
27
- IAPEER_LAUNCHAGENTS_DIR: join(root, 'LA'),
28
- IAPEER_TEST_SANDBOX: '1',
29
- HOME: root,
30
- } as NodeJS.ProcessEnv
31
- }
32
-
33
- async function fixture(): Promise<{ env: NodeJS.ProcessEnv; calls: string[][]; runTg: TgRunner; restarts: string[] }> {
34
- const env = envFor(mkTmp())
35
- writeRuntimeManifest({ runtime: 'telegram', selfConfig: { command: '/stub/telegram-runtime', args: ['self-config'] } }, { env })
36
- await upsertPeer({ personality: 'leo', runtime: 'claude', cwd: '/tmp/leo', intelligence: 'artificial' }, { env })
37
- await upsertPeer({ personality: 'arthur', runtime: 'telegram', cwd: '/tmp/arthur', intelligence: 'natural' }, { env })
38
- const calls: string[][] = []
39
- const runTg: TgRunner = (args, e) => {
40
- calls.push(args)
41
- if (args[0] === 'bot' && args[1] === 'add') {
42
- // the package's behavior: token → bots/<alias>/.env (incl. the username
43
- // field — the RELIABLE source, live-host fact); stdout also prints one
44
- const p = botEnvPath(args[2]!, e)
45
- mkdirSync(dirname(p), { recursive: true })
46
- writeFileSync(p, `TELEGRAM_BOT_TOKEN=${args[4]}\nTELEGRAM_BOT_USERNAME=leo_env_bot\n`)
47
- return { status: 0, stdout: 'bot added: @leo_stdout_bot\n', stderr: '' }
48
- }
49
- return { status: 0, stdout: '', stderr: '' }
50
- }
51
- const restarts: string[] = []
52
- return { env, calls, runTg, restarts }
53
- }
54
-
55
- const okRestart =
56
- (restarts: string[]) =>
57
- (human: string): RestartOutcome => {
58
- restarts.push(human)
59
- return { state: 'restarted' }
60
- }
61
-
62
- describe('connectTelegram (one flow: bot add → interface → restart → activation)', () => {
63
- test('happy path: adds the bot, interfaces the peer, restarts the HUMAN router, surfaces @username', async () => {
64
- const { env, calls, runTg, restarts } = await fixture()
65
- const r = await connectTelegram({ peer: 'leo', token: 'T1:abc', env, runTg, restart: okRestart(restarts) })
66
- expect(r.state).toBe('connected')
67
- expect(r.username).toBe('@leo_env_bot') // .env field WINS over the stdout match
68
- expect(r.restart?.state).toBe('restarted')
69
- expect(restarts).toEqual(['arthur']) // the router = the natural telegram peer, not leo
70
- expect(calls[0]).toEqual(['bot', 'add', 'leo', '--token', 'T1:abc'])
71
- expect(calls[1]).toEqual(['interface', 'bot', 'leo', '--peer', 'leo'])
72
- })
73
-
74
- test('IDEMPOTENT: the same token is a byte-stable no-op — router NOT restarted', async () => {
75
- const { env, runTg, restarts } = await fixture()
76
- expect((await connectTelegram({ peer: 'leo', token: 'T1:abc', env, runTg, restart: okRestart(restarts) })).state).toBe('connected')
77
- const second = await connectTelegram({ peer: 'leo', token: 'T1:abc', env, runTg, restart: okRestart(restarts) })
78
- expect(second.state).toBe('noop-same-token')
79
- expect(restarts).toEqual(['arthur']) // exactly ONE restart — the first connect
80
- })
81
-
82
- test('a NEW token replaces and RESTARTS again (credentials load at start)', async () => {
83
- const { env, runTg, restarts } = await fixture()
84
- await connectTelegram({ peer: 'leo', token: 'T1:abc', env, runTg, restart: okRestart(restarts) })
85
- const r = await connectTelegram({ peer: 'leo', token: 'T2:new', env, runTg, restart: okRestart(restarts) })
86
- expect(r.state).toBe('connected')
87
- expect(restarts).toEqual(['arthur', 'arthur'])
88
- })
89
-
90
- test('unregistered peer → refused BEFORE any bot add (interface precondition)', async () => {
91
- const { env, calls, runTg } = await fixture()
92
- const r = await connectTelegram({ peer: 'ghost', token: 'T', env, runTg })
93
- expect(r.state).toBe('unregistered-peer')
94
- expect(calls.length).toBe(0)
95
- })
96
-
97
- test('no telegram manifest → runtime-missing with the install recipe', async () => {
98
- const env = envFor(mkTmp())
99
- await upsertPeer({ personality: 'leo', runtime: 'claude', cwd: '/tmp/leo', intelligence: 'artificial' }, { env })
100
- const r = await connectTelegram({ peer: 'leo', token: 'T', env })
101
- expect(r.state).toBe('runtime-missing')
102
- expect(r.detail).toContain('install-runtime telegram')
103
- })
104
-
105
- test('non-tty without --token → explicit refusal with the BotFather recipe', async () => {
106
- const { env, runTg } = await fixture()
107
- const r = await connectTelegram({ peer: 'leo', env, runTg, isTty: false })
108
- expect(r.state).toBe('refused-no-token')
109
- expect(r.detail).toContain('@BotFather')
110
- })
111
-
112
- test('tty prompt path: the asked token flows in; empty answer → refusal', async () => {
113
- const { env, runTg, restarts } = await fixture()
114
- const r = await connectTelegram({ peer: 'leo', env, runTg, isTty: true, ask: async () => 'T9:asked', restart: okRestart(restarts) })
115
- expect(r.state).toBe('connected')
116
- const r2 = await connectTelegram({ peer: 'leo', env, runTg, isTty: true, ask: async () => '' })
117
- expect(r2.state).toBe('refused-no-token')
118
- })
119
-
120
- test('username falls back to the bot-add stdout when .env carries no username field', async () => {
121
- const env = envFor(mkTmp())
122
- writeRuntimeManifest({ runtime: 'telegram', selfConfig: '/stub/telegram-runtime self-config' }, { env })
123
- await upsertPeer({ personality: 'leo', runtime: 'claude', cwd: '/tmp/leo', intelligence: 'artificial' }, { env })
124
- await upsertPeer({ personality: 'arthur', runtime: 'telegram', cwd: '/tmp/arthur', intelligence: 'natural' }, { env })
125
- const runTg: TgRunner = (args, e) => {
126
- if (args[0] === 'bot') {
127
- const p = botEnvPath('leo', e)
128
- mkdirSync(dirname(p), { recursive: true })
129
- writeFileSync(p, 'TELEGRAM_BOT_TOKEN=T\n') // no username field (older package)
130
- return { status: 0, stdout: 'added @stdout_only_bot\n', stderr: '' }
131
- }
132
- return { status: 0, stdout: '', stderr: '' }
133
- }
134
- const r = await connectTelegram({ peer: 'leo', token: 'T', env, runTg, restart: okRestart([]) })
135
- expect(r.username).toBe('@stdout_only_bot')
136
- })
137
-
138
- test('bot add failure (getMe refusal on a bad token) → bot-add-failed with the package detail', async () => {
139
- const { env } = await fixture()
140
- const failTg: TgRunner = args =>
141
- args[0] === 'bot' ? { status: 1, stdout: '', stderr: 'getMe failed: 401 Unauthorized' } : { status: 0, stdout: '', stderr: '' }
142
- const r = await connectTelegram({ peer: 'leo', token: 'bad', env, runTg: failTg })
143
- expect(r.state).toBe('bot-add-failed')
144
- expect(r.detail).toContain('401')
145
- })
146
-
147
- test('no natural telegram peer in the registry → connected but restart=no-router (loud)', async () => {
148
- const env = envFor(mkTmp())
149
- writeRuntimeManifest({ runtime: 'telegram', selfConfig: '/stub/telegram-runtime self-config' }, { env })
150
- await upsertPeer({ personality: 'leo', runtime: 'claude', cwd: '/tmp/leo', intelligence: 'artificial' }, { env })
151
- const runTg: TgRunner = (args, e) => {
152
- if (args[0] === 'bot') {
153
- const p = botEnvPath('leo', e)
154
- mkdirSync(dirname(p), { recursive: true })
155
- writeFileSync(p, 'TELEGRAM_BOT_TOKEN=T\n')
156
- }
157
- return { status: 0, stdout: '', stderr: '' }
158
- }
159
- const r = await connectTelegram({ peer: 'leo', token: 'T', env, runTg })
160
- expect(r.state).toBe('connected')
161
- expect(r.restart?.state).toBe('no-router')
162
- })
163
- })
@@ -1,32 +0,0 @@
1
- // resolveSockDir — the ONE socket-dir resolver every socket-touching site shares
2
- // (transport scan/resolve, lifecycle, launchdRun). Regression guard against a site
3
- // re-hardcoding DEFAULT_SOCK_DIR, which made an IAPEER_SOCK_DIR sandbox lie (the
4
- // session created on the override dir, the resolver scanning /tmp → false offline).
5
-
6
- import { describe, expect, test } from 'bun:test'
7
- import { DEFAULT_SOCK_DIR, resolveSockDir } from './constants.ts'
8
-
9
- describe('resolveSockDir', () => {
10
- test('no override → DEFAULT_SOCK_DIR (/tmp, contract sock convention)', () => {
11
- expect(resolveSockDir({})).toBe(DEFAULT_SOCK_DIR)
12
- expect(resolveSockDir({})).toBe('/tmp')
13
- })
14
- test('IAPEER_SOCK_DIR override is respected (host-wide, like IAPEER_ROOT)', () => {
15
- expect(resolveSockDir({ IAPEER_SOCK_DIR: '/tmp/sbx/socks' })).toBe('/tmp/sbx/socks')
16
- })
17
- test('blank/whitespace override falls back to the default (not an empty dir)', () => {
18
- expect(resolveSockDir({ IAPEER_SOCK_DIR: ' ' })).toBe(DEFAULT_SOCK_DIR)
19
- expect(resolveSockDir({ IAPEER_SOCK_DIR: '' })).toBe(DEFAULT_SOCK_DIR)
20
- })
21
- test('IAPEER_ROOT implies socket isolation: <root>/socks (boris e2e find 10.06)', () => {
22
- // An alt-root used to inherit GLOBAL /tmp — a sandboxed list saw PROD sessions
23
- // live by name collision, and sandboxed stop/start would have hit prod.
24
- expect(resolveSockDir({ IAPEER_ROOT: '/tmp/sbx/iapeer' })).toBe('/tmp/sbx/iapeer/socks')
25
- })
26
- test('explicit IAPEER_SOCK_DIR wins over the root-derived dir', () => {
27
- expect(resolveSockDir({ IAPEER_ROOT: '/tmp/sbx/iapeer', IAPEER_SOCK_DIR: '/tmp/elsewhere' })).toBe('/tmp/elsewhere')
28
- })
29
- test('prod shape (no IAPEER_ROOT, no IAPEER_SOCK_DIR) stays on /tmp — untouched', () => {
30
- expect(resolveSockDir({ HOME: '/Users/x' })).toBe('/tmp')
31
- })
32
- })
@@ -1,98 +0,0 @@
1
- // normalizeNameCandidate — the shared name normalizer (normalize = slug∘translit).
2
- // Covers the zone goldens, ASCII non-regression, writing systems (Cyrillic / Greek
3
- // / CJK), idempotency, the ICU `№` symbol-alignment, and the fail-to-explicit edge
4
- // (a non-transliterable / leading-digit / empty result is returned AS-IS and does
5
- // NOT satisfy NAME_RE, so the caller can reject it rather than invent a name).
6
-
7
- import { describe, expect, test } from 'bun:test'
8
- import { isValidName, NAME_RE, normalizeNameCandidate as normalize } from './constants.ts'
9
-
10
- describe('normalizeNameCandidate — zone goldens', () => {
11
- test.each([
12
- ['Café №2', 'cafe-no-2'], // accents + ICU `№ → No.` symbol alignment
13
- ['项目', 'xiang-mu'], // Han → pinyin WITH syllable spacing
14
- ['My Project', 'my-project'],
15
- ])('%j → %j', (input, expected) => {
16
- expect(normalize(input)).toBe(expected)
17
- })
18
- })
19
-
20
- describe('normalizeNameCandidate — ASCII non-regression', () => {
21
- test.each([
22
- ['iapeer', 'iapeer'],
23
- ['boris', 'boris'],
24
- ['notifier-timer', 'notifier-timer'], // hyphen preserved, not collapsed
25
- ['darwin', 'darwin'],
26
- ])('%j → %j', (input, expected) => {
27
- expect(normalize(input)).toBe(expected)
28
- })
29
- })
30
-
31
- describe('normalizeNameCandidate — writing systems (ICU Any-Latin; Latin-ASCII)', () => {
32
- test.each([
33
- ['Артур', 'artur'], // Cyrillic
34
- ['Борис', 'boris'],
35
- ['Наталья', 'natalya'], // matches the live registered peer "natalya"
36
- ['東京', 'dong-jing'], // CJK with syllable spacing
37
- ['北京', 'bei-jing'],
38
- ['Ω', 'o'], // Greek
39
- ['naïve', 'naive'], // Latin diacritics
40
- ['Köln', 'koln'],
41
- ['Straße', 'strasse'],
42
- ])('%j → %j', (input, expected) => {
43
- expect(normalize(input)).toBe(expected)
44
- })
45
- })
46
-
47
- describe('normalizeNameCandidate — slug rules', () => {
48
- test('collapses non-alnum runs to a single hyphen and trims edges', () => {
49
- expect(normalize(' a b ')).toBe('a-b')
50
- expect(normalize('a___b...c')).toBe('a-b-c')
51
- expect(normalize('--lead-and-trail--')).toBe('lead-and-trail')
52
- })
53
-
54
- test('caps length at 32 with no dangling trailing hyphen', () => {
55
- const long = 'a'.repeat(40)
56
- expect(normalize(long)).toBe('a'.repeat(32))
57
- // a hyphen landing exactly at the cut must not survive as a trailing hyphen
58
- const cut = `${'a'.repeat(31)}-bbb`
59
- const out = normalize(cut)
60
- expect(out.length).toBeLessThanOrEqual(32)
61
- expect(out.endsWith('-')).toBe(false)
62
- })
63
- })
64
-
65
- describe('normalizeNameCandidate — idempotency (normalize∘normalize === normalize)', () => {
66
- test.each(['Café №2', '项目', 'My Project', 'Артур', 'boris', 'notifier-timer', '東京'])(
67
- '%j is a fixed point of a second pass',
68
- input => {
69
- const once = normalize(input)
70
- expect(normalize(once)).toBe(once)
71
- },
72
- )
73
- })
74
-
75
- describe('normalizeNameCandidate — fail-to-explicit edges (returned AS-IS, not mangled)', () => {
76
- test.each([
77
- ['', ''], // empty
78
- ['①②③', ''], // non-transliterable circled digits → drop
79
- ['😀', ''], // emoji → drop
80
- ['---', ''], // only separators
81
- ['2nd', '2nd'], // leading digit survives but is NOT a valid name
82
- ['42', '42'],
83
- ])('%j → %j', (input, expected) => {
84
- expect(normalize(input)).toBe(expected)
85
- })
86
-
87
- test('edge results do NOT satisfy NAME_RE → caller fails-to-explicit', () => {
88
- for (const bad of ['', '①②③', '😀', '---', '2nd', '42']) {
89
- expect(isValidName(normalize(bad))).toBe(false)
90
- }
91
- })
92
-
93
- test('valid goldens DO satisfy NAME_RE', () => {
94
- for (const good of ['cafe-no-2', 'xiang-mu', 'my-project', 'iapeer', 'artur']) {
95
- expect(NAME_RE.test(good)).toBe(true)
96
- }
97
- })
98
- })