@agfpd/iapeer 0.2.77 → 0.2.78
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/LICENSE +202 -0
- package/README.md +2 -2
- package/package.json +21 -2
- package/src/cli/index.ts +47 -48
- package/src/connect/index.ts +4 -4
- package/src/core/constants.ts +3 -3
- package/src/core/normalize.ts +2 -2
- package/src/create/index.ts +1 -1
- package/src/daemon/index.ts +5 -5
- package/src/daemon/main.ts +9 -9
- package/src/enable/index.ts +19 -18
- package/src/enable/provisionCommand.ts +10 -10
- package/src/identity/index.ts +2 -2
- package/src/identity/profileStandard.ts +8 -8
- package/src/init/index.ts +10 -10
- package/src/install/index.ts +2 -2
- package/src/install/signing.ts +3 -4
- package/src/launch/adapters/claude.ts +7 -7
- package/src/launch/adapters/codex.ts +5 -6
- package/src/launch/canary.ts +8 -8
- package/src/launch/cmdlog.ts +3 -3
- package/src/launch/composeSystemPrompt.ts +2 -1
- package/src/launch/index.ts +5 -6
- package/src/launch/invocation.ts +1 -1
- package/src/launch/launchd.ts +14 -15
- package/src/launch/launchdRun.ts +6 -6
- package/src/launch/nativeMemory.ts +9 -9
- package/src/launch/ptyHost.ts +1 -1
- package/src/launch/readyGateModel.ts +3 -3
- package/src/launch/types.ts +7 -7
- package/src/lifecycle/eventlog.ts +2 -2
- package/src/lifecycle/index.ts +21 -21
- package/src/lifecycle/queue.ts +1 -1
- package/src/onboard/index.ts +19 -13
- package/src/onboard/memory.ts +5 -5
- package/src/onboard/steps.ts +8 -9
- package/src/provision/index.ts +2 -3
- package/src/runtime/deploy.ts +2 -2
- package/src/runtime/index.ts +4 -4
- package/src/shadow/gate.ts +6 -6
- package/src/shadow/index.ts +2 -2
- package/src/status/index.ts +11 -11
- package/src/storage/index.ts +1 -1
- package/src/supervisor/daemon.ts +14 -8
- package/src/transport/index.ts +11 -11
- package/src/update/index.ts +4 -4
- package/src/cli/cli.test.ts +0 -603
- package/src/cli/listTui.test.ts +0 -71
- package/src/codec/codec.test.ts +0 -271
- package/src/connect/connect.test.ts +0 -163
- package/src/core/constants.test.ts +0 -32
- package/src/core/normalize.test.ts +0 -98
- package/src/create/create.test.ts +0 -143
- package/src/daemon/daemon-http.test.ts +0 -114
- package/src/daemon/daemon.test.ts +0 -267
- package/src/daemon/main.test.ts +0 -413
- package/src/enable/enable.test.ts +0 -95
- package/src/enable/provisionCommand.test.ts +0 -107
- package/src/identity/identity.test.ts +0 -279
- package/src/identity/profileStandard.test.ts +0 -204
- package/src/init/init.test.ts +0 -215
- package/src/init/runtime-resolve.test.ts +0 -49
- package/src/install/install.test.ts +0 -66
- package/src/install/signing.test.ts +0 -84
- package/src/launch/bootdeliver.test.ts +0 -108
- package/src/launch/bootstrap.test.ts +0 -56
- package/src/launch/canary.test.ts +0 -346
- package/src/launch/cmdlog.test.ts +0 -94
- package/src/launch/codexHooksTrust.test.ts +0 -248
- package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
- package/src/launch/composeSystemPrompt.test.ts +0 -145
- package/src/launch/hostComposerOccupied.test.ts +0 -63
- package/src/launch/invocation.test.ts +0 -174
- package/src/launch/launch.test.ts +0 -359
- package/src/launch/launchd.test.ts +0 -499
- package/src/launch/nativeMemory.test.ts +0 -193
- package/src/launch/ptyHost.test.ts +0 -38
- package/src/launch/readyGateModel.test.ts +0 -72
- package/src/launch/sockdir.test.ts +0 -74
- package/src/lifecycle/eventlog.test.ts +0 -114
- package/src/lifecycle/lifecycle.test.ts +0 -1056
- package/src/lifecycle/queue.test.ts +0 -185
- package/src/onboard/memory.test.ts +0 -157
- package/src/onboard/onboard.test.ts +0 -39
- package/src/onboard/steps.test.ts +0 -201
- package/src/provision/provision.test.ts +0 -288
- package/src/registry/registry.test.ts +0 -432
- package/src/runtime/runtime.test.ts +0 -226
- package/src/runtime/update.test.ts +0 -192
- package/src/shadow/enumerate-skip.test.ts +0 -67
- package/src/shadow/gate.test.ts +0 -286
- package/src/shadow/install.test.ts +0 -153
- package/src/shadow/shadow.test.ts +0 -42
- package/src/status/status.test.ts +0 -208
- package/src/storage/peers-home.test.ts +0 -34
- package/src/storage/rotatelog.test.ts +0 -44
- package/src/storage/storage.test.ts +0 -65
- package/src/supervisor/attached-marker.test.ts +0 -84
- package/src/supervisor/boot.test.ts +0 -66
- package/src/supervisor/client.attach.test.ts +0 -107
- package/src/supervisor/daemon.test.ts +0 -213
- package/src/supervisor/deliver.test.ts +0 -58
- package/src/supervisor/panelog.test.ts +0 -95
- package/src/supervisor/protocol.test.ts +0 -222
- package/src/supervisor/selfargv.test.ts +0 -22
- package/src/supervisor/spinner-strip.test.ts +0 -52
- package/src/transport/compactDone.test.ts +0 -144
- package/src/transport/composerQueue.test.ts +0 -204
- package/src/transport/hostControl.test.ts +0 -87
- package/src/transport/hostDeliver.test.ts +0 -137
- package/src/transport/hostLive.test.ts +0 -50
- package/src/transport/routesend-wake.test.ts +0 -357
- package/src/update/update.test.ts +0 -208
package/src/cli/listTui.test.ts
DELETED
|
@@ -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
|
-
})
|
package/src/codec/codec.test.ts
DELETED
|
@@ -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(/"/g, '"')
|
|
14
|
-
.replace(/</g, '<')
|
|
15
|
-
.replace(/>/g, '>')
|
|
16
|
-
.replace(/&/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
|
-
})
|