ttsc 0.23.0 → 0.24.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 (46) hide show
  1. package/cmd/ttscgraph/serve.go +264 -48
  2. package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
  3. package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
  4. package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
  5. package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
  6. package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
  7. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
  8. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
  9. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
  10. package/cmd/ttscgraph/serve_shards.go +1390 -0
  11. package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
  12. package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
  13. package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
  14. package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
  15. package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
  16. package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
  17. package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
  18. package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
  19. package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
  20. package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
  21. package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
  22. package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
  23. package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
  24. package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
  25. package/driver/declaration_shape.go +85 -0
  26. package/driver/program.go +39 -10
  27. package/internal/graph/build.go +62 -10
  28. package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
  29. package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
  30. package/internal/graph/dump.go +84 -43
  31. package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
  32. package/internal/graph/dump_paths.go +134 -11
  33. package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
  34. package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
  35. package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
  36. package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
  37. package/internal/graph/edges.go +58 -16
  38. package/internal/graph/exports.go +9 -2
  39. package/internal/graph/graph.go +47 -7
  40. package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
  41. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  42. package/internal/graph/provenance.go +11 -1
  43. package/internal/graph/resolve.go +1 -4
  44. package/internal/graph/source.go +23 -0
  45. package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
  46. package/package.json +8 -8
@@ -13,6 +13,7 @@ import (
13
13
  "slices"
14
14
  "sort"
15
15
  "strings"
16
+ "time"
16
17
 
17
18
  shimtsoptions "github.com/microsoft/typescript-go/shim/tsoptions"
18
19
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"
@@ -45,7 +46,9 @@ const (
45
46
  serveModeUnchanged = "unchanged"
46
47
  // serveModeIncremental is edits applied onto the reused resident program.
47
48
  serveModeIncremental = "incremental"
48
- // serveModeRebuild is edits applied but the program could not be reused.
49
+ // serveModeRebuild is edits applied but graph projection requires a complete
50
+ // rebuild, either because the program could not be reused or because a
51
+ // declaration-file boundary has no authored source shard of its own.
49
52
  serveModeRebuild = "rebuild"
50
53
  // serveModeError is a request that produced no snapshot. It is a transport
51
54
  // mode, not a computation mode: it exists so mode is never absent, because a
@@ -71,10 +74,15 @@ var serveCapabilities = fullSnapshotCapabilities
71
74
 
72
75
  type serveRequest struct {
73
76
  ID int `json:"id"`
77
+ // GraphSnapshotVersion opts into the incremental shard protocol. Omitted
78
+ // requests retain the schema-v6 full-dump response for existing
79
+ // @ttsc/graph clients.
80
+ GraphSnapshotVersion int `json:"graphSnapshotVersion,omitempty"`
74
81
  }
75
82
 
76
83
  type serveResponse struct {
77
- Dump *graph.Dump `json:"dump,omitempty"`
84
+ Dump *graph.Dump `json:"dump,omitempty"`
85
+ Snapshot *serveGraphSnapshot `json:"snapshot,omitempty"`
78
86
  // Error is set when the request produced no snapshot; Mode is then
79
87
  // serveModeError.
80
88
  Error string `json:"error,omitempty"`
@@ -90,6 +98,8 @@ type serveResponse struct {
90
98
  Changed bool `json:"changed"`
91
99
  }
92
100
 
101
+ const graphPhaseTraceEnvironment = "SAMCHON_GRAPH_TTSC_PHASE_TRACE"
102
+
93
103
  // newServeResponse stamps the fields every response owes the client, so no exit
94
104
  // from the serve loop can forget one.
95
105
  func newServeResponse(id int) serveResponse {
@@ -124,13 +134,17 @@ type graphSession struct {
124
134
  // generation, captured from the same parse that produced configHashes and
125
135
  // rootFiles so the published evidence and the invalidation state can never
126
136
  // describe different loads.
127
- configDigests []graph.FileDigest
128
- roots []graph.RootFile
129
- initialized bool
130
- // pendingDumpMode remembers a generation whose state was captured but whose
131
- // dump failed. Until an input change lets the session rebuild, an unchanged
132
- // request must retry that dump instead of falsely confirming the older graph.
133
- pendingDumpMode string
137
+ configDigests []graph.FileDigest
138
+ roots []graph.RootFile
139
+ initialized bool
140
+ graphStore *serveGraphStore
141
+ requestProtocol int
142
+ requestProtocolSelected bool
143
+ // pending remembers a generation whose state was captured but whose selected
144
+ // projection failed. Until an input change lets the session rebuild, an
145
+ // unchanged request retries the same full or partial invalidation instead of
146
+ // falsely confirming the older client graph.
147
+ pending *graphChange
134
148
  }
135
149
 
136
150
  func newGraphSession(cwd, tsconfig string) (*graphSession, error) {
@@ -149,106 +163,187 @@ func (s *graphSession) Close() error {
149
163
  }
150
164
 
151
165
  func (s *graphSession) Snapshot() (*graph.Dump, string, bool, error) {
166
+ change, err := s.nextChange(false)
167
+ if err != nil {
168
+ return nil, "", false, err
169
+ }
170
+ if change == nil {
171
+ return nil, serveModeUnchanged, false, nil
172
+ }
173
+ dump, err := s.buildDump()
174
+ if err != nil {
175
+ s.pending = change
176
+ return nil, "", false, err
177
+ }
178
+ s.pending = nil
179
+ return &dump, change.mode, true, nil
180
+ }
181
+
182
+ type graphChange struct {
183
+ mode string
184
+ files []string
185
+ publicFiles []string
186
+ full bool
187
+ }
188
+
189
+ // nextChange advances only the resident compiler and its captured input state.
190
+ // Projection is deliberately outside this method so the legacy full-dump and
191
+ // incremental shard protocols share exactly one invalidation decision without
192
+ // forcing either representation onto the other.
193
+ func (s *graphSession) nextChange(trackPublicShape bool) (*graphChange, error) {
152
194
  if !s.initialized {
153
195
  // The captured compiler state is initialized even when its first dump is
154
196
  // not publishable. Mark the attempt before building so a later request can
155
197
  // observe a config/source edit that repairs the path error instead of
156
198
  // retrying the stale Program forever.
157
199
  s.initialized = true
158
- dump, err := s.buildDump()
159
- if err != nil {
160
- s.pendingDumpMode = serveModeInitial
161
- return nil, "", false, err
162
- }
163
- s.pendingDumpMode = ""
164
- return &dump, serveModeInitial, true, nil
200
+ return &graphChange{mode: serveModeInitial, full: true}, nil
165
201
  }
166
202
 
167
203
  configChanged, err := hashesChanged(s.configHashes)
168
204
  if err != nil {
169
- return nil, "", false, err
205
+ return nil, err
170
206
  }
171
207
  if configChanged {
172
208
  if err := s.reload(); err != nil {
173
- return nil, "", false, err
209
+ return nil, err
174
210
  }
175
- return s.changedSnapshot(serveModeReload)
211
+ return &graphChange{mode: serveModeReload, full: true}, nil
176
212
  }
177
213
 
178
214
  if diskStatesChanged(s.auxStates) {
179
215
  if err := s.reload(); err != nil {
180
- return nil, "", false, err
216
+ return nil, err
181
217
  }
182
- return s.changedSnapshot(serveModeReload)
218
+ return &graphChange{mode: serveModeReload, full: true}, nil
183
219
  }
184
220
 
185
221
  roots, err := projectRootFiles(s.compiler.Program(), true)
186
222
  if err != nil {
187
- return nil, "", false, err
223
+ return nil, err
188
224
  }
189
225
  if !slices.Equal(s.rootFiles, roots) {
190
226
  if err := s.reload(); err != nil {
191
- return nil, "", false, err
227
+ return nil, err
192
228
  }
193
- return s.changedSnapshot(serveModeReload)
229
+ return &graphChange{mode: serveModeReload, full: true}, nil
194
230
  }
195
231
 
196
232
  changed, deleted, err := changedSources(s.sourceHashes)
197
233
  if err != nil {
198
- return nil, "", false, err
234
+ return nil, err
199
235
  }
200
236
  if deleted {
201
237
  if err := s.reload(); err != nil {
202
- return nil, "", false, err
238
+ return nil, err
203
239
  }
204
- return s.changedSnapshot(serveModeReload)
240
+ return &graphChange{mode: serveModeReload, full: true}, nil
205
241
  }
206
242
  if len(changed) == 0 {
207
- if s.pendingDumpMode != "" {
208
- return s.changedSnapshot(s.pendingDumpMode)
243
+ if s.pending != nil {
244
+ return s.pending, nil
209
245
  }
210
- return nil, serveModeUnchanged, false, nil
246
+ return nil, nil
211
247
  }
212
248
  if s.compiler.Program().HasLinkedProgramPlugins() {
213
249
  if err := s.reload(); err != nil {
214
- return nil, "", false, err
250
+ return nil, err
215
251
  }
216
- return s.changedSnapshot(serveModeReload)
252
+ return &graphChange{mode: serveModeReload, full: true}, nil
217
253
  }
218
254
 
219
255
  mode := serveModeIncremental
256
+ full := false
220
257
  paths := make([]string, 0, len(changed))
221
258
  for path := range changed {
222
259
  paths = append(paths, path)
223
260
  }
224
261
  sort.Strings(paths)
262
+ oldShapes := make(map[string]string, len(paths))
263
+ for _, path := range paths {
264
+ source := s.compiler.Program().SourceFile(path)
265
+ if source == nil {
266
+ if err := s.reload(); err != nil {
267
+ return nil, err
268
+ }
269
+ return &graphChange{mode: serveModeReload, full: true}, nil
270
+ }
271
+ if source.IsDeclarationFile {
272
+ mode = serveModeRebuild
273
+ full = true
274
+ continue
275
+ }
276
+ if trackPublicShape {
277
+ shape, err := s.compiler.Program().DeclarationShapeDigest(source)
278
+ if err != nil {
279
+ return nil, err
280
+ }
281
+ oldShapes[path] = shape
282
+ }
283
+ }
225
284
  for _, path := range paths {
226
285
  if reused := s.compiler.Apply(path, changed[path]); !reused {
227
286
  mode = serveModeRebuild
287
+ full = true
228
288
  }
229
289
  current, exists := s.compiler.SourceText(path)
230
290
  expected := driver.ApplySourcePreambleToFile(path, changed[path], s.compiler.Program().SourcePreamble)
231
291
  if !exists || current != expected {
232
292
  if err := s.reload(); err != nil {
233
- return nil, "", false, err
293
+ return nil, err
234
294
  }
235
- return s.changedSnapshot(serveModeReload)
295
+ return &graphChange{mode: serveModeReload, full: true}, nil
296
+ }
297
+ source := s.compiler.Program().SourceFile(path)
298
+ if source == nil {
299
+ if err := s.reload(); err != nil {
300
+ return nil, err
301
+ }
302
+ return &graphChange{mode: serveModeReload, full: true}, nil
303
+ }
304
+ if source.IsDeclarationFile {
305
+ mode = serveModeRebuild
306
+ full = true
236
307
  }
237
308
  }
238
309
  if err := s.captureState(); err != nil {
239
- return nil, "", false, err
310
+ // The compiler already accepted the edit. Preserve a full retry marker so
311
+ // a transient hashing/input failure cannot make the next request compare
312
+ // public shape against the uncommitted compiler generation and publish an
313
+ // incomplete closure.
314
+ s.pending = &graphChange{mode: serveModeRebuild, full: true}
315
+ return nil, err
240
316
  }
241
- return s.changedSnapshot(mode)
242
- }
243
-
244
- func (s *graphSession) changedSnapshot(mode string) (*graph.Dump, string, bool, error) {
245
- dump, err := s.buildDump()
246
- if err != nil {
247
- s.pendingDumpMode = mode
248
- return nil, "", false, err
317
+ publicFiles := []string{}
318
+ if trackPublicShape && !full {
319
+ for _, path := range paths {
320
+ source := s.compiler.Program().SourceFile(path)
321
+ if source == nil {
322
+ return &graphChange{mode: serveModeRebuild, full: true}, nil
323
+ }
324
+ shape, err := s.compiler.Program().DeclarationShapeDigest(source)
325
+ if err != nil {
326
+ mode = serveModeRebuild
327
+ full = true
328
+ publicFiles = nil
329
+ break
330
+ }
331
+ if shape != oldShapes[path] {
332
+ publicFiles = append(publicFiles, path)
333
+ }
334
+ }
335
+ }
336
+ if s.pending != nil {
337
+ if s.pending.full {
338
+ mode = s.pending.mode
339
+ full = true
340
+ paths = nil
341
+ } else if !full {
342
+ paths = compactSortedStrings(append(paths, s.pending.files...))
343
+ publicFiles = compactSortedStrings(append(publicFiles, s.pending.publicFiles...))
344
+ }
249
345
  }
250
- s.pendingDumpMode = ""
251
- return &dump, mode, true, nil
346
+ return &graphChange{mode: mode, files: paths, publicFiles: publicFiles, full: full}, nil
252
347
  }
253
348
 
254
349
  func (s *graphSession) reload() error {
@@ -750,26 +845,86 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
750
845
  fmt.Fprintf(stderr, "ttscgraph: unaddressable serve request: %v\n", err)
751
846
  return 1
752
847
  }
848
+ requestStarted := time.Now()
849
+ if request.GraphSnapshotVersion != 0 && request.GraphSnapshotVersion != graphSnapshotProtocolVersion {
850
+ if err := encodeServeResponseWithTrace(encoder, errorResponse(
851
+ request.ID,
852
+ fmt.Sprintf(
853
+ "ttscgraph: unsupported graph snapshot protocol v%d (supported: v%d)",
854
+ request.GraphSnapshotVersion,
855
+ graphSnapshotProtocolVersion,
856
+ ),
857
+ ), requestStarted, 0, 0, 0); err != nil {
858
+ fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
859
+ return 1
860
+ }
861
+ continue
862
+ }
863
+ var loadDuration time.Duration
753
864
  if session == nil {
865
+ loadStarted := time.Now()
754
866
  created, err := newGraphSession(cwd, tsconfig)
867
+ loadDuration = time.Since(loadStarted)
755
868
  if err != nil {
756
- _ = encoder.Encode(errorResponse(request.ID, err.Error()))
869
+ if err := encodeServeResponseWithTrace(
870
+ encoder,
871
+ errorResponse(request.ID, err.Error()),
872
+ requestStarted,
873
+ loadDuration,
874
+ 0,
875
+ 0,
876
+ ); err != nil {
877
+ fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
878
+ return 1
879
+ }
757
880
  continue
758
881
  }
759
882
  session = created
760
883
  }
761
- dump, mode, changed, err := session.Snapshot()
884
+ if session.requestProtocolSelected && session.requestProtocol != request.GraphSnapshotVersion {
885
+ if err := encodeServeResponseWithTrace(encoder, errorResponse(
886
+ request.ID,
887
+ "ttscgraph: graphSnapshotVersion cannot change within one resident session",
888
+ ), requestStarted, loadDuration, 0, 0); err != nil {
889
+ fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
890
+ return 1
891
+ }
892
+ continue
893
+ }
894
+ session.requestProtocol = request.GraphSnapshotVersion
895
+ session.requestProtocolSelected = true
896
+ var dump *graph.Dump
897
+ var snapshot *serveGraphSnapshot
898
+ var mode string
899
+ var changed bool
900
+ var semanticDuration time.Duration
901
+ var exportDuration time.Duration
902
+ var err error
903
+ if request.GraphSnapshotVersion == graphSnapshotProtocolVersion {
904
+ snapshot, mode, changed, semanticDuration, exportDuration, err = session.snapshotShardsWithTiming()
905
+ } else {
906
+ dump, mode, changed, err = session.Snapshot()
907
+ }
762
908
  response := newServeResponse(request.ID)
763
909
  response.Dump = dump
910
+ response.Snapshot = snapshot
764
911
  response.Mode = mode
765
912
  response.Changed = changed
766
913
  if err != nil {
767
914
  response.Error = err.Error()
768
915
  response.Dump = nil
916
+ response.Snapshot = nil
769
917
  response.Mode = serveModeError
770
918
  response.Changed = false
771
919
  }
772
- if err := encoder.Encode(response); err != nil {
920
+ if err := encodeServeResponseWithTrace(
921
+ encoder,
922
+ response,
923
+ requestStarted,
924
+ loadDuration,
925
+ semanticDuration,
926
+ exportDuration,
927
+ ); err != nil {
773
928
  fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
774
929
  return 1
775
930
  }
@@ -780,3 +935,64 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
780
935
  }
781
936
  return 0
782
937
  }
938
+
939
+ // encodeServeResponseWithTrace accounts for every addressed response, including
940
+ // failures that occur before a graph session exists.
941
+ func encodeServeResponseWithTrace(
942
+ encoder *json.Encoder,
943
+ response serveResponse,
944
+ requestStarted time.Time,
945
+ loadDuration time.Duration,
946
+ semanticDuration time.Duration,
947
+ exportDuration time.Duration,
948
+ ) error {
949
+ encodeStarted := time.Now()
950
+ if err := encoder.Encode(response); err != nil {
951
+ return err
952
+ }
953
+ writeServePhaseTrace(
954
+ response.ID,
955
+ response.Mode,
956
+ loadDuration,
957
+ semanticDuration,
958
+ exportDuration,
959
+ time.Since(encodeStarted),
960
+ time.Since(requestStarted),
961
+ )
962
+ return nil
963
+ }
964
+
965
+ // writeServePhaseTrace emits opt-in timings without source paths or payloads.
966
+ func writeServePhaseTrace(
967
+ request int,
968
+ mode string,
969
+ load time.Duration,
970
+ semantic time.Duration,
971
+ export time.Duration,
972
+ encode time.Duration,
973
+ total time.Duration,
974
+ ) {
975
+ if os.Getenv(graphPhaseTraceEnvironment) != "1" {
976
+ return
977
+ }
978
+ phases := []struct {
979
+ name string
980
+ duration time.Duration
981
+ }{
982
+ {name: "native-load", duration: load},
983
+ {name: "semantic-refresh", duration: semantic},
984
+ {name: "shard-export", duration: export},
985
+ {name: "encode", duration: encode},
986
+ {name: "producer-total", duration: total},
987
+ }
988
+ for _, phase := range phases {
989
+ fmt.Fprintf(
990
+ stderr,
991
+ "@samchon/graph: ttscgraph-phase owner=producer request=%d mode=%s phase=%s durationMs=%.3f\n",
992
+ request,
993
+ mode,
994
+ phase.name,
995
+ float64(phase.duration)/float64(time.Millisecond),
996
+ )
997
+ }
998
+ }
@@ -0,0 +1,40 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "errors"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ type rejectedServeWriter struct{}
11
+
12
+ func (rejectedServeWriter) Write([]byte) (int, error) {
13
+ return 0, errors.New("synthetic response write failure")
14
+ }
15
+
16
+ // TestServeEarlyErrorEncodeFailureIsObservable proves a rejected early error
17
+ // response fails the stream without claiming that an unencoded response ran.
18
+ //
19
+ // 1. Reject protocol negotiation before a graph session is constructed.
20
+ // 2. Fail the response writer and require a nonzero server result.
21
+ // 3. Require the stderr cause while forbidding successful phase rows.
22
+ func TestServeEarlyErrorEncodeFailureIsObservable(t *testing.T) {
23
+ root := graphSessionFixture(t)
24
+ oldStderr := stderr
25
+ defer func() { stderr = oldStderr }()
26
+ t.Setenv(graphPhaseTraceEnvironment, "1")
27
+
28
+ var diagnostic bytes.Buffer
29
+ stderr = &diagnostic
30
+ request := "{\"id\":29,\"graphSnapshotVersion\":99}\n"
31
+ if code := serveSnapshots(strings.NewReader(request), rejectedServeWriter{}, root, "tsconfig.json"); code == 0 {
32
+ t.Fatal("failed early error encoding returned success")
33
+ }
34
+ if !strings.Contains(diagnostic.String(), "write serve response: synthetic response write failure") {
35
+ t.Fatalf("failed early error encoding hid its cause: %q", diagnostic.String())
36
+ }
37
+ if strings.Contains(diagnostic.String(), "ttscgraph-phase") {
38
+ t.Fatalf("failed early error encoding claimed successful phase rows: %q", diagnostic.String())
39
+ }
40
+ }
@@ -0,0 +1,32 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "regexp"
7
+ "strconv"
8
+ "testing"
9
+ )
10
+
11
+ // TestServeGraphSnapshotVersionMatchesTypeScriptClient verifies the native
12
+ // opt-in number and the resident client move together. The outer serve protocol
13
+ // stays backward compatible, so only this explicit negotiation field can stop
14
+ // an old full-dump client from misreading a shard response.
15
+ func TestServeGraphSnapshotVersionMatchesTypeScriptClient(t *testing.T) {
16
+ source := filepath.Join("..", "..", "..", "graph", "src", "model", "TtscGraphSession.ts")
17
+ content, err := os.ReadFile(source)
18
+ if err != nil {
19
+ t.Fatal(err)
20
+ }
21
+ match := regexp.MustCompile(`const GRAPH_SNAPSHOT_PROTOCOL_VERSION = ([0-9]+);`).FindSubmatch(content)
22
+ if len(match) != 2 {
23
+ t.Fatalf("could not find GRAPH_SNAPSHOT_PROTOCOL_VERSION in %s", source)
24
+ }
25
+ version, err := strconv.Atoi(string(match[1]))
26
+ if err != nil {
27
+ t.Fatal(err)
28
+ }
29
+ if version != graphSnapshotProtocolVersion {
30
+ t.Fatalf("TypeScript client graph snapshot protocol v%d, native server v%d", version, graphSnapshotProtocolVersion)
31
+ }
32
+ }
@@ -0,0 +1,45 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ // TestServePhaseTraceAccountsForLoadFailure proves addressed requests retain a
10
+ // complete timing record even when no resident compiler session can be built.
11
+ //
12
+ // 1. Request a missing tsconfig with phase tracing enabled.
13
+ // 2. Require a normal addressed error response rather than process failure.
14
+ // 3. Require all five payload-free phases under mode=error.
15
+ func TestServePhaseTraceAccountsForLoadFailure(t *testing.T) {
16
+ root := graphSessionFixture(t)
17
+ request := "{\"id\":23,\"graphSnapshotVersion\":1}\n"
18
+ oldStderr := stderr
19
+ defer func() { stderr = oldStderr }()
20
+ t.Setenv(graphPhaseTraceEnvironment, "1")
21
+
22
+ var output bytes.Buffer
23
+ var trace bytes.Buffer
24
+ stderr = &trace
25
+ if code := serveSnapshots(strings.NewReader(request), &output, root, "missing-tsconfig.json"); code != 0 {
26
+ t.Fatalf("failed load exited %d", code)
27
+ }
28
+ if !strings.Contains(output.String(), `"id":23`) || !strings.Contains(output.String(), `"mode":"error"`) {
29
+ t.Fatalf("failed load omitted its addressed error response: %q", output.String())
30
+ }
31
+ for _, phase := range []string{
32
+ "native-load",
33
+ "semantic-refresh",
34
+ "shard-export",
35
+ "encode",
36
+ "producer-total",
37
+ } {
38
+ if !strings.Contains(trace.String(), "owner=producer request=23 mode=error phase="+phase+" durationMs=") {
39
+ t.Fatalf("failed-load trace omitted %s: %q", phase, trace.String())
40
+ }
41
+ }
42
+ if strings.Contains(trace.String(), root) || strings.Contains(trace.String(), "missing-tsconfig") || strings.Contains(trace.String(), "{\"") {
43
+ t.Fatalf("failed-load trace exposed request or project content: %q", trace.String())
44
+ }
45
+ }
@@ -0,0 +1,52 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ // TestServePhaseTraceIsOptInAndPayloadFree proves the benchmark diagnostic is
10
+ // disabled by default and exposes timings without project or request content.
11
+ //
12
+ // 1. Run the same native shard request with tracing disabled and enabled.
13
+ // 2. Capture only the server diagnostic stream, not the response payload.
14
+ // 3. Require the five named phases and reject fixture paths and JSON bodies.
15
+ func TestServePhaseTraceIsOptInAndPayloadFree(t *testing.T) {
16
+ root := graphSessionFixture(t)
17
+ request := "{\"id\":17,\"graphSnapshotVersion\":1}\n"
18
+ oldStderr := stderr
19
+ defer func() { stderr = oldStderr }()
20
+
21
+ t.Setenv(graphPhaseTraceEnvironment, "")
22
+ var disabled bytes.Buffer
23
+ stderr = &disabled
24
+ if code := serveSnapshots(strings.NewReader(request), &bytes.Buffer{}, root, "tsconfig.json"); code != 0 {
25
+ t.Fatalf("disabled trace exited %d", code)
26
+ }
27
+ if disabled.Len() != 0 {
28
+ t.Fatalf("disabled trace wrote %q", disabled.String())
29
+ }
30
+
31
+ t.Setenv(graphPhaseTraceEnvironment, "1")
32
+ var enabled bytes.Buffer
33
+ stderr = &enabled
34
+ if code := serveSnapshots(strings.NewReader(request), &bytes.Buffer{}, root, "tsconfig.json"); code != 0 {
35
+ t.Fatalf("enabled trace exited %d", code)
36
+ }
37
+ trace := enabled.String()
38
+ for _, phase := range []string{
39
+ "native-load",
40
+ "semantic-refresh",
41
+ "shard-export",
42
+ "encode",
43
+ "producer-total",
44
+ } {
45
+ if !strings.Contains(trace, "owner=producer request=17 mode=initial phase="+phase+" durationMs=") {
46
+ t.Fatalf("trace omitted %s: %q", phase, trace)
47
+ }
48
+ }
49
+ if strings.Contains(trace, root) || strings.Contains(trace, "graphSnapshotVersion") || strings.Contains(trace, "{\"") {
50
+ t.Fatalf("trace exposed request or project content: %q", trace)
51
+ }
52
+ }
@@ -0,0 +1,44 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ // TestServeProtocolNegotiatesIncrementalShards pins the opt-in boundary between
11
+ // existing full-dump clients and the native shard protocol. A negotiated first
12
+ // response carries one complete manifest and transaction, while the following
13
+ // no-op carries neither a dump nor a shard payload.
14
+ func TestServeProtocolNegotiatesIncrementalShards(t *testing.T) {
15
+ root := graphSessionFixture(t)
16
+ input := strings.NewReader("{\"id\":1,\"graphSnapshotVersion\":1}\n{\"id\":2,\"graphSnapshotVersion\":1}\n")
17
+ var output bytes.Buffer
18
+ if code := serveSnapshots(input, &output, root, "tsconfig.json"); code != 0 {
19
+ t.Fatalf("serveSnapshots exited %d", code)
20
+ }
21
+
22
+ decoder := json.NewDecoder(&output)
23
+ var initial serveResponse
24
+ var unchanged serveResponse
25
+ if err := decoder.Decode(&initial); err != nil {
26
+ t.Fatal(err)
27
+ }
28
+ if err := decoder.Decode(&unchanged); err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ if initial.Error != "" || initial.Mode != serveModeInitial || !initial.Changed || initial.Dump != nil || initial.Snapshot == nil {
32
+ t.Fatalf("negotiated initial response: %#v", initial)
33
+ }
34
+ snapshot := initial.Snapshot
35
+ if snapshot.ProtocolVersion != graphSnapshotProtocolVersion || snapshot.Sequence != 1 || snapshot.BaseSequence != 0 || snapshot.BaseGeneration != "" {
36
+ t.Fatalf("initial transaction coordinates: %#v", snapshot)
37
+ }
38
+ if len(snapshot.Manifest) == 0 || len(snapshot.Upserts) != len(snapshot.Manifest) || len(snapshot.Deletes) != 0 {
39
+ t.Fatalf("initial transaction is not complete: manifest=%d upserts=%d deletes=%d", len(snapshot.Manifest), len(snapshot.Upserts), len(snapshot.Deletes))
40
+ }
41
+ if unchanged.Error != "" || unchanged.Mode != serveModeUnchanged || unchanged.Changed || unchanged.Dump != nil || unchanged.Snapshot != nil {
42
+ t.Fatalf("negotiated unchanged response: %#v", unchanged)
43
+ }
44
+ }
@@ -39,12 +39,12 @@ func TestServeSessionReloadsCommonJSPackageEntryForESM(t *testing.T) {
39
39
  t.Fatal(err)
40
40
  }
41
41
 
42
- writeGraphFile(t, filepath.Join(root, "node_modules", "fixture-package", "dist", "main.ts"), "export function winner(): void {}\nexport function packageEntryWinner(): void {}\n")
42
+ writeGraphFile(t, filepath.Join(root, "node_modules", "fixture-package", "dist", "main.ts"), "export function winner(): void {}\n")
43
43
  dump, mode, changed, err := session.Snapshot()
44
44
  if err != nil {
45
45
  t.Fatal(err)
46
46
  }
47
- if dump == nil || mode != serveModeReload || !changed || !hasDumpNode(*dump, "packageEntryWinner") {
47
+ if dump == nil || mode != serveModeReload || !changed || !hasDumpNodeAt(*dump, "winner", "node_modules/fixture-package/dist/main.ts") {
48
48
  t.Fatalf("CommonJS package main.ts = dump:%v mode:%q changed:%v", dump != nil, mode, changed)
49
49
  }
50
50
  }