@agfpd/iapeer 0.2.2 → 0.2.3
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 +1 -1
- package/src/cli/index.ts +36 -0
- package/src/core/index.ts +1 -0
- package/src/core/version.ts +10 -0
- package/src/index.ts +2 -0
- package/src/launch/launchd.ts +32 -0
- package/src/update/index.ts +114 -0
- package/src/update/update.test.ts +123 -0
- package/tsconfig.json +1 -0
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type Intelligence,
|
|
20
20
|
type Runtime,
|
|
21
21
|
} from '../core/constants.ts'
|
|
22
|
+
import { IAPEER_VERSION } from '../core/version.ts'
|
|
23
|
+
import { updateIapeer } from '../update/index.ts'
|
|
22
24
|
import { buildProcessAddress, buildSocketPath, parseSessionName } from '../core/socket.ts'
|
|
23
25
|
import { ensureGlobalIapScaffold } from '../storage/index.ts'
|
|
24
26
|
import { findPeer, readPeersIndex, removePeer, type PeerRecord } from '../registry/index.ts'
|
|
@@ -346,6 +348,8 @@ export function parseArgs(argv: string[]): { positionals: string[]; flags: Recor
|
|
|
346
348
|
|
|
347
349
|
const USAGE = `usage: iapeer <verb> [args]
|
|
348
350
|
install build binary + global scaffold + daemon plist (one bootstrap)
|
|
351
|
+
update [--force] pull the latest @agfpd/iapeer from npm (cloud) + restart the daemon
|
|
352
|
+
version | --version | -v print the installed binary's version
|
|
349
353
|
daemon [--install-plist] run the host-wide HTTP-MCP router (launchd-held)
|
|
350
354
|
onboard [--dry-run] [--infra <csv>] register the agfpd marketplace (+ npx-install & deploy infra runtimes)
|
|
351
355
|
install-runtime <runtime> [--package pkg] [--npx] npx-install a runtime package + deploy its declared peer-set
|
|
@@ -549,6 +553,38 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
549
553
|
out(`delivered to ${r.delivered_to.personality} (${r.delivered_to.runtime})\n`)
|
|
550
554
|
return 0
|
|
551
555
|
}
|
|
556
|
+
case 'version':
|
|
557
|
+
case '--version':
|
|
558
|
+
case '-v': {
|
|
559
|
+
out(`${IAPEER_VERSION}\n`)
|
|
560
|
+
return 0
|
|
561
|
+
}
|
|
562
|
+
case 'update': {
|
|
563
|
+
// The single deploy path: pull the latest published foundation from npm and
|
|
564
|
+
// restart the daemon onto it (cloud-only — never a working-tree build). Foundation
|
|
565
|
+
// ONLY; the plist and other host packages are untouched. --force reinstalls even
|
|
566
|
+
// when already at latest. (Run from the installed binary; the first-ever install is
|
|
567
|
+
// `npx @agfpd/iapeer`.)
|
|
568
|
+
const r = updateIapeer({ env, force: flags.force === true })
|
|
569
|
+
if (r.status === 'failed') {
|
|
570
|
+
errOut(`update failed: ${r.reason}\n`)
|
|
571
|
+
return 1
|
|
572
|
+
}
|
|
573
|
+
if (r.status === 'already-latest') {
|
|
574
|
+
out(`already at the latest version (${r.from})\n`)
|
|
575
|
+
return 0
|
|
576
|
+
}
|
|
577
|
+
const daemonNote =
|
|
578
|
+
r.daemon === 'restarted'
|
|
579
|
+
? 'daemon restarted onto the new binary'
|
|
580
|
+
: r.daemon === 'not-loaded'
|
|
581
|
+
? 'daemon not loaded — new binary will be used on next start'
|
|
582
|
+
: r.daemon === 'failed'
|
|
583
|
+
? `WARNING — ${r.reason}`
|
|
584
|
+
: String(r.daemon)
|
|
585
|
+
out(`updated ${r.from} → ${r.to}; ${daemonNote}\n`)
|
|
586
|
+
return r.daemon === 'failed' ? 1 : 0
|
|
587
|
+
}
|
|
552
588
|
case 'install': {
|
|
553
589
|
// UNIFIED foundation install (contract Установка §1 — "один npx ставит
|
|
554
590
|
// фундамент"): ONE command does all three install-phase steps that used to be
|
package/src/core/index.ts
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The installed binary's own version — BAKED at build time. `bun build --compile`
|
|
2
|
+
// inlines this JSON import, so the standalone ~/.local/bin/iapeer reports its
|
|
3
|
+
// version with NO package.json present at runtime. `iapeer update` compares this
|
|
4
|
+
// against `npm view @agfpd/iapeer version` to decide whether a pull is needed.
|
|
5
|
+
//
|
|
6
|
+
// Single source of truth: package.json (bumped by `npm version` in the release
|
|
7
|
+
// flow), so the binary's version and the published npm version never drift.
|
|
8
|
+
import pkg from '../../package.json'
|
|
9
|
+
|
|
10
|
+
export const IAPEER_VERSION: string = (pkg as { version: string }).version
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ export * from './provision/index.ts'
|
|
|
21
21
|
export * from './init/index.ts'
|
|
22
22
|
// Install — the foundation install-phase (stable ~/.local/bin/iapeer, decoupled from src).
|
|
23
23
|
export * from './install/index.ts'
|
|
24
|
+
// Update — the cloud-only deploy path (`iapeer update`: npm latest → rebuild → restart).
|
|
25
|
+
export * from './update/index.ts'
|
|
24
26
|
// Onboard — the host-phase (idempotent marketplace registration in claude + codex).
|
|
25
27
|
export * from './onboard/index.ts'
|
|
26
28
|
export { composeSystemPrompt, gatherPromptInput } from './launch/composeSystemPrompt.ts'
|
package/src/launch/launchd.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { join } from 'path'
|
|
|
19
19
|
import { spawnSync } from 'child_process'
|
|
20
20
|
import { iapeerBinPath } from '../install/index.ts'
|
|
21
21
|
import {
|
|
22
|
+
DAEMON_PLIST_LABEL,
|
|
22
23
|
IAPEER_DIR,
|
|
23
24
|
INFRA_RUNTIME_BIN_ENV,
|
|
24
25
|
INFRA_RUNTIME_DEFAULT_BIN,
|
|
@@ -214,6 +215,37 @@ function isLaunchdLoaded(label: string, uid: string): boolean {
|
|
|
214
215
|
return spawnSync('launchctl', ['print', `gui/${uid}/${label}`], { stdio: 'ignore' }).status === 0
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
export type DaemonRestartState =
|
|
219
|
+
| 'restarted' // kickstart -k succeeded → the daemon is now on the freshly-installed binary
|
|
220
|
+
| 'not-loaded' // com.agfpd.iapeer is not in the gui domain → nothing to restart (new binary
|
|
221
|
+
// will be used whenever it IS next started; nothing to do here)
|
|
222
|
+
| 'skipped-sandbox' // IAPEER_TEST_SANDBOX=1 → never touch the real launchd
|
|
223
|
+
| 'failed' // launchctl kickstart exited non-zero
|
|
224
|
+
|
|
225
|
+
export interface DaemonRestartResult {
|
|
226
|
+
state: DaemonRestartState
|
|
227
|
+
detail?: string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Restart the foundation daemon (`com.agfpd.iapeer`) onto the binary currently on
|
|
232
|
+
* disk — `launchctl kickstart -k gui/<uid>/com.agfpd.iapeer`. This is the "activate
|
|
233
|
+
* the freshly-installed binary" step of `iapeer update`: a compiled daemon holds its
|
|
234
|
+
* old code in memory until restarted, and `-k` SIGKILLs + relaunches it (KeepAlive
|
|
235
|
+
* brings it right back). Only restarts when the service is actually loaded — a
|
|
236
|
+
* not-loaded daemon needs no restart (the new binary is taken on its next start).
|
|
237
|
+
* Never touches the plist (the launch path is version-stable). Sandbox-safe.
|
|
238
|
+
*/
|
|
239
|
+
export function kickstartDaemon(env: NodeJS.ProcessEnv = process.env): DaemonRestartResult {
|
|
240
|
+
if (env.IAPEER_TEST_SANDBOX === '1') return { state: 'skipped-sandbox' }
|
|
241
|
+
const uid = currentUid()
|
|
242
|
+
if (!isLaunchdLoaded(DAEMON_PLIST_LABEL, uid)) return { state: 'not-loaded' }
|
|
243
|
+
const r = spawnSync('launchctl', ['kickstart', '-k', `gui/${uid}/${DAEMON_PLIST_LABEL}`], { encoding: 'utf8' })
|
|
244
|
+
return r.status === 0
|
|
245
|
+
? { state: 'restarted' }
|
|
246
|
+
: { state: 'failed', detail: (r.stderr ?? '').trim() || `launchctl kickstart exit ${r.status}` }
|
|
247
|
+
}
|
|
248
|
+
|
|
217
249
|
/**
|
|
218
250
|
* AUTO-bootstrap a freshly-provisioned foundation plist into the gui domain
|
|
219
251
|
* (`launchctl bootstrap gui/<uid> <plist>`) — the "load it now, don't write-and-wait
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// `iapeer update` — the SINGLE deploy path: pull the latest published foundation
|
|
2
|
+
// from npm (the cloud) and restart the daemon onto it. There is deliberately NO
|
|
3
|
+
// "deploy from a working tree" — every host, including the dev host, activates a
|
|
4
|
+
// release the same way: `npm run release` (publish) → `iapeer update` (pull + restart).
|
|
5
|
+
//
|
|
6
|
+
// Flow:
|
|
7
|
+
// 1. latest = `npm view @agfpd/iapeer version` (the cloud's truth)
|
|
8
|
+
// 2. installed == latest && !--force → "already latest" (no needless rebuild/restart)
|
|
9
|
+
// 3. `npx @agfpd/iapeer@<latest> install` (fetch + rebuild ~/.local/bin/iapeer
|
|
10
|
+
// atomically; the COMPILED binary can't rebuild itself from source, so we shell to
|
|
11
|
+
// npx, which runs the freshly-fetched package's own install — same path consumers use)
|
|
12
|
+
// 4. kickstart com.agfpd.iapeer IF loaded (activate the new binary)
|
|
13
|
+
//
|
|
14
|
+
// Scope: the foundation ONLY (the @agfpd/iapeer binary + its daemon). It never
|
|
15
|
+
// touches the plist (version-stable launch path) and never touches other packages
|
|
16
|
+
// on the host (telegram-runtime / notifier-runtime / MergeMind / the peer fleet) —
|
|
17
|
+
// each of those has its own version and its own update.
|
|
18
|
+
//
|
|
19
|
+
// updateIapeer takes its three side-effects as INJECTED deps so the version-gate is
|
|
20
|
+
// unit-testable with no network and no launchctl; the defaults are the real impls.
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from 'child_process'
|
|
23
|
+
import { IapError } from '../core/errors.ts'
|
|
24
|
+
import { IAPEER_VERSION } from '../core/version.ts'
|
|
25
|
+
import { kickstartDaemon, type DaemonRestartResult } from '../launch/launchd.ts'
|
|
26
|
+
|
|
27
|
+
/** The npm package the foundation publishes / updates from. */
|
|
28
|
+
export const IAPEER_PACKAGE = '@agfpd/iapeer'
|
|
29
|
+
|
|
30
|
+
export interface UpdateDeps {
|
|
31
|
+
env?: NodeJS.ProcessEnv
|
|
32
|
+
/** Reinstall + restart even when already at the latest version. */
|
|
33
|
+
force?: boolean
|
|
34
|
+
/** The currently-installed version (default: this binary's baked IAPEER_VERSION). */
|
|
35
|
+
currentVersion?: string
|
|
36
|
+
/** Resolve the latest published version (default: `npm view`). Returns null on
|
|
37
|
+
* any failure (offline / registry error / non-semver) — update then fails loud. */
|
|
38
|
+
fetchLatest?: (env: NodeJS.ProcessEnv) => string | null
|
|
39
|
+
/** Pull + rebuild the binary for `version` (default: `npx @agfpd/iapeer@<v> install`).
|
|
40
|
+
* Returns true on success. */
|
|
41
|
+
runInstall?: (version: string, env: NodeJS.ProcessEnv) => boolean
|
|
42
|
+
/** Restart the daemon onto the new binary (default: kickstartDaemon). */
|
|
43
|
+
restartDaemon?: (env: NodeJS.ProcessEnv) => DaemonRestartResult
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UpdateResult {
|
|
47
|
+
status: 'updated' | 'already-latest' | 'failed'
|
|
48
|
+
/** Version before the update (the running binary). */
|
|
49
|
+
from: string
|
|
50
|
+
/** Latest published version resolved from npm (undefined if it couldn't be resolved). */
|
|
51
|
+
latest?: string
|
|
52
|
+
/** Version installed by this run (set only on status 'updated'). */
|
|
53
|
+
to?: string
|
|
54
|
+
/** What happened to the live daemon (only on 'updated'). */
|
|
55
|
+
daemon?: DaemonRestartResult['state']
|
|
56
|
+
/** Human reason on 'failed' / extra detail on a daemon restart hiccup. */
|
|
57
|
+
reason?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+].+)?$/
|
|
61
|
+
|
|
62
|
+
/** Default latest-resolver: `npm view @agfpd/iapeer version`. */
|
|
63
|
+
function defaultFetchLatest(env: NodeJS.ProcessEnv): string | null {
|
|
64
|
+
const r = spawnSync('npm', ['view', IAPEER_PACKAGE, 'version'], { encoding: 'utf8', env })
|
|
65
|
+
if (r.status !== 0) return null
|
|
66
|
+
const v = (r.stdout ?? '').trim()
|
|
67
|
+
return SEMVER_RE.test(v) ? v : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Default installer: `npx -y @agfpd/iapeer@<version> install` (pull from cloud + rebuild). */
|
|
71
|
+
function defaultRunInstall(version: string, env: NodeJS.ProcessEnv): boolean {
|
|
72
|
+
if (env.IAPEER_TEST_SANDBOX === '1') {
|
|
73
|
+
// A real npx install rebuilds the prod ~/.local/bin/iapeer — never under a test.
|
|
74
|
+
throw new IapError('refusing a real `npx install` under IAPEER_TEST_SANDBOX=1 — inject runInstall in tests')
|
|
75
|
+
}
|
|
76
|
+
const r = spawnSync('npx', ['-y', `${IAPEER_PACKAGE}@${version}`, 'install'], { stdio: 'inherit', env })
|
|
77
|
+
return r.status === 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Update the foundation to the latest published version (cloud-only) and restart
|
|
82
|
+
* the daemon onto it. Idempotent + version-gated: a no-op "already-latest" when the
|
|
83
|
+
* installed version equals the published one (unless `force`). Pure-ish — the three
|
|
84
|
+
* effects are injected, so this is fully unit-testable.
|
|
85
|
+
*/
|
|
86
|
+
export function updateIapeer(deps: UpdateDeps = {}): UpdateResult {
|
|
87
|
+
const env = deps.env ?? process.env
|
|
88
|
+
const from = deps.currentVersion ?? IAPEER_VERSION
|
|
89
|
+
const fetchLatest = deps.fetchLatest ?? defaultFetchLatest
|
|
90
|
+
|
|
91
|
+
const latest = fetchLatest(env)
|
|
92
|
+
if (!latest) {
|
|
93
|
+
return { status: 'failed', from, reason: `could not resolve the latest ${IAPEER_PACKAGE} version from npm (offline / registry error)` }
|
|
94
|
+
}
|
|
95
|
+
if (latest === from && !deps.force) {
|
|
96
|
+
return { status: 'already-latest', from, latest }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const runInstall = deps.runInstall ?? defaultRunInstall
|
|
100
|
+
if (!runInstall(latest, env)) {
|
|
101
|
+
return { status: 'failed', from, latest, reason: `\`npx ${IAPEER_PACKAGE}@${latest} install\` failed` }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const restart = deps.restartDaemon ?? kickstartDaemon
|
|
105
|
+
const d = restart(env)
|
|
106
|
+
return {
|
|
107
|
+
status: 'updated',
|
|
108
|
+
from,
|
|
109
|
+
to: latest,
|
|
110
|
+
latest,
|
|
111
|
+
daemon: d.state,
|
|
112
|
+
reason: d.state === 'failed' ? `binary updated but daemon restart failed: ${d.detail ?? ''}`.trim() : undefined,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// updateIapeer — the version-gated, cloud-only deploy. The three side-effects
|
|
2
|
+
// (resolve latest / install / restart) are injected, so the gate logic is tested
|
|
3
|
+
// with NO network and NO launchctl. The sandbox guard on the REAL installer is
|
|
4
|
+
// exercised too (a test must never npx-install over the prod binary).
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test'
|
|
7
|
+
import { IAPEER_VERSION, updateIapeer } from '../index.ts'
|
|
8
|
+
import type { DaemonRestartResult } from '../launch/launchd.ts'
|
|
9
|
+
|
|
10
|
+
const restarted = (): DaemonRestartResult => ({ state: 'restarted' })
|
|
11
|
+
|
|
12
|
+
/** A deps bundle with call-tracking spies. */
|
|
13
|
+
function harness(opts: {
|
|
14
|
+
current: string
|
|
15
|
+
latest: string | null
|
|
16
|
+
installOk?: boolean
|
|
17
|
+
restart?: DaemonRestartResult
|
|
18
|
+
force?: boolean
|
|
19
|
+
}) {
|
|
20
|
+
const calls = { install: [] as string[], restart: 0 }
|
|
21
|
+
const result = updateIapeer({
|
|
22
|
+
env: { IAPEER_TEST_SANDBOX: '1' },
|
|
23
|
+
force: opts.force,
|
|
24
|
+
currentVersion: opts.current,
|
|
25
|
+
fetchLatest: () => opts.latest,
|
|
26
|
+
runInstall: v => {
|
|
27
|
+
calls.install.push(v)
|
|
28
|
+
return opts.installOk ?? true
|
|
29
|
+
},
|
|
30
|
+
restartDaemon: () => {
|
|
31
|
+
calls.restart++
|
|
32
|
+
return opts.restart ?? restarted()
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
return { result, calls }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('updateIapeer — version gate', () => {
|
|
39
|
+
test('already at latest → no install, no restart', () => {
|
|
40
|
+
const { result, calls } = harness({ current: '0.2.2', latest: '0.2.2' })
|
|
41
|
+
expect(result.status).toBe('already-latest')
|
|
42
|
+
expect(result.from).toBe('0.2.2')
|
|
43
|
+
expect(result.latest).toBe('0.2.2')
|
|
44
|
+
expect(calls.install).toEqual([])
|
|
45
|
+
expect(calls.restart).toBe(0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('--force reinstalls + restarts even when already latest', () => {
|
|
49
|
+
const { result, calls } = harness({ current: '0.2.2', latest: '0.2.2', force: true })
|
|
50
|
+
expect(result.status).toBe('updated')
|
|
51
|
+
expect(result.to).toBe('0.2.2')
|
|
52
|
+
expect(calls.install).toEqual(['0.2.2'])
|
|
53
|
+
expect(calls.restart).toBe(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('newer published version → installs THAT version + restarts', () => {
|
|
57
|
+
const { result, calls } = harness({ current: '0.2.2', latest: '0.3.0' })
|
|
58
|
+
expect(result.status).toBe('updated')
|
|
59
|
+
expect(result.from).toBe('0.2.2')
|
|
60
|
+
expect(result.to).toBe('0.3.0')
|
|
61
|
+
expect(result.daemon).toBe('restarted')
|
|
62
|
+
expect(calls.install).toEqual(['0.3.0']) // installs latest, not current
|
|
63
|
+
expect(calls.restart).toBe(1)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('updateIapeer — failure paths', () => {
|
|
68
|
+
test('latest unresolved (offline / registry error) → failed, no install', () => {
|
|
69
|
+
const { result, calls } = harness({ current: '0.2.2', latest: null })
|
|
70
|
+
expect(result.status).toBe('failed')
|
|
71
|
+
expect(result.reason).toMatch(/latest.*version.*npm|offline|registry/i)
|
|
72
|
+
expect(calls.install).toEqual([])
|
|
73
|
+
expect(calls.restart).toBe(0)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('install fails → failed, daemon NOT restarted', () => {
|
|
77
|
+
const { result, calls } = harness({ current: '0.2.2', latest: '0.3.0', installOk: false })
|
|
78
|
+
expect(result.status).toBe('failed')
|
|
79
|
+
expect(result.latest).toBe('0.3.0')
|
|
80
|
+
expect(result.reason).toMatch(/install.*failed/i)
|
|
81
|
+
expect(calls.restart).toBe(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('daemon not loaded → updated, daemon=not-loaded (no error)', () => {
|
|
85
|
+
const { result } = harness({ current: '0.2.2', latest: '0.3.0', restart: { state: 'not-loaded' } })
|
|
86
|
+
expect(result.status).toBe('updated')
|
|
87
|
+
expect(result.daemon).toBe('not-loaded')
|
|
88
|
+
expect(result.reason).toBeUndefined()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('binary updated but restart failed → updated with a warning reason', () => {
|
|
92
|
+
const { result } = harness({
|
|
93
|
+
current: '0.2.2',
|
|
94
|
+
latest: '0.3.0',
|
|
95
|
+
restart: { state: 'failed', detail: 'kickstart exit 1' },
|
|
96
|
+
})
|
|
97
|
+
expect(result.status).toBe('updated')
|
|
98
|
+
expect(result.daemon).toBe('failed')
|
|
99
|
+
expect(result.reason).toMatch(/restart failed.*kickstart exit 1/i)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('updateIapeer — real-installer sandbox guard', () => {
|
|
104
|
+
test('default runInstall refuses a real npx install under IAPEER_TEST_SANDBOX', () => {
|
|
105
|
+
// fetchLatest injected (newer) so the gate proceeds to the DEFAULT installer,
|
|
106
|
+
// which must refuse rather than npx-install over the prod ~/.local/bin/iapeer.
|
|
107
|
+
expect(() =>
|
|
108
|
+
updateIapeer({
|
|
109
|
+
env: { IAPEER_TEST_SANDBOX: '1' },
|
|
110
|
+
currentVersion: '0.2.2',
|
|
111
|
+
fetchLatest: () => '0.3.0',
|
|
112
|
+
// runInstall NOT injected → exercises the real defaultRunInstall guard.
|
|
113
|
+
restartDaemon: restarted,
|
|
114
|
+
}),
|
|
115
|
+
).toThrow(/IAPEER_TEST_SANDBOX/)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('IAPEER_VERSION', () => {
|
|
120
|
+
test('is a baked semver string', () => {
|
|
121
|
+
expect(IAPEER_VERSION).toMatch(/^\d+\.\d+\.\d+/)
|
|
122
|
+
})
|
|
123
|
+
})
|