@termwright/probe-tview 0.2.0 → 0.3.1

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.
@@ -0,0 +1,483 @@
1
+ package tview
2
+
3
+ import (
4
+ "errors"
5
+ "reflect"
6
+ "runtime/debug"
7
+ "sync/atomic"
8
+ "testing"
9
+
10
+ "github.com/gdamore/tcell/v2"
11
+
12
+ "github.com/gorce-ai/termwright/clients/go/protocol"
13
+ )
14
+
15
+ type termwrightControlledPublicationSink struct {
16
+ ready chan struct{}
17
+ failures atomic.Int32
18
+ }
19
+
20
+ func (s *termwrightControlledPublicationSink) TryPublish(*protocol.Snapshot) (string, error) {
21
+ return "", protocol.ErrPublicationQueueFull
22
+ }
23
+
24
+ func (s *termwrightControlledPublicationSink) ReadyAfterDrop() <-chan struct{} { return s.ready }
25
+ func (s *termwrightControlledPublicationSink) Fail(string, string) { s.failures.Add(1) }
26
+
27
+ type termwrightTestScreen struct {
28
+ tcell.Screen
29
+ onShow func()
30
+ onFini func()
31
+ finalized atomic.Bool
32
+ }
33
+
34
+ func (s *termwrightTestScreen) Show() {
35
+ if s.onShow != nil {
36
+ s.onShow()
37
+ return
38
+ }
39
+ s.Screen.Show()
40
+ }
41
+
42
+ func (s *termwrightTestScreen) Fini() {
43
+ if !s.finalized.CompareAndSwap(false, true) {
44
+ return
45
+ }
46
+ if s.onFini != nil {
47
+ s.onFini()
48
+ }
49
+ s.Screen.Fini()
50
+ }
51
+
52
+ func termwrightNewTestScreen(t *testing.T) *termwrightTestScreen {
53
+ t.Helper()
54
+ simulation := tcell.NewSimulationScreen("UTF-8")
55
+ if err := simulation.Init(); err != nil {
56
+ t.Fatal(err)
57
+ }
58
+ screen := &termwrightTestScreen{Screen: simulation}
59
+ t.Cleanup(screen.Fini)
60
+ return screen
61
+ }
62
+
63
+ func termwrightDecorator(application *Application, screen tcell.Screen, commit func(Primitive, tcell.Screen)) *termwrightScreen {
64
+ decorated := &termwrightScreen{
65
+ Screen: screen, application: application, commit: commit,
66
+ }
67
+ decorated.previousBefore = application.beforeDraw
68
+ decorated.previousAfter = application.afterDraw
69
+ decorated.beforeHook = decorated.beforeDraw
70
+ decorated.afterHook = decorated.afterDraw
71
+ application.beforeDraw = decorated.beforeHook
72
+ application.afterDraw = decorated.afterHook
73
+ application.screen = decorated
74
+ return decorated
75
+ }
76
+
77
+ type termwrightIntermediateShowPrimitive struct{ *Box }
78
+
79
+ func (p *termwrightIntermediateShowPrimitive) Draw(screen tcell.Screen) {
80
+ x, y, _, _ := p.GetRect()
81
+ screen.SetContent(x, y, 'a', nil, tcell.StyleDefault)
82
+ screen.Show()
83
+ screen.SetContent(x+1, y, 'b', nil, tcell.StyleDefault)
84
+ }
85
+
86
+ func TestTermwrightScreenIgnoresIntermediateCustomPrimitiveShow(t *testing.T) {
87
+ application := NewApplication()
88
+ root := &termwrightIntermediateShowPrimitive{Box: NewBox()}
89
+ application.SetRoot(root, true)
90
+ underlying := termwrightNewTestScreen(t)
91
+ shows := 0
92
+ underlying.onShow = func() { shows++ }
93
+ commits := 0
94
+ termwrightDecorator(application, underlying, func(Primitive, tcell.Screen) { commits++ })
95
+
96
+ application.draw()
97
+ if shows != 2 || commits != 1 {
98
+ t.Fatalf("custom draw shows/commits = %d/%d, want 2/1", shows, commits)
99
+ }
100
+ }
101
+
102
+ func TestTermwrightScreenIgnoresShowsInsideLifecycleHooks(t *testing.T) {
103
+ for _, test := range []struct {
104
+ name string
105
+ set func(*Application)
106
+ }{
107
+ {name: "before-short-circuit", set: func(application *Application) {
108
+ application.SetBeforeDrawFunc(func(screen tcell.Screen) bool { screen.Show(); return true })
109
+ }},
110
+ {name: "after", set: func(application *Application) {
111
+ application.SetAfterDrawFunc(func(screen tcell.Screen) { screen.Show() })
112
+ }},
113
+ } {
114
+ t.Run(test.name, func(t *testing.T) {
115
+ application := NewApplication()
116
+ application.SetRoot(NewBox(), true)
117
+ test.set(application)
118
+ underlying := termwrightNewTestScreen(t)
119
+ shows := 0
120
+ underlying.onShow = func() { shows++ }
121
+ commits := 0
122
+ termwrightDecorator(application, underlying, func(Primitive, tcell.Screen) { commits++ })
123
+ application.draw()
124
+ if shows != 2 || commits != 1 {
125
+ t.Fatalf("hook shows/commits = %d/%d, want 2/1", shows, commits)
126
+ }
127
+ })
128
+ }
129
+ }
130
+
131
+ func TestTermwrightScreenFailsClosedWhenLifecycleHookIsDisplaced(t *testing.T) {
132
+ application := NewApplication()
133
+ application.SetRoot(NewBox(), true)
134
+ underlying := termwrightNewTestScreen(t)
135
+ commits := 0
136
+ decorated := termwrightDecorator(application, underlying, func(Primitive, tcell.Screen) { commits++ })
137
+ failures := 0
138
+ decorated.fail = func(code, message string) {
139
+ if code != "adapter-guarantee-violation" || message == "" {
140
+ t.Fatalf("unexpected failure %q: %q", code, message)
141
+ }
142
+ failures++
143
+ }
144
+ application.SetAfterDrawFunc(func(tcell.Screen) {})
145
+
146
+ application.draw()
147
+ if failures != 1 || commits != 0 || !decorated.failed.Load() {
148
+ t.Fatalf("displaced hook failures/commits/failed = %d/%d/%v, want 1/0/true", failures, commits, decorated.failed.Load())
149
+ }
150
+ }
151
+
152
+ func TestTermwrightDependencyVersionAcceptsWorkspaceAndLocalReplacement(t *testing.T) {
153
+ for _, test := range []struct {
154
+ module *debug.Module
155
+ want string
156
+ }{
157
+ {module: &debug.Module{Version: "v0.42.0", Replace: &debug.Module{Path: "/workspace/tview"}}, want: "v0.42.0"},
158
+ {module: &debug.Module{Version: "(devel)", Replace: &debug.Module{Path: "/workspace/tview"}}, want: "capability-local"},
159
+ {module: &debug.Module{Replace: &debug.Module{Path: "/workspace/tview"}}, want: "capability-local"},
160
+ } {
161
+ if got := termwrightDependencyVersion(test.module); got != test.want {
162
+ t.Fatalf("dependency version = %q, want %q", got, test.want)
163
+ }
164
+ }
165
+ }
166
+
167
+ func TestTermwrightScreenCommitsAfterTheOnlyUnderlyingShow(t *testing.T) {
168
+ application := NewApplication()
169
+ root := NewBox()
170
+ application.SetRoot(root, true)
171
+ events := make([]string, 0, 2)
172
+ underlying := termwrightNewTestScreen(t)
173
+ underlying.onShow = func() { events = append(events, "show") }
174
+ termwrightDecorator(application, underlying, func(got Primitive, screen tcell.Screen) {
175
+ if got != root || screen != underlying {
176
+ t.Fatalf("commit observed root/screen %T/%T", got, screen)
177
+ }
178
+ events = append(events, "commit-marker")
179
+ })
180
+
181
+ application.draw()
182
+ if want := []string{"show", "commit-marker"}; !reflect.DeepEqual(events, want) {
183
+ t.Fatalf("frame order = %v, want %v", events, want)
184
+ }
185
+ }
186
+
187
+ func TestTermwrightScreenObservesBeforeDrawShortCircuit(t *testing.T) {
188
+ application := NewApplication()
189
+ root := NewBox()
190
+ application.SetRoot(root, true)
191
+ events := make([]string, 0, 3)
192
+ application.SetBeforeDrawFunc(func(tcell.Screen) bool {
193
+ events = append(events, "before")
194
+ return true
195
+ })
196
+ underlying := termwrightNewTestScreen(t)
197
+ underlying.onShow = func() { events = append(events, "show") }
198
+ termwrightDecorator(application, underlying, func(got Primitive, _ tcell.Screen) {
199
+ if got != root {
200
+ t.Fatalf("commit root = %T, want initial root", got)
201
+ }
202
+ events = append(events, "commit-marker")
203
+ })
204
+
205
+ application.draw()
206
+ if want := []string{"before", "show", "commit-marker"}; !reflect.DeepEqual(events, want) {
207
+ t.Fatalf("short-circuit order = %v, want %v", events, want)
208
+ }
209
+ }
210
+
211
+ func TestTermwrightScreenReadsDynamicRootAfterEachShow(t *testing.T) {
212
+ application := NewApplication()
213
+ first := NewBox()
214
+ second := NewTextView().SetText("second")
215
+ application.SetRoot(first, true)
216
+ underlying := termwrightNewTestScreen(t)
217
+ roots := make([]Primitive, 0, 2)
218
+ termwrightDecorator(application, underlying, func(root Primitive, _ tcell.Screen) {
219
+ roots = append(roots, root)
220
+ })
221
+
222
+ application.draw()
223
+ application.SetRoot(second, true)
224
+ application.draw()
225
+ if want := []Primitive{first, second}; !reflect.DeepEqual(roots, want) {
226
+ t.Fatalf("committed roots = %v, want %v", roots, want)
227
+ }
228
+ }
229
+
230
+ func TestTermwrightScreensDoNotSerializeIndependentApplications(t *testing.T) {
231
+ firstApplication := NewApplication()
232
+ secondApplication := NewApplication()
233
+ firstApplication.root = NewBox()
234
+ secondApplication.root = NewBox()
235
+ firstUnderlying := termwrightNewTestScreen(t)
236
+ secondUnderlying := termwrightNewTestScreen(t)
237
+ entered := make(chan struct{})
238
+ release := make(chan struct{})
239
+ firstUnderlying.onShow = func() {
240
+ close(entered)
241
+ <-release
242
+ }
243
+ secondShown := false
244
+ secondUnderlying.onShow = func() { secondShown = true }
245
+ first := termwrightDecorator(firstApplication, firstUnderlying, nil)
246
+ second := termwrightDecorator(secondApplication, secondUnderlying, nil)
247
+ done := make(chan struct{})
248
+ go func() {
249
+ first.Show()
250
+ close(done)
251
+ }()
252
+ <-entered
253
+ second.Show()
254
+ if !secondShown {
255
+ t.Fatal("second application was serialized behind the first")
256
+ }
257
+ close(release)
258
+ <-done
259
+ }
260
+
261
+ func TestTermwrightIdleShowDoesNotReadHooksBeingConfigured(t *testing.T) {
262
+ application := NewApplication()
263
+ underlying := termwrightNewTestScreen(t)
264
+ underlying.onShow = func() {}
265
+ decorated := termwrightDecorator(application, underlying, nil)
266
+ done := make(chan struct{})
267
+ go func() {
268
+ defer close(done)
269
+ for index := 0; index < 1000; index++ {
270
+ application.SetAfterDrawFunc(func(tcell.Screen) {})
271
+ }
272
+ }()
273
+ for index := 0; index < 1000; index++ {
274
+ decorated.Show()
275
+ }
276
+ <-done
277
+ }
278
+
279
+ func TestTermwrightScreenFailsClosedOnReentrantShow(t *testing.T) {
280
+ application := NewApplication()
281
+ application.root = NewBox()
282
+ underlying := termwrightNewTestScreen(t)
283
+ var decorated *termwrightScreen
284
+ var failures atomic.Int32
285
+ commits := 0
286
+ decorated = termwrightDecorator(application, underlying, func(Primitive, tcell.Screen) {
287
+ commits++
288
+ })
289
+ decorated.fail = func(code, message string) {
290
+ if code != "adapter-guarantee-violation" || message == "" {
291
+ t.Fatalf("unexpected failure %q: %q", code, message)
292
+ }
293
+ failures.Add(1)
294
+ }
295
+ underlying.onShow = func() { decorated.Show() }
296
+
297
+ decorated.Show()
298
+ if failures.Load() != 1 || commits != 0 {
299
+ t.Fatalf("reentrant Show failures/commits = %d/%d, want 1/0", failures.Load(), commits)
300
+ }
301
+ }
302
+
303
+ func TestTermwrightScreenCleanupRestoresOnlyItsOwnInstallation(t *testing.T) {
304
+ t.Run("restores", func(t *testing.T) {
305
+ application := NewApplication()
306
+ underlying := termwrightNewTestScreen(t)
307
+ decorated := termwrightDecorator(application, underlying, nil)
308
+ decorated.detach()
309
+ if application.screen != underlying {
310
+ t.Fatalf("cleanup restored %T, want underlying screen", application.screen)
311
+ }
312
+ })
313
+ t.Run("later replacement wins", func(t *testing.T) {
314
+ application := NewApplication()
315
+ underlying := termwrightNewTestScreen(t)
316
+ replacement := termwrightNewTestScreen(t)
317
+ decorated := termwrightDecorator(application, underlying, nil)
318
+ application.screen = replacement
319
+ decorated.detach()
320
+ if application.screen != replacement {
321
+ t.Fatalf("cleanup clobbered later screen with %T", application.screen)
322
+ }
323
+ })
324
+ }
325
+
326
+ func TestTermwrightScreenFailsClosedBeforeRuntimeReplacement(t *testing.T) {
327
+ application := NewApplication()
328
+ underlying := termwrightNewTestScreen(t)
329
+ replacement := termwrightNewTestScreen(t)
330
+ finalized := 0
331
+ underlying.onFini = func() { finalized++ }
332
+ decorated := termwrightDecorator(application, underlying, nil)
333
+ failures := 0
334
+ decorated.fail = func(code, message string) {
335
+ if code != "adapter-guarantee-violation" || message == "" {
336
+ t.Fatalf("unexpected failure %q: %q", code, message)
337
+ }
338
+ failures++
339
+ }
340
+
341
+ application.SetScreen(replacement)
342
+ if failures != 1 || finalized != 1 || !decorated.failed.Load() || !decorated.detached.Load() {
343
+ t.Fatalf("replacement failures/finalized/failed/detached = %d/%d/%v/%v", failures, finalized, decorated.failed.Load(), decorated.detached.Load())
344
+ }
345
+ decorated.Fini()
346
+ if finalized != 1 {
347
+ t.Fatalf("underlying screen finalized %d times, want once", finalized)
348
+ }
349
+ }
350
+
351
+ func TestTermwrightScreenStopDoesNotReenterApplicationLock(t *testing.T) {
352
+ application := NewApplication()
353
+ underlying := termwrightNewTestScreen(t)
354
+ finalized := 0
355
+ underlying.onFini = func() { finalized++ }
356
+ decorated := termwrightDecorator(application, underlying, nil)
357
+ failures := 0
358
+ decorated.fail = func(string, string) { failures++ }
359
+
360
+ // This is the real upstream lifecycle: Stop holds Application.Lock while it
361
+ // invokes the decorated screen's Fini method.
362
+ application.Stop()
363
+ if finalized != 1 || failures != 0 || !decorated.failed.Load() || !decorated.detached.Load() {
364
+ t.Fatalf("stop finalized/failures/failed/detached = %d/%d/%v/%v", finalized, failures, decorated.failed.Load(), decorated.detached.Load())
365
+ }
366
+ decorated.detach()
367
+ if application.beforeDraw != nil || application.afterDraw != nil {
368
+ t.Fatal("cleanup after Stop left the probe lifecycle hooks installed")
369
+ }
370
+ }
371
+
372
+ func TestTermwrightScreenFiniUnderUnrelatedWriterFailsClosedWithoutBlocking(t *testing.T) {
373
+ application := NewApplication()
374
+ underlying := termwrightNewTestScreen(t)
375
+ commits := 0
376
+ decorated := termwrightDecorator(application, underlying, func(Primitive, tcell.Screen) {
377
+ commits++
378
+ })
379
+ failures := make(chan string, 1)
380
+ decorated.fail = func(code, message string) {
381
+ if code != "adapter-guarantee-violation" || message == "" {
382
+ t.Errorf("unexpected failure %q: %q", code, message)
383
+ }
384
+ failures <- message
385
+ }
386
+
387
+ application.Lock()
388
+ decorated.Fini()
389
+ application.Unlock()
390
+ <-failures
391
+ underlying.onShow = func() {}
392
+ decorated.Show()
393
+ if commits != 0 || !decorated.failed.Load() || !decorated.detached.Load() {
394
+ t.Fatalf("writer collision commits/failed/detached = %d/%v/%v", commits, decorated.failed.Load(), decorated.detached.Load())
395
+ }
396
+ decorated.detach()
397
+ application.Lock()
398
+ application.screen = nil
399
+ application.Unlock()
400
+ }
401
+
402
+ func TestTermwrightQueuePressureRequestsOneFreshAuthoritativeDraw(t *testing.T) {
403
+ application := NewApplication()
404
+ root := NewTextView().SetText("latest")
405
+ application.SetRoot(root, true)
406
+ underlying := termwrightNewTestScreen(t)
407
+ underlying.onShow = func() {}
408
+ commits := 0
409
+ termwrightDecorator(application, underlying, func(got Primitive, _ tcell.Screen) {
410
+ if got != root {
411
+ t.Fatalf("recovery committed %T, want latest root", got)
412
+ }
413
+ commits++
414
+ })
415
+ probe := &termwrightProbeState{
416
+ application: application,
417
+ recoveryStop: make(chan struct{}),
418
+ }
419
+ sink := &termwrightControlledPublicationSink{ready: make(chan struct{})}
420
+
421
+ probe.onPublishFailed(sink, protocol.ErrPublicationQueueFull)
422
+ probe.onPublishFailed(sink, protocol.ErrPublicationQueueFull)
423
+ if sink.failures.Load() != 0 || probe.dropped.Load() != 2 {
424
+ t.Fatalf("recoverable pressure failures/drops = %d/%d, want 0/2", sink.failures.Load(), probe.dropped.Load())
425
+ }
426
+ select {
427
+ case <-application.updates:
428
+ t.Fatal("redraw was queued before publication capacity returned")
429
+ default:
430
+ }
431
+
432
+ close(sink.ready)
433
+ update := <-application.updates
434
+ update.f()
435
+ if commits != 1 || probe.recoveryPending.Load() {
436
+ t.Fatalf("authoritative redraw commits/pending = %d/%v, want 1/false", commits, probe.recoveryPending.Load())
437
+ }
438
+ close(probe.recoveryStop)
439
+ probe.recoveryWorkers.Wait()
440
+ }
441
+
442
+ func TestTermwrightAdmissionContentionDefersOneFreshAuthoritativeDraw(t *testing.T) {
443
+ application := NewApplication()
444
+ root := NewTextView().SetText("after-contention")
445
+ application.SetRoot(root, true)
446
+ underlying := termwrightNewTestScreen(t)
447
+ underlying.onShow = func() {}
448
+ commits := 0
449
+ termwrightDecorator(application, underlying, func(got Primitive, _ tcell.Screen) {
450
+ if got != root {
451
+ t.Fatalf("deferred draw committed %T, want latest root", got)
452
+ }
453
+ commits++
454
+ })
455
+ probe := &termwrightProbeState{
456
+ application: application,
457
+ recoveryStop: make(chan struct{}),
458
+ }
459
+ sink := &termwrightControlledPublicationSink{ready: make(chan struct{})}
460
+
461
+ probe.onPublishFailed(sink, protocol.ErrPublicationQueueBusy)
462
+ probe.onPublishFailed(sink, protocol.ErrPublicationQueueBusy)
463
+ update := <-application.updates
464
+ update.f()
465
+ if sink.failures.Load() != 0 || probe.dropped.Load() != 2 || commits != 1 || probe.recoveryPending.Load() {
466
+ t.Fatalf(
467
+ "contention failures/drops/commits/pending = %d/%d/%d/%v, want 0/2/1/false",
468
+ sink.failures.Load(), probe.dropped.Load(), commits, probe.recoveryPending.Load(),
469
+ )
470
+ }
471
+ close(probe.recoveryStop)
472
+ probe.recoveryWorkers.Wait()
473
+ }
474
+
475
+ func TestTermwrightNonPressurePublicationRefusalStillFailsClosed(t *testing.T) {
476
+ probe := &termwrightProbeState{recoveryStop: make(chan struct{})}
477
+ sink := &termwrightControlledPublicationSink{ready: make(chan struct{})}
478
+ probe.onPublishFailed(sink, errors.New("invalid snapshot"))
479
+ if sink.failures.Load() != 1 || probe.dropped.Load() != 1 {
480
+ t.Fatalf("hard refusal failures/drops = %d/%d, want 1/1", sink.failures.Load(), probe.dropped.Load())
481
+ }
482
+ close(probe.recoveryStop)
483
+ }
package/dist/index.d.ts CHANGED
@@ -39,49 +39,33 @@ declare function roleFor(frameworkType: string): SemanticRole;
39
39
  */
40
40
  declare function recognize(frame: ProbeFrame, options: RecognizeOptions): SemanticSnapshot;
41
41
 
42
- /**
43
- * The one call a launcher makes.
44
- *
45
- * Everything else in this package is a step: read the workspace, materialise a
46
- * copy, patch it, key the cache, write the file. A user should not assemble
47
- * those, and neither should a test — the assembly order is where the mistakes
48
- * live, and there is exactly one correct one.
49
- */
50
- /** Module path of the framework this probe instruments. */
42
+ /** Capability-driven, add-only compiler injection for tview. */
51
43
  declare const FRAMEWORK = "github.com/rivo/tview";
52
- /** Module path of the protocol client the injected probe imports. */
53
44
  declare const CLIENT_MODULE = "github.com/gorce-ai/termwright/clients/go";
54
- /** Version of this probe; part of the cache key, so a new patch set invalidates copies. */
55
- declare const PROBE_VERSION = "0.1.0";
45
+ declare const PROBE_VERSION = "0.3.1";
56
46
  interface PrepareOptions {
57
- /** Directory of the Go module to build. */
58
47
  readonly moduleDir: string;
59
- /** Framework version to instrument, e.g. `v0.42.0`. */
48
+ /** Advisory expectation; runtime capability compilation is authoritative. */
60
49
  readonly frameworkVersion?: string;
61
- /** Where the protocol client lives on disk. Defaults to the copy shipped here. */
62
- readonly clientDir?: string;
63
- /** Where the generated workspace is written. Defaults to inside the copy's cache entry. */
64
- readonly workspaceFile?: string;
65
- /** Environment the build will run with; checked, never mutated. */
50
+ readonly outputDir?: string;
66
51
  readonly env?: NodeJS.ProcessEnv;
67
52
  }
68
53
  interface PreparedBuild {
69
- /** Hand this to the build as `GOWORK`. */
70
- readonly workspaceFile: string;
71
- /** The instrumented copy, for a canary check or for diagnosis. */
72
- readonly copyDir: string;
73
- /** Environment to build with: the caller's, plus GOWORK. */
54
+ /** Canonical module directory that must be used as the build cwd. */
55
+ readonly moduleDir: string;
56
+ /** Insert after `go build` or `go test`. */
57
+ readonly goArgs: readonly [string, string];
74
58
  readonly env: NodeJS.ProcessEnv;
75
- /** True when the copy was built during this call rather than reused. */
76
- readonly built: boolean;
59
+ readonly wrapperFile: string;
60
+ readonly configDigest: string;
61
+ readonly frameworkVersion: string;
62
+ readonly sourceDigests: readonly string[];
77
63
  }
78
64
  /**
79
- * Makes a build of `moduleDir` compile against the instrumented framework.
80
- *
81
- * Does not build anything and does not spawn the application: a launcher owns
82
- * that. This returns what the build needs and nothing else, which keeps the
83
- * package testable and keeps the decision about *how* to run the user's build
84
- * where it belongs.
65
+ * Prepares one official Go `-toolexec` wrapper. It adds compilation units to
66
+ * package namespaces selected by TOOLEXEC_IMPORTPATH and therefore works for
67
+ * module-cache, workspace, replacement, and vendored dependency layouts.
68
+ * No upstream path is copied, patched, or hashed.
85
69
  */
86
70
  declare function prepareInstrumentedBuild(options: PrepareOptions): Promise<PreparedBuild>;
87
71