@pi-in-go/pigpen-ahp 0.1.0

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 (304) hide show
  1. package/CREDITS.md +27 -0
  2. package/LICENSE +23 -0
  3. package/README.md +148 -0
  4. package/extensions/ahp/access_options_test.go +116 -0
  5. package/extensions/ahp/extension.go +54 -0
  6. package/extensions/ahp/go.mod +12 -0
  7. package/extensions/ahp/go.sum +2 -0
  8. package/extensions/ahp/go.work +8 -0
  9. package/extensions/ahp/internal/channels/chat.go +110 -0
  10. package/extensions/ahp/internal/channels/root.go +32 -0
  11. package/extensions/ahp/internal/channels/session.go +128 -0
  12. package/extensions/ahp/internal/channels/session_test.go +108 -0
  13. package/extensions/ahp/internal/compose/compose.go +153 -0
  14. package/extensions/ahp/internal/compose/compose_test.go +120 -0
  15. package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
  16. package/extensions/ahp/internal/host/handshake_test.go +206 -0
  17. package/extensions/ahp/internal/host/host.go +1305 -0
  18. package/extensions/ahp/internal/host/reconnect_test.go +197 -0
  19. package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
  20. package/extensions/ahp/internal/host/schema_test.go +79 -0
  21. package/extensions/ahp/internal/host/store.go +291 -0
  22. package/extensions/ahp/internal/host/store_update_test.go +85 -0
  23. package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
  24. package/extensions/ahp/internal/host/surface_test.go +95 -0
  25. package/extensions/ahp/internal/host/workarounds.go +458 -0
  26. package/extensions/ahp/internal/host/workarounds_test.go +96 -0
  27. package/extensions/ahp/internal/live/live.go +328 -0
  28. package/extensions/ahp/internal/live/live_test.go +91 -0
  29. package/extensions/ahp/internal/live/store.go +113 -0
  30. package/extensions/ahp/internal/mapper/activity.go +199 -0
  31. package/extensions/ahp/internal/mapper/activity_test.go +188 -0
  32. package/extensions/ahp/internal/mapper/bench_test.go +38 -0
  33. package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
  34. package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
  35. package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
  36. package/extensions/ahp/internal/mapper/js.go +131 -0
  37. package/extensions/ahp/internal/mapper/mapper.go +600 -0
  38. package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
  39. package/extensions/ahp/internal/mapper/messageinput.go +272 -0
  40. package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
  41. package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
  42. package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
  43. package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
  44. package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
  45. package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
  46. package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
  47. package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
  48. package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
  49. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
  50. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
  51. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
  52. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
  53. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
  54. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
  55. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
  56. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
  57. package/extensions/ahp/internal/mapper/title.go +34 -0
  58. package/extensions/ahp/internal/mapper/usermsg.go +87 -0
  59. package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
  60. package/extensions/ahp/internal/pi/backend.go +64 -0
  61. package/extensions/ahp/internal/pi/catalogue.go +370 -0
  62. package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
  63. package/extensions/ahp/internal/pi/chatdriver.go +533 -0
  64. package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
  65. package/extensions/ahp/internal/pi/clientactions.go +212 -0
  66. package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
  67. package/extensions/ahp/internal/pi/completions.go +230 -0
  68. package/extensions/ahp/internal/pi/completions_test.go +272 -0
  69. package/extensions/ahp/internal/pi/config_test.go +269 -0
  70. package/extensions/ahp/internal/pi/deletesession.go +69 -0
  71. package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
  72. package/extensions/ahp/internal/pi/disposal_test.go +362 -0
  73. package/extensions/ahp/internal/pi/fixture_test.go +308 -0
  74. package/extensions/ahp/internal/pi/foreign_test.go +122 -0
  75. package/extensions/ahp/internal/pi/harness_test.go +83 -0
  76. package/extensions/ahp/internal/pi/history.go +269 -0
  77. package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
  78. package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
  79. package/extensions/ahp/internal/pi/hydration_test.go +209 -0
  80. package/extensions/ahp/internal/pi/hydrator.go +212 -0
  81. package/extensions/ahp/internal/pi/imageinput.go +224 -0
  82. package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
  83. package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
  84. package/extensions/ahp/internal/pi/models.go +212 -0
  85. package/extensions/ahp/internal/pi/models_test.go +128 -0
  86. package/extensions/ahp/internal/pi/paging.go +95 -0
  87. package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
  88. package/extensions/ahp/internal/pi/projecttrust.go +222 -0
  89. package/extensions/ahp/internal/pi/registry.go +924 -0
  90. package/extensions/ahp/internal/pi/restart_test.go +185 -0
  91. package/extensions/ahp/internal/pi/services.go +70 -0
  92. package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
  93. package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
  94. package/extensions/ahp/internal/pi/sessionstore.go +83 -0
  95. package/extensions/ahp/internal/pi/summary_test.go +426 -0
  96. package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
  97. package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
  98. package/extensions/ahp/internal/pisession/json.go +31 -0
  99. package/extensions/ahp/internal/pisession/pisession.go +580 -0
  100. package/extensions/ahp/internal/settings/access.go +63 -0
  101. package/extensions/ahp/internal/settings/access_test.go +52 -0
  102. package/extensions/ahp/internal/settings/settings.go +117 -0
  103. package/extensions/ahp/internal/settings/settings_test.go +104 -0
  104. package/extensions/ahp/internal/svc/etag_other.go +15 -0
  105. package/extensions/ahp/internal/svc/etag_unix.go +34 -0
  106. package/extensions/ahp/internal/svc/glob.go +197 -0
  107. package/extensions/ahp/internal/svc/mime.go +37 -0
  108. package/extensions/ahp/internal/svc/paths.go +108 -0
  109. package/extensions/ahp/internal/svc/pty.go +64 -0
  110. package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
  111. package/extensions/ahp/internal/svc/pty_linux.go +36 -0
  112. package/extensions/ahp/internal/svc/pty_other.go +9 -0
  113. package/extensions/ahp/internal/svc/pty_test.go +114 -0
  114. package/extensions/ahp/internal/svc/pty_unix.go +147 -0
  115. package/extensions/ahp/internal/svc/resource.go +495 -0
  116. package/extensions/ahp/internal/svc/resource_test.go +501 -0
  117. package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
  118. package/extensions/ahp/internal/svc/stat_linux.go +9 -0
  119. package/extensions/ahp/internal/svc/terminal.go +454 -0
  120. package/extensions/ahp/internal/svc/terminal_test.go +512 -0
  121. package/extensions/ahp/internal/svc/watch.go +590 -0
  122. package/extensions/ahp/internal/svc/watch_test.go +627 -0
  123. package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
  124. package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
  125. package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
  126. package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
  127. package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
  128. package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
  129. package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
  130. package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
  131. package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
  132. package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
  133. package/extensions/ahp/internal/testkit/schema.go +360 -0
  134. package/extensions/ahp/internal/testkit/testkit.go +367 -0
  135. package/extensions/ahp/internal/twin/twin.go +27 -0
  136. package/extensions/ahp/internal/wire/dispatchable.go +106 -0
  137. package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
  138. package/extensions/ahp/internal/wire/helpers_test.go +5 -0
  139. package/extensions/ahp/internal/wire/wire.go +218 -0
  140. package/extensions/ahp/internal/wire/wire_test.go +123 -0
  141. package/extensions/ahp/internal/ws/helpers_test.go +7 -0
  142. package/extensions/ahp/internal/ws/vscode_test.go +198 -0
  143. package/extensions/ahp/internal/ws/ws.go +674 -0
  144. package/extensions/ahp/internal/ws/ws_test.go +324 -0
  145. package/extensions/ahp/realpig_test.go +589 -0
  146. package/extensions/ahp/runtime.go +311 -0
  147. package/extensions/ahp/runtime_test.go +110 -0
  148. package/extensions/ahp/testdata/upstream-tests.json +3833 -0
  149. package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
  150. package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
  151. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
  152. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
  153. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
  154. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
  155. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
  156. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
  157. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
  158. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
  159. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
  160. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
  161. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
  162. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
  163. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
  164. package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
  165. package/extensions/ahp/twins_test.go +91 -0
  166. package/package.json +40 -0
  167. package/proof/PORT.md +121 -0
  168. package/proof/mutations.json +152 -0
  169. package/proof/oracle/LICENSE +21 -0
  170. package/proof/oracle/README.md +120 -0
  171. package/proof/oracle/UPSTREAM.md +7 -0
  172. package/proof/oracle/package.json +68 -0
  173. package/proof/oracle/src/bin/cli.ts +66 -0
  174. package/proof/oracle/src/bin/tunnel.ts +128 -0
  175. package/proof/oracle/src/channels/chat.ts +134 -0
  176. package/proof/oracle/src/channels/root.ts +39 -0
  177. package/proof/oracle/src/channels/session.ts +118 -0
  178. package/proof/oracle/src/channels/terminal.ts +11 -0
  179. package/proof/oracle/src/core/channels.ts +108 -0
  180. package/proof/oracle/src/core/client-workarounds.ts +358 -0
  181. package/proof/oracle/src/core/connection.ts +41 -0
  182. package/proof/oracle/src/core/host.ts +882 -0
  183. package/proof/oracle/src/core/sequencer.ts +75 -0
  184. package/proof/oracle/src/core/state-store.ts +162 -0
  185. package/proof/oracle/src/core/uri.ts +25 -0
  186. package/proof/oracle/src/host/direct-settings.ts +110 -0
  187. package/proof/oracle/src/host/pi-host.ts +220 -0
  188. package/proof/oracle/src/host/serve.ts +71 -0
  189. package/proof/oracle/src/host/terminal-service.ts +346 -0
  190. package/proof/oracle/src/pi/activity.ts +171 -0
  191. package/proof/oracle/src/pi/changeset-service.ts +646 -0
  192. package/proof/oracle/src/pi/changeset-uri.ts +64 -0
  193. package/proof/oracle/src/pi/chat-driver.ts +529 -0
  194. package/proof/oracle/src/pi/completions.ts +160 -0
  195. package/proof/oracle/src/pi/delete-session.ts +53 -0
  196. package/proof/oracle/src/pi/event-mapper.ts +648 -0
  197. package/proof/oracle/src/pi/git-changes.ts +605 -0
  198. package/proof/oracle/src/pi/history.ts +305 -0
  199. package/proof/oracle/src/pi/image-input.ts +56 -0
  200. package/proof/oracle/src/pi/image-mime.ts +8 -0
  201. package/proof/oracle/src/pi/in-process-backend.ts +155 -0
  202. package/proof/oracle/src/pi/message-input.ts +212 -0
  203. package/proof/oracle/src/pi/models.ts +124 -0
  204. package/proof/oracle/src/pi/project-trust.ts +66 -0
  205. package/proof/oracle/src/pi/provider.ts +2 -0
  206. package/proof/oracle/src/pi/resource-paths.ts +80 -0
  207. package/proof/oracle/src/pi/resource-service.ts +356 -0
  208. package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
  209. package/proof/oracle/src/pi/resource-watch.ts +357 -0
  210. package/proof/oracle/src/pi/session-catalogue.ts +318 -0
  211. package/proof/oracle/src/pi/session-config.ts +84 -0
  212. package/proof/oracle/src/pi/session-history.ts +75 -0
  213. package/proof/oracle/src/pi/session-hydrator.ts +219 -0
  214. package/proof/oracle/src/pi/session-registry.ts +815 -0
  215. package/proof/oracle/src/pi/session-storage.ts +29 -0
  216. package/proof/oracle/src/pi/session-title.ts +18 -0
  217. package/proof/oracle/src/pi/turn-paging.ts +94 -0
  218. package/proof/oracle/src/pi/user-message.ts +66 -0
  219. package/proof/oracle/src/protocol/errors.ts +44 -0
  220. package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
  221. package/proof/oracle/src/protocol/version.ts +39 -0
  222. package/proof/oracle/src/transport/websocket.ts +136 -0
  223. package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
  224. package/proof/oracle/src/tunnel/discovery.ts +37 -0
  225. package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
  226. package/proof/oracle/test/activity.test.ts +171 -0
  227. package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
  228. package/proof/oracle/test/changeset-uri.test.ts +37 -0
  229. package/proof/oracle/test/changeset.test.serial.ts +487 -0
  230. package/proof/oracle/test/chat-driver.test.ts +691 -0
  231. package/proof/oracle/test/client-actions.test.ts +409 -0
  232. package/proof/oracle/test/client-workarounds.test.ts +320 -0
  233. package/proof/oracle/test/completions.test.ts +323 -0
  234. package/proof/oracle/test/delete-session.test.ts +142 -0
  235. package/proof/oracle/test/direct-settings.test.ts +81 -0
  236. package/proof/oracle/test/event-mapper.test.ts +619 -0
  237. package/proof/oracle/test/fetch-turns.test.ts +207 -0
  238. package/proof/oracle/test/fixtures/abort.json +711 -0
  239. package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
  240. package/proof/oracle/test/fixtures/compaction.json +6285 -0
  241. package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
  242. package/proof/oracle/test/fixtures/plain-text.json +1856 -0
  243. package/proof/oracle/test/fixtures/single-tool.json +1128 -0
  244. package/proof/oracle/test/fixtures/steering.json +3926 -0
  245. package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
  246. package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
  247. package/proof/oracle/test/fixtures/tool-error.json +2850 -0
  248. package/proof/oracle/test/fixtures/tool-find.json +1162 -0
  249. package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
  250. package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
  251. package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
  252. package/proof/oracle/test/fixtures/tool-write.json +1271 -0
  253. package/proof/oracle/test/handshake.test.ts +275 -0
  254. package/proof/oracle/test/harness.ts +153 -0
  255. package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
  256. package/proof/oracle/test/image-input.test.ts +28 -0
  257. package/proof/oracle/test/image-session.test.ts +69 -0
  258. package/proof/oracle/test/live-turn.test.ts +304 -0
  259. package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
  260. package/proof/oracle/test/message-input.test.ts +181 -0
  261. package/proof/oracle/test/model-discovery.test.ts +170 -0
  262. package/proof/oracle/test/models.test.ts +109 -0
  263. package/proof/oracle/test/pi-host.test.ts +91 -0
  264. package/proof/oracle/test/pi-replay.test.ts +141 -0
  265. package/proof/oracle/test/project-trust.test.ts +125 -0
  266. package/proof/oracle/test/protocol-surface.test.ts +121 -0
  267. package/proof/oracle/test/pty.test.ts +100 -0
  268. package/proof/oracle/test/reconnect.test.ts +389 -0
  269. package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
  270. package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
  271. package/proof/oracle/test/resource.test.ts +499 -0
  272. package/proof/oracle/test/schema.test.ts +88 -0
  273. package/proof/oracle/test/session-catalogue.test.ts +314 -0
  274. package/proof/oracle/test/session-config.test.ts +224 -0
  275. package/proof/oracle/test/session-disposal.test.ts +364 -0
  276. package/proof/oracle/test/session-hydration.test.ts +194 -0
  277. package/proof/oracle/test/session-lifecycle.test.ts +416 -0
  278. package/proof/oracle/test/session-storage.test.serial.ts +32 -0
  279. package/proof/oracle/test/session-summary.test.ts +418 -0
  280. package/proof/oracle/test/subscriptions.test.ts +198 -0
  281. package/proof/oracle/test/support/assertions.ts +32 -0
  282. package/proof/oracle/test/support/async.ts +26 -0
  283. package/proof/oracle/test/support/hydrated-session.ts +205 -0
  284. package/proof/oracle/test/support/images.ts +8 -0
  285. package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
  286. package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
  287. package/proof/oracle/test/support/replay.ts +194 -0
  288. package/proof/oracle/test/support/schema.ts +221 -0
  289. package/proof/oracle/test/support/session-files.ts +12 -0
  290. package/proof/oracle/test/support/session-storage.ts +22 -0
  291. package/proof/oracle/test/support/upstream.ts +25 -0
  292. package/proof/oracle/test/support/watch-events.ts +108 -0
  293. package/proof/oracle/test/terminal-service.test.ts +495 -0
  294. package/proof/oracle/test/truncate.test.ts +230 -0
  295. package/proof/oracle/test/tunnel.test.ts +258 -0
  296. package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
  297. package/proof/oracle/test/uri.test.ts +24 -0
  298. package/proof/oracle/test/watch-events.test.ts +118 -0
  299. package/proof/oracle/tsconfig.json +32 -0
  300. package/proof/proof-piglet/piglet.yaml +18 -0
  301. package/proof/tools/fakellm/go.mod +3 -0
  302. package/proof/tools/fakellm/main.go +109 -0
  303. package/proof/tools/gen-dispatchable.py +30 -0
  304. package/provenance.json +28 -0
@@ -0,0 +1,308 @@
1
+ package pi_test
2
+
3
+ import (
4
+ "context"
5
+ "encoding/base64"
6
+ "encoding/json"
7
+ "sync"
8
+ "testing"
9
+
10
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
11
+
12
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
13
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
14
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
15
+ "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
16
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
17
+ )
18
+
19
+ // onePixelPNG is upstream's test/support/images.ts ONE_PIXEL_PNG.
20
+ const onePixelPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
21
+
22
+ type obj = map[string]any
23
+
24
+ // event decodes a JSON literal into a Pi event.
25
+ func event(t testing.TB, literal string) mapper.Event {
26
+ t.Helper()
27
+ var e mapper.Event
28
+ if err := json.Unmarshal([]byte(literal), &e); err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ return e
32
+ }
33
+
34
+ // say is upstream's chat-driver test script: one assistant message that says text, then settles.
35
+ func say(t testing.TB, text string) []mapper.Event {
36
+ quoted, _ := json.Marshal(text)
37
+ return []mapper.Event{
38
+ event(t, `{"type":"agent_start"}`),
39
+ event(t, `{"type":"message_start","message":{"role":"assistant"}}`),
40
+ event(t, `{"type":"message_update","message":{"role":"assistant"},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":`+string(quoted)+`}}`),
41
+ event(t, `{"type":"message_end","message":{"role":"assistant"}}`),
42
+ event(t, `{"type":"agent_end","messages":[],"willRetry":false}`),
43
+ event(t, `{"type":"agent_settled"}`),
44
+ }
45
+ }
46
+
47
+ // release is a gate a test can open exactly once.
48
+ type release struct {
49
+ once sync.Once
50
+ ch chan struct{}
51
+ }
52
+
53
+ func newRelease() *release { return &release{ch: make(chan struct{})} }
54
+ func (r *release) open() { r.once.Do(func() { close(r.ch) }) }
55
+
56
+ // scriptedBackend records prompts and replays a scripted event sequence for each one, so a
57
+ // turn's shape is fully determined by the test (upstream ScriptedBackend).
58
+ type scriptedBackend struct {
59
+ mu sync.Mutex
60
+ prompts []string
61
+ promptImages [][]mapper.Image
62
+ steers []string
63
+ steerImages [][]mapper.Image
64
+ promptCalls int
65
+ aborts int
66
+ selections int
67
+ promptGate *release
68
+ selectionGate *release
69
+ abortGate *release
70
+ listeners map[int]func(mapper.Event)
71
+ nextListener int
72
+ script func(text string) []mapper.Event
73
+ }
74
+
75
+ func newScriptedBackend(script func(string) []mapper.Event) *scriptedBackend {
76
+ return &scriptedBackend{script: script, listeners: map[int]func(mapper.Event){}}
77
+ }
78
+
79
+ func (b *scriptedBackend) Subscribe(l func(mapper.Event)) func() {
80
+ b.mu.Lock()
81
+ defer b.mu.Unlock()
82
+ id := b.nextListener
83
+ b.nextListener++
84
+ b.listeners[id] = l
85
+ return func() { b.mu.Lock(); delete(b.listeners, id); b.mu.Unlock() }
86
+ }
87
+
88
+ func (b *scriptedBackend) emit(e mapper.Event) {
89
+ b.mu.Lock()
90
+ ls := make([]func(mapper.Event), 0, len(b.listeners))
91
+ for _, l := range b.listeners {
92
+ ls = append(ls, l)
93
+ }
94
+ b.mu.Unlock()
95
+ for _, l := range ls {
96
+ l(e)
97
+ }
98
+ }
99
+
100
+ func (b *scriptedBackend) Prompt(ctx context.Context, text string, images []mapper.Image) error {
101
+ b.mu.Lock()
102
+ b.promptCalls++
103
+ gate := b.promptGate
104
+ b.mu.Unlock()
105
+ if gate != nil {
106
+ <-gate.ch
107
+ }
108
+ if ctx.Err() != nil {
109
+ return nil
110
+ }
111
+ b.mu.Lock()
112
+ b.prompts = append(b.prompts, text)
113
+ b.promptImages = append(b.promptImages, images)
114
+ b.mu.Unlock()
115
+ // Deliver like a real agent: the driver must not depend on events arriving inside Prompt.
116
+ for _, e := range b.script(text) {
117
+ b.emit(e)
118
+ }
119
+ return nil
120
+ }
121
+
122
+ func (b *scriptedBackend) Steer(_ context.Context, text string, images []mapper.Image) error {
123
+ b.mu.Lock()
124
+ defer b.mu.Unlock()
125
+ b.steers = append(b.steers, text)
126
+ b.steerImages = append(b.steerImages, images)
127
+ return nil
128
+ }
129
+
130
+ func (b *scriptedBackend) Abort(context.Context) error {
131
+ b.mu.Lock()
132
+ b.aborts++
133
+ gate := b.abortGate
134
+ b.mu.Unlock()
135
+ if gate != nil {
136
+ <-gate.ch
137
+ }
138
+ return nil
139
+ }
140
+
141
+ func (b *scriptedBackend) SelectModel(context.Context, ahptypes.ModelSelection) error {
142
+ b.mu.Lock()
143
+ b.selections++
144
+ gate := b.selectionGate
145
+ b.mu.Unlock()
146
+ if gate != nil {
147
+ <-gate.ch
148
+ }
149
+ return nil
150
+ }
151
+
152
+ func (b *scriptedBackend) count(field func(*scriptedBackend) int) int {
153
+ b.mu.Lock()
154
+ defer b.mu.Unlock()
155
+ return field(b)
156
+ }
157
+
158
+ func (b *scriptedBackend) promptList() []string {
159
+ b.mu.Lock()
160
+ defer b.mu.Unlock()
161
+ return append([]string(nil), b.prompts...)
162
+ }
163
+
164
+ // funcBackend is a backend made of closures.
165
+ type funcBackend struct {
166
+ subscribe func(func(mapper.Event)) func()
167
+ prompt func(ctx context.Context, text string) error
168
+ steer func(ctx context.Context, text string) error
169
+ abort func(ctx context.Context) error
170
+ }
171
+
172
+ func (b funcBackend) Subscribe(l func(mapper.Event)) func() {
173
+ if b.subscribe == nil {
174
+ return func() {}
175
+ }
176
+ return b.subscribe(l)
177
+ }
178
+ func (b funcBackend) Prompt(ctx context.Context, text string, _ []mapper.Image) error {
179
+ if b.prompt == nil {
180
+ return nil
181
+ }
182
+ return b.prompt(ctx, text)
183
+ }
184
+ func (b funcBackend) Steer(ctx context.Context, text string, _ []mapper.Image) error {
185
+ if b.steer == nil {
186
+ return nil
187
+ }
188
+ return b.steer(ctx, text)
189
+ }
190
+ func (b funcBackend) Abort(ctx context.Context) error {
191
+ if b.abort == nil {
192
+ return nil
193
+ }
194
+ return b.abort(ctx)
195
+ }
196
+
197
+ // fixture is a host with a session registry over in-memory storage and one connected client.
198
+ type fixture struct {
199
+ t *testing.T
200
+ host *host.Host
201
+ sessions *pi.Registry
202
+ client *testkit.Client
203
+ sessionChannel string
204
+ chatChannel string
205
+ id string
206
+ }
207
+
208
+ var uuidCounter struct {
209
+ sync.Mutex
210
+ n int
211
+ }
212
+
213
+ func newID() string {
214
+ uuidCounter.Lock()
215
+ defer uuidCounter.Unlock()
216
+ uuidCounter.n++
217
+ return "0190a1b2-c3d4-7e5f-8a9b-" + pad(uuidCounter.n)
218
+ }
219
+
220
+ func pad(n int) string {
221
+ s := "000000000000"
222
+ d := []byte(s)
223
+ for i := len(d) - 1; i >= 0 && n > 0; i-- {
224
+ d[i] = byte('0' + n%10)
225
+ n /= 10
226
+ }
227
+ return string(d)
228
+ }
229
+
230
+ func (f *fixture) chat() *ahptypes.ChatState { return f.host.Store().Chat(f.chatChannel) }
231
+ func (f *fixture) session() *ahptypes.SessionState { return f.host.Store().Session(f.sessionChannel) }
232
+
233
+ // start creates the host and registry (no session yet).
234
+ func startWith(t *testing.T, opts pi.RegistryOptions) (*host.Host, *pi.Registry) {
235
+ t.Helper()
236
+ h := testkit.NewHost(host.Options{})
237
+ opts.Host = h
238
+ if opts.CreateSessionManager == nil {
239
+ opts.CreateSessionManager = pi.InMemoryStorage
240
+ }
241
+ reg := pi.NewRegistry(opts)
242
+ h.Serve(host.Capabilities{Sessions: reg, TurnPaging: fetchTurns{reg}})
243
+ return h, reg
244
+ }
245
+
246
+ type fetchTurns struct{ r *pi.Registry }
247
+
248
+ func (f fetchTurns) FetchTurns(ctx context.Context, p ahptypes.FetchTurnsParams) error {
249
+ return f.r.FetchTurns(ctx, p)
250
+ }
251
+
252
+ // startFixture is upstream's chat-driver startFixture: a created session with both channels
253
+ // subscribed.
254
+ func startFixture(t *testing.T, backend pi.Backend) *fixture {
255
+ t.Helper()
256
+ h, reg := startWith(t, pi.RegistryOptions{CreateBackend: func(*pi.LiveSession) (pi.Backend, error) { return backend, nil }})
257
+ client := testkit.Connect(t, h)
258
+ t.Cleanup(client.Close)
259
+ client.Initialize("driver-client", nil)
260
+ id := newID()
261
+ f := &fixture{t: t, host: h, sessions: reg, client: client, id: id, sessionChannel: wire.SessionURI(id), chatChannel: wire.ChatURI(id)}
262
+ client.Must("createSession", obj{"channel": f.sessionChannel})
263
+ client.Subscribe(f.sessionChannel)
264
+ client.Subscribe(f.chatChannel)
265
+ // The session is ready once its backend is attached.
266
+ testkit.Eventually(t, "the session to become ready", func() bool {
267
+ s := f.session()
268
+ return s != nil && s.Lifecycle == ahptypes.SessionLifecycleReady
269
+ })
270
+ return f
271
+ }
272
+
273
+ func userMessage(text string) obj {
274
+ return obj{"text": text, "origin": obj{"kind": "user"}}
275
+ }
276
+
277
+ func embeddedText(text, label string) obj {
278
+ return obj{"type": "embeddedResource", "label": label, "contentType": "text/plain", "data": base64.StdEncoding.EncodeToString([]byte(text))}
279
+ }
280
+
281
+ func embeddedImage(label string) obj {
282
+ return obj{"type": "embeddedResource", "label": label, "displayKind": "image", "contentType": "image/png", "data": onePixelPNG}
283
+ }
284
+
285
+ func (f *fixture) turnStarted(turnID string, message obj) {
286
+ f.client.Dispatch(f.chatChannel, obj{"type": "chat/turnStarted", "turnId": turnID, "startedAt": "2025-01-01T00:00:00.000Z", "message": message})
287
+ }
288
+
289
+ func (f *fixture) pending(kind, id string, message obj) {
290
+ f.client.Dispatch(f.chatChannel, obj{"type": "chat/pendingMessageSet", "kind": kind, "id": id, "message": message})
291
+ }
292
+
293
+ func (f *fixture) cancel(turnID string) {
294
+ f.client.Dispatch(f.chatChannel, obj{"type": "chat/turnCancelled", "turnId": turnID, "duration": 0})
295
+ }
296
+
297
+ type turnShape struct {
298
+ ID, Text string
299
+ State ahptypes.TurnState
300
+ }
301
+
302
+ func turnShapes(turns []ahptypes.Turn) []turnShape {
303
+ out := []turnShape{}
304
+ for _, t := range turns {
305
+ out = append(out, turnShape{t.Id, t.Message.Text, t.State})
306
+ }
307
+ return out
308
+ }
@@ -0,0 +1,122 @@
1
+ package pi_test
2
+
3
+ import (
4
+ "testing"
5
+
6
+ ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
7
+
8
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
10
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
11
+ "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
12
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
13
+ )
14
+
15
+ // Additions (not upstream twins): a turn nobody dispatched through AHP, typed into the running
16
+ // PiG session, is mirrored into the chat when AdoptForeignTurns is on; upstream's host owns its
17
+ // agent, so it never sees one and ignores such events (which stays the default).
18
+
19
+ func foreignFixture(t *testing.T, adopt bool) (*fixture, *scriptedBackend) {
20
+ t.Helper()
21
+ backend := newScriptedBackend(func(string) []mapper.Event { return nil })
22
+ h, reg := startWith(t, pi.RegistryOptions{
23
+ CreateBackend: func(*pi.LiveSession) (pi.Backend, error) { return backend, nil },
24
+ AdoptForeignTurns: adopt,
25
+ })
26
+ client := testkit.Connect(t, h)
27
+ t.Cleanup(client.Close)
28
+ client.Initialize("foreign-client", nil)
29
+ id := newID()
30
+ f := &fixture{t: t, host: h, sessions: reg, client: client, id: id, sessionChannel: wire.SessionURI(id), chatChannel: wire.ChatURI(id)}
31
+ client.Must("createSession", obj{"channel": f.sessionChannel})
32
+ client.Subscribe(f.sessionChannel)
33
+ client.Subscribe(f.chatChannel)
34
+ testkit.Eventually(t, "the session to become ready", func() bool {
35
+ s := f.session()
36
+ return s != nil && s.Lifecycle == ahptypes.SessionLifecycleReady
37
+ })
38
+ return f, backend
39
+ }
40
+
41
+ func localPrompt(t *testing.T, backend *scriptedBackend, text, answer string) {
42
+ backend.emit(event(t, `{"type":"agent_start"}`))
43
+ backend.emit(event(t, `{"type":"turn_start"}`))
44
+ backend.emit(event(t, `{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"`+text+`"}]}}`))
45
+ backend.emit(event(t, `{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"`+text+`"}]}}`))
46
+ for _, e := range say(t, answer)[1:] {
47
+ backend.emit(e)
48
+ }
49
+ }
50
+
51
+ func TestForeignTurnsAreMirrored(t *testing.T) {
52
+ f, backend := foreignFixture(t, true)
53
+ localPrompt(t, backend, "typed locally", "answered locally")
54
+ testkit.Eventually(t, "the local turn to complete", func() bool {
55
+ c := f.chat()
56
+ return c != nil && len(c.Turns) == 1 && len(c.Turns[0].ResponseParts) > 0 && c.Activity == nil
57
+ })
58
+ turn := f.chat().Turns[0]
59
+ if turn.Message.Text != "typed locally" {
60
+ t.Fatalf("user message %q", turn.Message.Text)
61
+ }
62
+ if len(turn.ResponseParts) == 0 {
63
+ t.Fatal("the local answer never reached the chat")
64
+ }
65
+ // a second local prompt is a second turn
66
+ localPrompt(t, backend, "again", "second")
67
+ testkit.Eventually(t, "the second local turn", func() bool { return len(f.chat().Turns) == 2 })
68
+ }
69
+
70
+ func TestForeignTurnsAreIgnoredByDefault(t *testing.T) {
71
+ f, backend := foreignFixture(t, false)
72
+ localPrompt(t, backend, "typed locally", "answered locally")
73
+ if !f.client.Silent(func(n testkit.Notification) bool { env, ok := n.Envelope(); return ok && env.Channel == f.chatChannel }, 200_000_000) {
74
+ t.Fatal("a turn nobody started must be ignored unless AdoptForeignTurns is set")
75
+ }
76
+ if n := len(f.chat().Turns); n != 0 {
77
+ t.Fatalf("%d turns", n)
78
+ }
79
+ _ = host.Options{}
80
+ _ = mapper.Event{}
81
+ }
82
+
83
+ func TestCancelledTailIsNotAdopted(t *testing.T) {
84
+ f, backend := foreignFixture(t, true)
85
+ f.turnStarted("remote-1", userMessage("go"))
86
+ testkit.Eventually(t, "the prompt to reach the backend", func() bool { return backend.count(func(b *scriptedBackend) int { return b.promptCalls }) == 1 })
87
+ f.cancel("remote-1")
88
+ testkit.Eventually(t, "the cancel to reach the backend", func() bool { return backend.count(func(b *scriptedBackend) int { return b.aborts }) >= 1 })
89
+ // the aborted run's trailing events carry no user message, so they start nothing
90
+ for _, e := range say(t, "late")[1:] {
91
+ backend.emit(e)
92
+ }
93
+ if n := len(f.chat().Turns); n != 1 {
94
+ t.Fatalf("%d turns after a cancelled run's tail", n)
95
+ }
96
+ }
97
+
98
+ // While a cancellation is settling, a user message must not open a turn: it can only be the cancelled
99
+ // run's own input replaying, and adopting it would resurrect the turn the client just cancelled.
100
+ func TestNothingIsAdoptedWhileACancellationSettles(t *testing.T) {
101
+ f, backend := foreignFixture(t, true)
102
+ gate := newRelease()
103
+ backend.mu.Lock()
104
+ backend.abortGate = gate
105
+ backend.mu.Unlock()
106
+ f.turnStarted("remote-1", userMessage("go"))
107
+ testkit.Eventually(t, "the prompt to reach the backend", func() bool { return backend.count(func(b *scriptedBackend) int { return b.promptCalls }) == 1 })
108
+ f.cancel("remote-1")
109
+ testkit.Eventually(t, "the abort to be in flight", func() bool { return backend.count(func(b *scriptedBackend) int { return b.aborts }) == 1 })
110
+
111
+ backend.emit(event(t, `{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"replayed"}]}}`))
112
+ if n, active := len(f.chat().Turns), f.chat().ActiveTurn; n != 1 || active != nil {
113
+ t.Fatalf("a message during the settling cancellation opened a turn: %d turns, active=%v", n, active != nil)
114
+ }
115
+
116
+ gate.open()
117
+ // The settling has no signal of its own: a user message is offered until one is adopted.
118
+ testkit.Eventually(t, "a local prompt after the cancellation to be mirrored", func() bool {
119
+ backend.emit(event(t, `{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"typed later"}]}}`))
120
+ return f.chat().ActiveTurn != nil
121
+ })
122
+ }
@@ -0,0 +1,83 @@
1
+ package pi_test
2
+
3
+ import (
4
+ "path/filepath"
5
+ "sync"
6
+ "sync/atomic"
7
+ "testing"
8
+
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
10
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
11
+ "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
12
+ )
13
+
14
+ // harness is upstream test/harness.ts: a live host with the session services over a fixture-owned
15
+ // session root. Clients attach in memory (the transport itself is covered by internal/ws).
16
+ type harness struct {
17
+ t *testing.T
18
+ host *host.Host
19
+ services *pi.Services
20
+ root string
21
+
22
+ mu sync.Mutex
23
+ deletedFiles []string
24
+ }
25
+
26
+ type harnessOptions struct {
27
+ createBackend pi.BackendFactory
28
+ deleteFile func(path string) (pi.SessionFileDeletionResult, error)
29
+ sessionRoot string
30
+ replayBuffer int
31
+ workingDir string
32
+ }
33
+
34
+ func startHarness(t *testing.T, o harnessOptions) *harness {
35
+ t.Helper()
36
+ h := &harness{t: t}
37
+ h.root = o.sessionRoot
38
+ if h.root == "" {
39
+ h.root = t.TempDir()
40
+ }
41
+ h.host = testkit.NewHost(host.Options{ReplayBufferCapacity: o.replayBuffer})
42
+ deleteFile := o.deleteFile
43
+ if deleteFile == nil {
44
+ deleteFile = func(path string) (pi.SessionFileDeletionResult, error) {
45
+ h.mu.Lock()
46
+ h.deletedFiles = append(h.deletedFiles, path)
47
+ h.mu.Unlock()
48
+ return pi.SessionFileDeletionResult{OK: true}, nil
49
+ }
50
+ }
51
+ workingDir := o.workingDir
52
+ if workingDir == "" {
53
+ workingDir = t.TempDir()
54
+ }
55
+ h.services = pi.NewServices(pi.ServicesOptions{
56
+ Host: h.host, SessionRoot: h.root, DefaultWorkingDirectory: workingDir,
57
+ CreateBackend: o.createBackend, CreateSessionManager: pi.PersistentStorage(filepath.Join(h.root, "created")),
58
+ DeleteFile: deleteFile,
59
+ })
60
+ h.host.Serve(h.services.Capabilities())
61
+ return h
62
+ }
63
+
64
+ func (h *harness) deleted() []string {
65
+ h.mu.Lock()
66
+ defer h.mu.Unlock()
67
+ return append([]string(nil), h.deletedFiles...)
68
+ }
69
+
70
+ var clientCounter atomic.Int64
71
+
72
+ func nextClientID() string { return "test-client-" + itoa(int(clientCounter.Add(1))) }
73
+
74
+ func itoa(i int) string {
75
+ if i == 0 {
76
+ return "0"
77
+ }
78
+ var d []byte
79
+ for ; i > 0; i /= 10 {
80
+ d = append([]byte{byte('0' + i%10)}, d...)
81
+ }
82
+ return string(d)
83
+ }