@raidiant/notifai 0.1.1 → 0.1.2

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.
@@ -3,6 +3,7 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import type {
5
5
  CapabilityDocument,
6
+ EvidenceSnapshot,
6
7
  ListRepliesResponse,
7
8
  ReplyView,
8
9
  SubmissionReceipt,
@@ -21,10 +22,12 @@ import {
21
22
  hooksInstallCommand,
22
23
  hooksUninstallCommand,
23
24
  initCommand,
25
+ SKILLS_SOURCE,
24
26
  loginCommand,
25
27
  projectSlugFrom,
26
28
  repliesCommand,
27
29
  sendCommand,
30
+ statusCommand,
28
31
  type CommandDeps,
29
32
  type CommandIo,
30
33
  type CommandSpinner,
@@ -494,6 +497,80 @@ describe('command contracts', () => {
494
497
  })
495
498
  })
496
499
 
500
+ describe('delivery evidence status', () => {
501
+ function snapshot(
502
+ companionReceipt: EvidenceSnapshot['deliveries'][number]['companion_receipt'],
503
+ ): EvidenceSnapshot {
504
+ return {
505
+ request_id: 'req_status_test',
506
+ event: 'tests_passed',
507
+ accepted_at: '2026-08-05T13:05:48.000Z',
508
+ overall: 'provider_accepted_all',
509
+ deliveries: [
510
+ {
511
+ delivery_id: 'del_status_test',
512
+ device_id: 'dev_status_test',
513
+ device_name: 'iPhone',
514
+ state: 'provider_accepted',
515
+ attempts: 1,
516
+ provider_status: 200,
517
+ provider_reason: null,
518
+ provider_id: 'provider_status_test',
519
+ updated_at: '2026-08-05T13:05:50.000Z',
520
+ companion_receipt: companionReceipt,
521
+ events: [
522
+ {
523
+ stage: 'attempt_started',
524
+ source: 'worker',
525
+ reason: null,
526
+ attempt: 1,
527
+ occurred_at: '2026-08-05T13:05:49.000Z',
528
+ },
529
+ {
530
+ stage: 'provider_accepted',
531
+ source: 'worker',
532
+ reason: null,
533
+ attempt: 1,
534
+ occurred_at: '2026-08-05T13:05:50.000Z',
535
+ },
536
+ ],
537
+ },
538
+ ],
539
+ }
540
+ }
541
+
542
+ it('calls an unobserved first-minute receipt unknown rather than failed', async () => {
543
+ const io = new CapturedIo()
544
+ const client = {
545
+ evidence: async () => snapshot({ state: 'unknown', observed_at: null, latency_ms: null }),
546
+ } as unknown as ApiClient
547
+
548
+ expect(await statusCommand(makeDeps(io, client), 'req_status_test', {})).toBe(EXIT.ok)
549
+ const said = io.outLines.join('\n')
550
+ expect(said).toContain('Provider Acceptance: accepted')
551
+ expect(said).toContain('Companion Receipt: unknown')
552
+ expect(said).toContain('not a failure')
553
+ expect(said).toContain('attempt_started')
554
+ })
555
+
556
+ it('reports the observed device receipt and measured provider-to-companion latency', async () => {
557
+ const io = new CapturedIo()
558
+ const client = {
559
+ evidence: async () =>
560
+ snapshot({
561
+ state: 'observed',
562
+ observed_at: '2026-08-05T13:17:17.000Z',
563
+ latency_ms: 687_000,
564
+ }),
565
+ } as unknown as ApiClient
566
+
567
+ expect(await statusCommand(makeDeps(io, client), 'req_status_test', {})).toBe(EXIT.ok)
568
+ const said = io.outLines.join('\n')
569
+ expect(said).toContain('Companion Receipt: observed')
570
+ expect(said).toContain('11m 27s after Provider Acceptance')
571
+ })
572
+ })
573
+
497
574
  describe('Cursor hook commands', () => {
498
575
  const execPath = '/usr/local/bin/node'
499
576
  const scriptPath = '/opt/notifai/dist/main.js'
@@ -855,21 +932,83 @@ describe('interactive command UX', () => {
855
932
  })
856
933
 
857
934
  describe('init', () => {
935
+ const readyIphone = {
936
+ device_id: 'dev_iphone',
937
+ display_name: 'iPhone',
938
+ platform: 'ios' as const,
939
+ permission_status: 'authorized',
940
+ registration_healthy: true,
941
+ last_seen_at: '2026-08-05T18:00:00.000Z',
942
+ }
943
+
944
+ function setupEvidence(
945
+ requestId: string,
946
+ companionReceipt: EvidenceSnapshot['deliveries'][number]['companion_receipt'],
947
+ device = readyIphone,
948
+ ): EvidenceSnapshot {
949
+ return {
950
+ request_id: requestId,
951
+ event: 'setup_verified',
952
+ accepted_at: '2026-08-05T18:00:00.000Z',
953
+ overall: 'provider_accepted_all',
954
+ deliveries: [
955
+ {
956
+ delivery_id: 'del_setup',
957
+ device_id: device.device_id,
958
+ device_name: device.display_name,
959
+ state: 'provider_accepted',
960
+ attempts: 1,
961
+ provider_status: 200,
962
+ provider_reason: null,
963
+ provider_id: 'provider_setup',
964
+ updated_at: '2026-08-05T18:00:01.000Z',
965
+ companion_receipt: companionReceipt,
966
+ events:
967
+ companionReceipt.state === 'observed'
968
+ ? [
969
+ {
970
+ stage: 'companion_received',
971
+ source: 'companion',
972
+ reason: null,
973
+ attempt: null,
974
+ occurred_at: companionReceipt.observed_at!,
975
+ },
976
+ ]
977
+ : [],
978
+ },
979
+ ],
980
+ }
981
+ }
982
+
983
+ function setupReceipt(requestId = 'req_setup'): SubmissionReceipt {
984
+ return {
985
+ ...receipt,
986
+ request_id: requestId,
987
+ deliveries: [
988
+ {
989
+ ...receipt.deliveries[0]!,
990
+ device_id: readyIphone.device_id,
991
+ device_name: readyIphone.display_name,
992
+ },
993
+ ],
994
+ }
995
+ }
996
+
858
997
  it('writes the project identifier into .notifai/config.toml and is idempotent', async () => {
859
998
  const cwd = mkdtempSync(path.join(os.tmpdir(), 'My Project-'))
860
999
  const io = new CapturedIo()
861
1000
  const deps = { ...makeDeps(io, {} as ApiClient), cwd }
862
1001
 
863
- expect(await initCommand(deps, {})).toBe(EXIT.ok)
1002
+ expect(await initCommand(deps, {})).toBe(EXIT.failed)
864
1003
  const configPath = path.join(cwd, '.notifai', 'config.toml')
865
1004
  expect(readFileSync(configPath, 'utf8')).toContain('project = "my-project-')
866
1005
  // Safe by default: without an explicit --skills opt-in, init only writes
867
1006
  // configuration and never spawns the skill installer.
868
- expect(io.outLines.join('\n')).toContain('no published skill source configured')
1007
+ expect(io.outLines.join('\n')).not.toContain('Installing the notifai agent skill')
869
1008
  expect(io.outLines.join('\n')).not.toContain('All set.')
870
1009
 
871
1010
  io.outLines = []
872
- expect(await initCommand(deps, { skills: false })).toBe(EXIT.ok)
1011
+ expect(await initCommand(deps, { skills: false })).toBe(EXIT.failed)
873
1012
  // Idempotent: the second run re-derives the same slug and says so as a
874
1013
  // settled state rather than repeating the write.
875
1014
  expect(io.outLines.join('\n')).toContain('Project identity: "my-project-')
@@ -889,7 +1028,7 @@ describe('init', () => {
889
1028
  store: { load: () => null, save: () => {}, clear: () => {}, describe: () => 'empty store' },
890
1029
  }
891
1030
 
892
- expect(await initCommand(deps, {})).toBe(EXIT.ok)
1031
+ expect(await initCommand(deps, {})).toBe(EXIT.failed)
893
1032
  const out = io.outLines.join('\n')
894
1033
  expect(out).toContain('Next: This machine')
895
1034
  expect(out).toContain('notifai login')
@@ -904,7 +1043,7 @@ describe('init', () => {
904
1043
  const io = new CapturedIo()
905
1044
  const deps = { ...makeDeps(io, {} as ApiClient), cwd }
906
1045
 
907
- expect(await initCommand(deps, { projectId: 'Custom Name', skills: false })).toBe(EXIT.ok)
1046
+ expect(await initCommand(deps, { projectId: 'Custom Name', skills: false })).toBe(EXIT.failed)
908
1047
  expect(readFileSync(path.join(cwd, '.notifai', 'config.toml'), 'utf8')).toContain(
909
1048
  'project = "custom-name"',
910
1049
  )
@@ -916,24 +1055,16 @@ describe('init', () => {
916
1055
  const io = new CapturedIo()
917
1056
  const deps = { ...makeDeps(io, {} as ApiClient), cwd }
918
1057
 
919
- expect(await initCommand(deps, {})).toBe(EXIT.ok)
1058
+ expect(await initCommand(deps, {})).toBe(EXIT.failed)
920
1059
  const out = io.outLines.join('\n')
921
- expect(out).toContain('no published skill source configured')
922
- expect(out).not.toContain('notifai init --skills')
923
1060
  expect(out).not.toContain('Installing the notifai agent skill')
924
1061
  // Never prompted, and never assumed into a change it did not request.
925
1062
  expect(io.errLines).toEqual([])
926
1063
  })
927
1064
 
928
- it('fails explicitly when --skills requests the unpublished source', async () => {
929
- const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-skills-unpublished-'))
930
- const io = new CapturedIo()
931
- const deps = { ...makeDeps(io, {} as ApiClient), cwd }
932
-
933
- expect(await initCommand(deps, { skills: true })).toBe(EXIT.failed)
934
- expect(io.errLines).toContain(
935
- 'The optional agent skill is not published yet; this build has no skill source configured.',
936
- )
1065
+ it('pins the skill installer to the tagged public release syntax', () => {
1066
+ expect(SKILLS_SOURCE).toBe('RafaelVidaurre/notifai#v0.1.2')
1067
+ expect(SKILLS_SOURCE).not.toContain('@v')
937
1068
  })
938
1069
 
939
1070
  it('tells the user what only they can do when nothing is signed in', async () => {
@@ -945,7 +1076,7 @@ describe('init', () => {
945
1076
  store: { load: () => null, save: () => {}, clear: () => {}, describe: () => 'empty store' },
946
1077
  }
947
1078
 
948
- expect(await initCommand(deps, {})).toBe(EXIT.ok)
1079
+ expect(await initCommand(deps, {})).toBe(EXIT.failed)
949
1080
  expect(io.outLines.join('\n')).toContain('notifai login')
950
1081
  })
951
1082
 
@@ -970,6 +1101,299 @@ describe('init', () => {
970
1101
  // Refused, so it stays the next step rather than being treated as done.
971
1102
  expect(io.outLines.join('\n')).toContain('notifai login')
972
1103
  })
1104
+
1105
+ it('never prompts or opens a browser when an agent runs it unattended', async () => {
1106
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-agent-no-input-'))
1107
+ const io = new (class extends CapturedIo {
1108
+ override async confirm(): Promise<boolean> {
1109
+ throw new Error('an unattended init reached a prompt')
1110
+ }
1111
+
1112
+ override openUrl(): void {
1113
+ throw new Error('an unattended init opened a browser')
1114
+ }
1115
+ })()
1116
+ const deps: CommandDeps = {
1117
+ ...makeDeps(io, { health: async () => true } as unknown as ApiClient),
1118
+ cwd,
1119
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: path.join(cwd, 'state') },
1120
+ store: { load: () => null, save: () => {}, clear: () => {}, describe: () => 'empty store' },
1121
+ }
1122
+
1123
+ expect(await initCommand(deps, {})).toBe(EXIT.failed)
1124
+ expect(io.outLines.join('\n')).toContain('Next: This machine')
1125
+ })
1126
+
1127
+ it('makes the unavailable distribution bridge explicit when no app has registered', async () => {
1128
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-no-device-'))
1129
+ const io = new CapturedIo()
1130
+ const client = {
1131
+ health: async () => true,
1132
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1133
+ listDevices: async () => ({ devices: [] }),
1134
+ } as unknown as ApiClient
1135
+ const deps = {
1136
+ ...makeDeps(io, client),
1137
+ cwd,
1138
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: path.join(cwd, 'state') },
1139
+ }
1140
+
1141
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
1142
+ const out = io.outLines.join('\n')
1143
+ expect(out).toContain('Next: Your devices')
1144
+ expect(out).toContain('no supported App Store/TestFlight URL')
1145
+ expect(out).toContain('open it, sign in with the same account, and allow notifications')
1146
+ expect(out.match(/^Next:/gm)).toHaveLength(1)
1147
+ })
1148
+
1149
+ it.each([
1150
+ ['denied', 'system settings'],
1151
+ ['not_determined', 'allow its notification prompt'],
1152
+ ])('gives one permission-specific next action for %s', async (permission, expected) => {
1153
+ const cwd = mkdtempSync(path.join(os.tmpdir(), `init-permission-${permission}-`))
1154
+ const io = new CapturedIo()
1155
+ const client = {
1156
+ health: async () => true,
1157
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1158
+ listDevices: async () => ({
1159
+ devices: [{ ...readyIphone, permission_status: permission, registration_healthy: false }],
1160
+ }),
1161
+ } as unknown as ApiClient
1162
+ const deps = {
1163
+ ...makeDeps(io, client),
1164
+ cwd,
1165
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: path.join(cwd, 'state') },
1166
+ }
1167
+
1168
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
1169
+ const out = io.outLines.join('\n')
1170
+ expect(out).toContain(`iPhone (${permission})`)
1171
+ expect(out).toContain(expected)
1172
+ expect(out.match(/^Next:/gm)).toHaveLength(1)
1173
+ })
1174
+
1175
+ it('waits on the supported device registry, then ends with an observed real receipt', async () => {
1176
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-device-bridge-'))
1177
+ const io = new InteractiveIo()
1178
+ let now = 0
1179
+ let deviceReady = false
1180
+ let submitCalls = 0
1181
+ let submittedDraft: SubmitNotificationRequestT | null = null
1182
+ const client = {
1183
+ health: async () => true,
1184
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1185
+ listDevices: async () => ({ devices: deviceReady ? [readyIphone] : [] }),
1186
+ submit: async (draft: SubmitNotificationRequestT) => {
1187
+ submitCalls += 1
1188
+ submittedDraft = draft
1189
+ return setupReceipt()
1190
+ },
1191
+ evidence: async (requestId: string) =>
1192
+ setupEvidence(requestId, {
1193
+ state: 'observed',
1194
+ observed_at: '2026-08-05T18:00:02.000Z',
1195
+ latency_ms: 1_000,
1196
+ }),
1197
+ } as unknown as ApiClient
1198
+ const deps: CommandDeps = {
1199
+ ...makeDeps(io, client),
1200
+ cwd,
1201
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: path.join(cwd, 'state') },
1202
+ now: () => now,
1203
+ sleep: async (milliseconds) => {
1204
+ now += milliseconds
1205
+ deviceReady = true
1206
+ },
1207
+ }
1208
+
1209
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.ok)
1210
+ expect(io.prompts).toEqual(['Wait here while you finish that on your device?'])
1211
+ expect(io.spinnerEvents).toContain('stop:iPhone is ready to receive')
1212
+ expect(io.spinnerEvents).toContain('stop:Receipt observed from iPhone')
1213
+ expect(io.outLines.join('\n')).toContain('Companion Receipt observed from iPhone')
1214
+ expect(io.outLines.join('\n')).toContain('All set.')
1215
+ expect(submitCalls).toBe(1)
1216
+ expect(submittedDraft?.draft.event).toBe('setup_verified')
1217
+ expect(submittedDraft?.draft.targets).toEqual({ mode: 'selected', device_ids: ['dev_iphone'] })
1218
+
1219
+ io.outLines = []
1220
+ io.prompts = []
1221
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.ok)
1222
+ expect(submitCalls).toBe(1)
1223
+ expect(io.prompts).toEqual([])
1224
+ expect(io.outLines.join('\n')).toContain('All set.')
1225
+ })
1226
+
1227
+ it('persists a partial proof and checks the same request instead of sending again', async () => {
1228
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-proof-partial-'))
1229
+ const io = new CapturedIo()
1230
+ let now = 0
1231
+ let submitCalls = 0
1232
+ let savedRequestMissing = false
1233
+ const client = {
1234
+ health: async () => true,
1235
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1236
+ listDevices: async () => ({ devices: [readyIphone] }),
1237
+ submit: async () => {
1238
+ submitCalls += 1
1239
+ return setupReceipt(submitCalls === 1 ? 'req_partial' : 'req_replacement')
1240
+ },
1241
+ evidence: async (requestId: string) => {
1242
+ if (savedRequestMissing && requestId === 'req_partial') {
1243
+ throw new ApiCallError(404, 'not_found', 'No such request.')
1244
+ }
1245
+ return setupEvidence(
1246
+ requestId,
1247
+ savedRequestMissing
1248
+ ? {
1249
+ state: 'observed',
1250
+ observed_at: '2026-08-05T18:00:02.000Z',
1251
+ latency_ms: 1_000,
1252
+ }
1253
+ : { state: 'unknown', observed_at: null, latency_ms: null },
1254
+ )
1255
+ },
1256
+ } as unknown as ApiClient
1257
+ const deps: CommandDeps = {
1258
+ ...makeDeps(io, client),
1259
+ cwd,
1260
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: path.join(cwd, 'state') },
1261
+ now: () => now,
1262
+ sleep: async (milliseconds) => {
1263
+ now += milliseconds
1264
+ },
1265
+ }
1266
+
1267
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
1268
+ expect(submitCalls).toBe(1)
1269
+ expect(io.outLines.join('\n')).toContain('Next: Delivery proof')
1270
+ expect(io.errLines.join('\n')).toContain('not proof of non-receipt')
1271
+
1272
+ io.outLines = []
1273
+ io.errLines = []
1274
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
1275
+ expect(submitCalls).toBe(1)
1276
+ expect(io.outLines.join('\n')).toContain('Checking verification notification req_partial again.')
1277
+
1278
+ io.outLines = []
1279
+ io.errLines = []
1280
+ savedRequestMissing = true
1281
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.ok)
1282
+ expect(submitCalls).toBe(2)
1283
+ expect(io.outLines.join('\n')).toContain('saved proof had expired; sent replacement req_replacement')
1284
+ expect(io.outLines.join('\n')).toContain('Companion Receipt observed from iPhone')
1285
+ })
1286
+
1287
+ it('reports a proof-state write failure instead of crashing or sending twice', async () => {
1288
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-proof-unwritable-'))
1289
+ const io = new CapturedIo()
1290
+ let submitCalls = 0
1291
+ const client = {
1292
+ health: async () => true,
1293
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1294
+ listDevices: async () => ({ devices: [readyIphone] }),
1295
+ submit: async () => {
1296
+ submitCalls += 1
1297
+ return setupReceipt('req_unwritable')
1298
+ },
1299
+ } as unknown as ApiClient
1300
+ const deps = {
1301
+ ...makeDeps(io, client),
1302
+ cwd,
1303
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: '/proc/notifai-unwritable' },
1304
+ }
1305
+
1306
+ await expect(initCommand(deps, { hooks: false, skills: false })).resolves.toBe(EXIT.failed)
1307
+ expect(submitCalls).toBe(1)
1308
+ expect(io.errLines.join('\n')).toContain('Could not save setup proof req_unwritable')
1309
+ expect(io.outLines.join('\n')).toContain('Next: Delivery proof')
1310
+ })
1311
+
1312
+ it('does not claim proof for a macOS-only setup whose receipt bridge is unavailable', async () => {
1313
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-macos-proof-'))
1314
+ const io = new CapturedIo()
1315
+ let submitCalls = 0
1316
+ const client = {
1317
+ health: async () => true,
1318
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1319
+ listDevices: async () => ({
1320
+ devices: [{ ...readyIphone, device_id: 'dev_mac', display_name: 'Mac', platform: 'macos' }],
1321
+ }),
1322
+ submit: async () => {
1323
+ submitCalls += 1
1324
+ return setupReceipt()
1325
+ },
1326
+ } as unknown as ApiClient
1327
+ const deps = {
1328
+ ...makeDeps(io, client),
1329
+ cwd,
1330
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: path.join(cwd, 'state') },
1331
+ }
1332
+
1333
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
1334
+ expect(submitCalls).toBe(0)
1335
+ expect(io.outLines.join('\n')).toContain('Next: Delivery proof')
1336
+ expect(io.outLines.join('\n')).toContain('no supported macOS receipt bridge')
1337
+ })
1338
+
1339
+ it('treats a revoked credential as the one blocker and points back to pairing', async () => {
1340
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'init-revoked-'))
1341
+ const io = new CapturedIo()
1342
+ const client = {
1343
+ health: async () => true,
1344
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1345
+ listDevices: async () => {
1346
+ throw new ApiCallError(401, 'machine_revoked', 'This machine was revoked.')
1347
+ },
1348
+ } as unknown as ApiClient
1349
+ const deps = {
1350
+ ...makeDeps(io, client),
1351
+ cwd,
1352
+ env: { XDG_CONFIG_HOME: path.join(cwd, 'config'), XDG_STATE_HOME: path.join(cwd, 'state') },
1353
+ }
1354
+
1355
+ expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
1356
+ const out = io.outLines.join('\n')
1357
+ expect(out).toContain('Next: Account')
1358
+ expect(out).toContain('pair it again')
1359
+ expect(out).toContain('notifai login')
1360
+ expect(out.match(/^Next:/gm)).toHaveLength(1)
1361
+ })
1362
+
1363
+ it('scopes proof to each project worktree even on the same paired machine', async () => {
1364
+ const stateRoot = mkdtempSync(path.join(os.tmpdir(), 'init-worktrees-state-'))
1365
+ let submitCalls = 0
1366
+ const client = {
1367
+ health: async () => true,
1368
+ capabilities: async () => ({ schema_version: 1, platform: 'ios' }),
1369
+ listDevices: async () => ({ devices: [readyIphone] }),
1370
+ submit: async () => {
1371
+ submitCalls += 1
1372
+ return setupReceipt(`req_worktree_${submitCalls}`)
1373
+ },
1374
+ evidence: async (requestId: string) =>
1375
+ setupEvidence(requestId, {
1376
+ state: 'observed',
1377
+ observed_at: '2026-08-05T18:00:02.000Z',
1378
+ latency_ms: 1_000,
1379
+ }),
1380
+ } as unknown as ApiClient
1381
+
1382
+ for (const name of ['worktree-a', 'worktree-b']) {
1383
+ const cwd = mkdtempSync(path.join(os.tmpdir(), `${name}-`))
1384
+ const io = new CapturedIo()
1385
+ const deps = {
1386
+ ...makeDeps(io, client),
1387
+ cwd,
1388
+ env: { XDG_CONFIG_HOME: stateRoot, XDG_STATE_HOME: stateRoot },
1389
+ }
1390
+ expect(
1391
+ await initCommand(deps, { projectId: 'shared-project', hooks: false, skills: false }),
1392
+ ).toBe(EXIT.ok)
1393
+ }
1394
+
1395
+ expect(submitCalls).toBe(2)
1396
+ })
973
1397
  })
974
1398
 
975
1399
  describe('an outage is not an answer', () => {