@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,452 @@
1
+ package svc
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "fmt"
7
+ "sync"
8
+ "testing"
9
+ "time"
10
+
11
+ "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
12
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
13
+ ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
14
+ )
15
+
16
+ // watchEvents is the test-side journal of one watch subscription: one reader per subscription, a
17
+ // retained event journal, assertions that observe it (port of test/support/watch-events.ts).
18
+
19
+ type batchSource interface {
20
+ // Next blocks for the next batch; it returns errStopped once stop closes.
21
+ Next(stop <-chan struct{}) ([]ahptypes.ResourceChange, error)
22
+ }
23
+
24
+ var errStopped = errors.New("watch reader stopped")
25
+
26
+ type watchEvents struct {
27
+ src batchSource
28
+ clock Clock
29
+ describe func() any
30
+
31
+ mu sync.Mutex
32
+ batches [][]ahptypes.ResourceChange
33
+ err error
34
+ closed bool
35
+ changed chan struct{}
36
+
37
+ stop chan struct{}
38
+ reading chan struct{}
39
+ }
40
+
41
+ func newWatchEvents(src batchSource, clock Clock, describe func() any) *watchEvents {
42
+ if clock == nil {
43
+ clock = realClock{}
44
+ }
45
+ if describe == nil {
46
+ describe = func() any { return map[string]any{} }
47
+ }
48
+ e := &watchEvents{src: src, clock: clock, describe: describe, changed: make(chan struct{}), stop: make(chan struct{}), reading: make(chan struct{})}
49
+ go e.read()
50
+ return e
51
+ }
52
+
53
+ func (e *watchEvents) notifyLocked() {
54
+ close(e.changed)
55
+ e.changed = make(chan struct{})
56
+ }
57
+
58
+ func (e *watchEvents) read() {
59
+ defer close(e.reading)
60
+ for {
61
+ batch, err := e.src.Next(e.stop)
62
+ e.mu.Lock()
63
+ if e.closed {
64
+ e.mu.Unlock()
65
+ return
66
+ }
67
+ if err != nil {
68
+ e.err = err
69
+ e.notifyLocked()
70
+ e.mu.Unlock()
71
+ return
72
+ }
73
+ e.batches = append(e.batches, batch)
74
+ e.notifyLocked()
75
+ e.mu.Unlock()
76
+ }
77
+ }
78
+
79
+ func (e *watchEvents) changesLocked() []ahptypes.ResourceChange {
80
+ var all []ahptypes.ResourceChange
81
+ for _, b := range e.batches {
82
+ all = append(all, b...)
83
+ }
84
+ return all
85
+ }
86
+
87
+ func (e *watchEvents) Changes() []ahptypes.ResourceChange {
88
+ e.mu.Lock()
89
+ defer e.mu.Unlock()
90
+ return e.changesLocked()
91
+ }
92
+
93
+ func (e *watchEvents) Batches() [][]ahptypes.ResourceChange {
94
+ e.mu.Lock()
95
+ defer e.mu.Unlock()
96
+ return append([][]ahptypes.ResourceChange(nil), e.batches...)
97
+ }
98
+
99
+ func (e *watchEvents) Mark() int { return len(e.Changes()) }
100
+
101
+ // WaitFor blocks until complete accepts the changes since a checkpoint. A timeout stops only this
102
+ // assertion; it never starts or strands another read.
103
+ func (e *watchEvents) WaitFor(label string, complete func([]ahptypes.ResourceChange) bool, since int, timeout time.Duration) ([]ahptypes.ResourceChange, error) {
104
+ timedOut := make(chan struct{})
105
+ timer := e.clock.AfterFunc(timeout, func() { close(timedOut) })
106
+ defer timer.Stop()
107
+ fail := func(cause error) error {
108
+ e.mu.Lock()
109
+ batches, _ := json.Marshal(e.batches)
110
+ e.mu.Unlock()
111
+ state, _ := json.Marshal(e.describe())
112
+ return fmt.Errorf("Expected %s; watch=%s; batches=%s: %w", label, state, batches, cause)
113
+ }
114
+ for {
115
+ e.mu.Lock()
116
+ if e.err != nil || e.closed {
117
+ err := e.err
118
+ if err == nil {
119
+ err = errors.New("Collector closed")
120
+ }
121
+ e.mu.Unlock()
122
+ return nil, fail(err)
123
+ }
124
+ changes := e.changesLocked()
125
+ if since > len(changes) {
126
+ since = len(changes)
127
+ }
128
+ changes = changes[since:]
129
+ if complete(changes) {
130
+ e.mu.Unlock()
131
+ return changes, nil
132
+ }
133
+ changed := e.changed
134
+ e.mu.Unlock()
135
+ select {
136
+ case <-changed:
137
+ case <-timedOut:
138
+ return nil, fail(errors.New("Timed out waiting for filesystem events"))
139
+ }
140
+ }
141
+ }
142
+
143
+ // Close ends the reader, settles pending assertions and reports a reader failure.
144
+ func (e *watchEvents) Close() error {
145
+ e.mu.Lock()
146
+ if !e.closed {
147
+ e.closed = true
148
+ close(e.stop)
149
+ e.notifyLocked()
150
+ }
151
+ e.mu.Unlock()
152
+ <-e.reading
153
+ e.mu.Lock()
154
+ defer e.mu.Unlock()
155
+ return e.err
156
+ }
157
+
158
+ // clientSource reads resourceWatch/changed batches for one channel from a testkit client.
159
+ type clientSource struct {
160
+ client *testkit.Client
161
+ channel string
162
+ }
163
+
164
+ func (s clientSource) Next(stop <-chan struct{}) ([]ahptypes.ResourceChange, error) {
165
+ for {
166
+ n, ok := s.client.Await(func(n testkit.Notification) bool {
167
+ if n.Method != "action" {
168
+ return false
169
+ }
170
+ var p struct {
171
+ Channel string `json:"channel"`
172
+ Action struct {
173
+ Type string `json:"type"`
174
+ } `json:"action"`
175
+ }
176
+ return json.Unmarshal(n.Params, &p) == nil && p.Channel == s.channel && p.Action.Type == string(ahptypes.ActionTypeResourceWatchChanged)
177
+ }, 20*time.Millisecond)
178
+ if ok {
179
+ var p struct {
180
+ Action struct {
181
+ Changes struct {
182
+ Items []ahptypes.ResourceChange `json:"items"`
183
+ } `json:"changes"`
184
+ } `json:"action"`
185
+ }
186
+ if err := json.Unmarshal(n.Params, &p); err != nil {
187
+ return nil, err
188
+ }
189
+ return p.Action.Changes.Items, nil
190
+ }
191
+ select {
192
+ case <-stop:
193
+ return nil, errStopped
194
+ default:
195
+ }
196
+ }
197
+ }
198
+
199
+ // ── a hand-driven clock ─────────────────────────────────────────────────
200
+
201
+ type fakeClock struct {
202
+ mu sync.Mutex
203
+ now time.Duration
204
+ timers []*fakeTimer
205
+ }
206
+
207
+ type fakeTimer struct {
208
+ c *fakeClock
209
+ at time.Duration
210
+ f func()
211
+ stopped bool
212
+ fired bool
213
+ }
214
+
215
+ func (c *fakeClock) AfterFunc(d time.Duration, f func()) Stopper {
216
+ c.mu.Lock()
217
+ defer c.mu.Unlock()
218
+ t := &fakeTimer{c: c, at: c.now + d, f: f}
219
+ c.timers = append(c.timers, t)
220
+ return t
221
+ }
222
+
223
+ func (t *fakeTimer) Stop() bool {
224
+ t.c.mu.Lock()
225
+ defer t.c.mu.Unlock()
226
+ was := !t.stopped && !t.fired
227
+ t.stopped = true
228
+ return was
229
+ }
230
+
231
+ // Tick advances the clock and runs every timer that came due, in order.
232
+ func (c *fakeClock) Tick(d time.Duration) {
233
+ c.mu.Lock()
234
+ target := c.now + d
235
+ c.mu.Unlock()
236
+ for {
237
+ c.mu.Lock()
238
+ var next *fakeTimer
239
+ for _, t := range c.timers {
240
+ if !t.stopped && !t.fired && t.at <= target && (next == nil || t.at < next.at) {
241
+ next = t
242
+ }
243
+ }
244
+ if next == nil {
245
+ c.now = target
246
+ c.mu.Unlock()
247
+ return
248
+ }
249
+ next.fired = true
250
+ if next.at > c.now {
251
+ c.now = next.at
252
+ }
253
+ c.mu.Unlock()
254
+ next.f()
255
+ }
256
+ }
257
+
258
+ // ── a scripted source ───────────────────────────────────────────────────
259
+
260
+ type scriptedSource struct {
261
+ mu sync.Mutex
262
+ reads int
263
+ pending chan scriptedResult
264
+ t *testing.T
265
+ }
266
+
267
+ type scriptedResult struct {
268
+ batch []ahptypes.ResourceChange
269
+ err error
270
+ }
271
+
272
+ func (s *scriptedSource) Next(stop <-chan struct{}) ([]ahptypes.ResourceChange, error) {
273
+ s.mu.Lock()
274
+ if s.pending != nil {
275
+ s.mu.Unlock()
276
+ s.t.Error("only one Next may be outstanding")
277
+ return nil, errStopped
278
+ }
279
+ s.reads++
280
+ ch := make(chan scriptedResult, 1)
281
+ s.pending = ch
282
+ s.mu.Unlock()
283
+ select {
284
+ case r := <-ch:
285
+ return r.batch, r.err
286
+ case <-stop:
287
+ s.mu.Lock()
288
+ if s.pending == ch {
289
+ s.pending = nil // detaching does not settle an outstanding read, like the SDK
290
+ }
291
+ s.mu.Unlock()
292
+ return nil, errStopped
293
+ }
294
+ }
295
+
296
+ func (s *scriptedSource) Reads() int { s.mu.Lock(); defer s.mu.Unlock(); return s.reads }
297
+
298
+ func (s *scriptedSource) resolve(r scriptedResult) {
299
+ // wait for the reader to park
300
+ for i := 0; i < 500; i++ {
301
+ s.mu.Lock()
302
+ ch := s.pending
303
+ s.pending = nil
304
+ s.mu.Unlock()
305
+ if ch != nil {
306
+ ch <- r
307
+ return
308
+ }
309
+ time.Sleep(2 * time.Millisecond)
310
+ }
311
+ s.t.Error("no read is outstanding")
312
+ }
313
+
314
+ func (s *scriptedSource) Push(items ...ahptypes.ResourceChange) {
315
+ s.resolve(scriptedResult{batch: items})
316
+ }
317
+ func (s *scriptedSource) Fail(err error) { s.resolve(scriptedResult{err: err}) }
318
+
319
+ func addedChange(name string) ahptypes.ResourceChange {
320
+ return ahptypes.ResourceChange{Uri: "file:///" + name, Type: ahptypes.ResourceChangeTypeAdded}
321
+ }
322
+
323
+ func hasURI(uri string) func([]ahptypes.ResourceChange) bool {
324
+ return func(items []ahptypes.ResourceChange) bool {
325
+ for _, i := range items {
326
+ if i.Uri == uri {
327
+ return true
328
+ }
329
+ }
330
+ return false
331
+ }
332
+ }
333
+
334
+ // Twins of upstream test/watch-events.test.ts: regression tests for the journal itself.
335
+
336
+ func TestWatchEventsJournal(t *testing.T) {
337
+ twin.Run(t, "watch-events", "watch assertions share a reader and retain the entire batch, even after a match", func(t *testing.T) {
338
+ input := &scriptedSource{t: t}
339
+ events := newWatchEvents(input, nil, nil)
340
+ defer events.Close()
341
+ type result struct {
342
+ changes []ahptypes.ResourceChange
343
+ err error
344
+ }
345
+ first, second := make(chan result, 1), make(chan result, 1)
346
+ go func() { c, err := events.WaitFor("a", hasURI("file:///a"), 0, 4*time.Second); first <- result{c, err} }()
347
+ go func() { c, err := events.WaitFor("b", hasURI("file:///b"), 0, 4*time.Second); second <- result{c, err} }()
348
+ testkit.Eventually(t, "the single reader parks", func() bool { return input.Reads() == 1 })
349
+ input.Push(addedChange("a"), addedChange("b"))
350
+ want := []ahptypes.ResourceChange{addedChange("a"), addedChange("b")}
351
+ for _, ch := range []chan result{first, second} {
352
+ r := <-ch
353
+ if r.err != nil || fmt.Sprint(r.changes) != fmt.Sprint(want) {
354
+ t.Fatalf("%+v", r)
355
+ }
356
+ }
357
+ if got := events.Batches(); len(got) != 1 || fmt.Sprint(got[0]) != fmt.Sprint(want) {
358
+ t.Fatalf("batches %v", got)
359
+ }
360
+ again, err := events.WaitFor("already received b", hasURI("file:///b"), 0, 4*time.Second)
361
+ if err != nil || fmt.Sprint(again) != fmt.Sprint(events.Changes()) {
362
+ t.Fatalf("%v %v", again, err)
363
+ }
364
+ })
365
+
366
+ twin.Run(t, "watch-events", "a timed-out assertion neither steals the next batch nor accepts events before its checkpoint", func(t *testing.T) {
367
+ clock := &fakeClock{}
368
+ input := &scriptedSource{t: t}
369
+ events := newWatchEvents(input, clock, func() any { return map[string]any{"exists": true} })
370
+ defer events.Close()
371
+ testkit.Eventually(t, "reader parks", func() bool { return input.Reads() == 1 })
372
+ input.Push(addedChange("a"))
373
+ if _, err := events.WaitFor("first a", func(items []ahptypes.ResourceChange) bool { return len(items) == 1 }, 0, time.Minute); err != nil {
374
+ t.Fatal(err)
375
+ }
376
+ since := events.Mark()
377
+ rejected := make(chan error, 1)
378
+ go func() {
379
+ _, err := events.WaitFor("second a", func(items []ahptypes.ResourceChange) bool { return len(items) > 0 }, since, 10*time.Millisecond)
380
+ rejected <- err
381
+ }()
382
+ // the assertion must have armed its timer before the clock moves
383
+ testkit.Eventually(t, "the assertion armed its timer", func() bool { clock.mu.Lock(); defer clock.mu.Unlock(); return len(clock.timers) >= 2 })
384
+ clock.Tick(10 * time.Millisecond)
385
+ err := <-rejected
386
+ if err == nil {
387
+ t.Fatal("the assertion must time out")
388
+ }
389
+ for _, want := range []string{"second a", `"exists":true`, "file:///a"} {
390
+ if !contains(err.Error(), want) {
391
+ t.Fatalf("error %q lacks %q", err, want)
392
+ }
393
+ }
394
+ if input.Reads() != 2 {
395
+ t.Fatalf("timeout must not start another reader, reads=%d", input.Reads())
396
+ }
397
+ next := make(chan []ahptypes.ResourceChange, 1)
398
+ go func() {
399
+ c, _ := events.WaitFor("second a", func(items []ahptypes.ResourceChange) bool { return len(items) > 0 }, since, time.Minute)
400
+ next <- c
401
+ }()
402
+ input.Push(addedChange("a"))
403
+ if got := <-next; fmt.Sprint(got) != fmt.Sprint([]ahptypes.ResourceChange{addedChange("a")}) {
404
+ t.Fatalf("%v", got)
405
+ }
406
+ if n := len(events.Batches()); n != 2 {
407
+ t.Fatalf("%d batches", n)
408
+ }
409
+ })
410
+
411
+ twin.Run(t, "watch-events", "reports a reader failure during cleanup even without a waiting assertion", func(t *testing.T) {
412
+ input := &scriptedSource{t: t}
413
+ events := newWatchEvents(input, nil, nil)
414
+ failure := errors.New("subscription read failed")
415
+ testkit.Eventually(t, "reader parks", func() bool { return input.Reads() == 1 })
416
+ input.Fail(failure)
417
+ time.Sleep(20 * time.Millisecond)
418
+ if err := events.Close(); err != failure {
419
+ t.Fatalf("got %v", err)
420
+ }
421
+ })
422
+
423
+ twin.Run(t, "watch-events", "closing the journal settles pending assertions and the sole subscription reader", func(t *testing.T) {
424
+ input := &scriptedSource{t: t}
425
+ events := newWatchEvents(input, nil, nil)
426
+ rejected := make(chan error, 1)
427
+ go func() {
428
+ _, err := events.WaitFor("never arrives", func(items []ahptypes.ResourceChange) bool { return len(items) > 0 }, 0, 4*time.Second)
429
+ rejected <- err
430
+ }()
431
+ testkit.Eventually(t, "reader parks", func() bool { return input.Reads() == 1 })
432
+ time.Sleep(10 * time.Millisecond)
433
+ if err := events.Close(); err != nil && !errors.Is(err, errStopped) {
434
+ t.Fatal(err)
435
+ }
436
+ if err := <-rejected; err == nil {
437
+ t.Fatal("a pending assertion must be rejected when the journal closes")
438
+ }
439
+ if input.Reads() != 1 {
440
+ t.Fatalf("reads=%d", input.Reads())
441
+ }
442
+ })
443
+ }
444
+
445
+ func contains(s, sub string) bool {
446
+ for i := 0; i+len(sub) <= len(s); i++ {
447
+ if s[i:i+len(sub)] == sub {
448
+ return true
449
+ }
450
+ }
451
+ return false
452
+ }
@@ -0,0 +1,81 @@
1
+ package svc
2
+
3
+ import (
4
+ "path/filepath"
5
+ "strings"
6
+
7
+ ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
8
+ )
9
+
10
+ // RelativeWatchPath is path relative to root in slash form: "" for the root itself, and ok=false
11
+ // for a sibling, a parent or a prefix lookalike.
12
+ func RelativeWatchPath(root, path string) (string, bool) {
13
+ abs, err := filepath.Abs(path)
14
+ if err != nil {
15
+ return "", false
16
+ }
17
+ rel, err := filepath.Rel(root, abs)
18
+ if err != nil || filepath.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
19
+ return "", false
20
+ }
21
+ if rel == "." {
22
+ return "", true
23
+ }
24
+ return filepath.ToSlash(rel), true
25
+ }
26
+
27
+ // IsExcluded reports whether any exclude pattern matches path. The root ("") is never excluded.
28
+ func IsExcluded(path string, excludes []string) bool {
29
+ if path == "" {
30
+ return false
31
+ }
32
+ for _, pattern := range excludes {
33
+ if matchesGlob(path, pattern) {
34
+ return true
35
+ }
36
+ }
37
+ return false
38
+ }
39
+
40
+ // MatchesPatterns applies include alternatives and exclude precedence; the root itself is always
41
+ // in scope, filters describe its descendants.
42
+ func MatchesPatterns(path string, includes, excludes []string) bool {
43
+ if path == "" {
44
+ return true
45
+ }
46
+ if IsExcluded(path, excludes) {
47
+ return false
48
+ }
49
+ if len(includes) == 0 {
50
+ return true
51
+ }
52
+ for _, pattern := range includes {
53
+ if matchesGlob(path, pattern) {
54
+ return true
55
+ }
56
+ }
57
+ return false
58
+ }
59
+
60
+ // MergeChange coalesces two consecutive changes to one path inside a batch. The empty string is
61
+ // "nothing pending" both as input and as the result (a transient path that came and went).
62
+ func MergeChange(previous, next ahptypes.ResourceChangeType) ahptypes.ResourceChangeType {
63
+ switch previous {
64
+ case "":
65
+ return next
66
+ case ahptypes.ResourceChangeTypeAdded:
67
+ if next == ahptypes.ResourceChangeTypeDeleted {
68
+ return ""
69
+ }
70
+ return ahptypes.ResourceChangeTypeAdded
71
+ case ahptypes.ResourceChangeTypeDeleted:
72
+ if next == ahptypes.ResourceChangeTypeDeleted {
73
+ return ahptypes.ResourceChangeTypeDeleted
74
+ }
75
+ return ahptypes.ResourceChangeTypeUpdated
76
+ }
77
+ if next == ahptypes.ResourceChangeTypeDeleted {
78
+ return ahptypes.ResourceChangeTypeDeleted
79
+ }
80
+ return ahptypes.ResourceChangeTypeUpdated
81
+ }
@@ -0,0 +1,151 @@
1
+ package svc
2
+
3
+ import (
4
+ "fmt"
5
+ "path/filepath"
6
+ "testing"
7
+
8
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
9
+ ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
10
+ )
11
+
12
+ // Twins of upstream test/resource-watch-policy.test.ts: deterministic, no OS events or clocks.
13
+
14
+ const (
15
+ cAdded = ahptypes.ResourceChangeTypeAdded
16
+ cUpdated = ahptypes.ResourceChangeTypeUpdated
17
+ cDeleted = ahptypes.ResourceChangeTypeDeleted
18
+ )
19
+
20
+ func TestWatchPathScopeAndFilters(t *testing.T) {
21
+ twin.Run(t, "resource-watch-policy", "normalizes descendants and rejects siblings and prefix lookalikes", func(t *testing.T) {
22
+ root, _ := filepath.Abs("workspace")
23
+ if rel, ok := RelativeWatchPath(root, root); !ok || rel != "" {
24
+ t.Fatalf("root: %q %v", rel, ok)
25
+ }
26
+ if rel, ok := RelativeWatchPath(root, filepath.Join(root, "nested/../file.txt")); !ok || rel != "file.txt" {
27
+ t.Fatalf("%q %v", rel, ok)
28
+ }
29
+ if rel, ok := RelativeWatchPath(root, filepath.Join(root, "nested/file.txt")); !ok || rel != "nested/file.txt" {
30
+ t.Fatalf("%q %v", rel, ok)
31
+ }
32
+ for _, path := range []string{filepath.Join(root, ".."), filepath.Join(root, "../sibling/file.txt"), root + "-other/file.txt"} {
33
+ if rel, ok := RelativeWatchPath(root, path); ok {
34
+ t.Errorf("%s must be out of scope, got %q", path, rel)
35
+ }
36
+ }
37
+ })
38
+
39
+ twin.Run(t, "resource-watch-policy", "keeps the watched root visible regardless of filters", func(t *testing.T) {
40
+ if IsExcluded("", []string{"**"}) {
41
+ t.Error("the root must never be excluded")
42
+ }
43
+ if !MatchesPatterns("", []string{"*.md"}, []string{"**"}) {
44
+ t.Error("the root must always match")
45
+ }
46
+ })
47
+
48
+ twin.Run(t, "resource-watch-policy", "applies include alternatives and exclude precedence to every path", func(t *testing.T) {
49
+ cases := []struct {
50
+ path string
51
+ includes, exclud []string
52
+ want bool
53
+ }{
54
+ {"file.txt", nil, nil, true},
55
+ {"nested/file.md", []string{"**/*.md"}, nil, true},
56
+ {"nested/file.txt", []string{"**/*.md"}, nil, false},
57
+ {"file.ts", []string{"**/*.md", "**/*.ts"}, nil, true},
58
+ {"nested/file.md", []string{"**/*.md"}, []string{"nested/**"}, false},
59
+ {"node_modules/pkg/index.js", nil, []string{"**/node_modules/**"}, false},
60
+ {"nested/node_modules/pkg/index.js", nil, []string{"**/node_modules/**"}, false},
61
+ {"node_modules", nil, []string{"**/node_modules/**"}, true},
62
+ {".git/config", nil, []string{"**/.git/**"}, false},
63
+ {"src/.hidden.ts", []string{"**/*.ts"}, nil, false},
64
+ {"src/.hidden.ts", []string{"**/.*.ts"}, nil, true},
65
+ }
66
+ for _, c := range cases {
67
+ if got := MatchesPatterns(c.path, c.includes, c.exclud); got != c.want {
68
+ t.Errorf("%s includes=%v excludes=%v: got %v, want %v", c.path, c.includes, c.exclud, got, c.want)
69
+ }
70
+ }
71
+ })
72
+
73
+ twin.Run(t, "resource-watch-policy", "does not prune a directory just because its children alone match the include filter", func(t *testing.T) {
74
+ if MatchesPatterns("src", []string{"**/*.ts"}, nil) {
75
+ t.Error("src alone does not match **/*.ts")
76
+ }
77
+ if IsExcluded("src", nil) {
78
+ t.Error("nothing is excluded without patterns")
79
+ }
80
+ if !MatchesPatterns("src/file.ts", []string{"**/*.ts"}, nil) {
81
+ t.Error("src/file.ts must match")
82
+ }
83
+ if !IsExcluded("src/generated", []string{"**/generated"}) {
84
+ t.Error("src/generated must be excluded")
85
+ }
86
+ })
87
+ }
88
+
89
+ func checkTransitions(t *testing.T, previous ahptypes.ResourceChangeType, expected [3]ahptypes.ResourceChangeType) {
90
+ t.Helper()
91
+ for i, change := range []ahptypes.ResourceChangeType{cAdded, cUpdated, cDeleted} {
92
+ if got := MergeChange(previous, change); got != expected[i] {
93
+ t.Errorf("%q then %s: got %q, want %q", previous, change, got, expected[i])
94
+ }
95
+ }
96
+ }
97
+
98
+ func TestWatchBatchTransitions(t *testing.T) {
99
+ twin.Run(t, "resource-watch-policy", "coalesces empty followed by each native event type", func(t *testing.T) {
100
+ checkTransitions(t, "", [3]ahptypes.ResourceChangeType{cAdded, cUpdated, cDeleted})
101
+ })
102
+ twin.Run(t, "resource-watch-policy", "coalesces added followed by each native event type", func(t *testing.T) {
103
+ checkTransitions(t, cAdded, [3]ahptypes.ResourceChangeType{cAdded, cAdded, ""})
104
+ })
105
+ twin.Run(t, "resource-watch-policy", "coalesces updated followed by each native event type", func(t *testing.T) {
106
+ checkTransitions(t, cUpdated, [3]ahptypes.ResourceChangeType{cUpdated, cUpdated, cDeleted})
107
+ })
108
+ twin.Run(t, "resource-watch-policy", "coalesces deleted followed by each native event type", func(t *testing.T) {
109
+ checkTransitions(t, cDeleted, [3]ahptypes.ResourceChangeType{cUpdated, cUpdated, cDeleted})
110
+ })
111
+
112
+ twin.Run(t, "resource-watch-policy", "handles replacement and transient paths across longer sequences", func(t *testing.T) {
113
+ cases := []struct {
114
+ sequence []ahptypes.ResourceChangeType
115
+ want ahptypes.ResourceChangeType
116
+ }{
117
+ {[]ahptypes.ResourceChangeType{cAdded, cUpdated, cDeleted}, ""},
118
+ {[]ahptypes.ResourceChangeType{cAdded, cDeleted, cAdded}, cAdded},
119
+ {[]ahptypes.ResourceChangeType{cDeleted, cAdded, cDeleted}, cDeleted},
120
+ {[]ahptypes.ResourceChangeType{cDeleted, cAdded, cDeleted, cAdded}, cUpdated},
121
+ {[]ahptypes.ResourceChangeType{cUpdated, cDeleted, cAdded}, cUpdated},
122
+ }
123
+ for _, c := range cases {
124
+ var got ahptypes.ResourceChangeType
125
+ for _, change := range c.sequence {
126
+ got = MergeChange(got, change)
127
+ }
128
+ if got != c.want {
129
+ t.Errorf("%s: got %q, want %q", fmt.Sprint(c.sequence), got, c.want)
130
+ }
131
+ }
132
+ })
133
+ }
134
+
135
+ func TestGlobMatcher(t *testing.T) {
136
+ // Additions: the matcher is ours (Node's path.matchesGlob is not in the standard library).
137
+ cases := []struct {
138
+ path, pattern string
139
+ want bool
140
+ }{
141
+ {"a/b.ts", "a/*.ts", true}, {"a/b/c.ts", "a/*.ts", false}, {"a", "a/**", false}, {"a/b", "a/**", true},
142
+ {"a/x/b", "a/**/b", true}, {"a/b", "a/**/b", true}, {"x.md", "*.{md,txt}", true}, {"x.go", "*.{md,txt}", false},
143
+ {"file1", "file[0-9]", true}, {"filex", "file[!0-9]", true}, {"file1", "file[!0-9]", false}, {"ab", "a?", true},
144
+ {".env", "*", false}, {".env", ".*", true}, {"a/.git/x", "a/**", false}, {"a[b", "a[b", true},
145
+ }
146
+ for _, c := range cases {
147
+ if got := matchesGlob(c.path, c.pattern); got != c.want {
148
+ t.Errorf("matchesGlob(%q, %q) = %v, want %v", c.path, c.pattern, got, c.want)
149
+ }
150
+ }
151
+ }