Skip to content

Commit b2df729

Browse files
tae2089claude
andauthored
Remove test-driven seams: Bounded* interfaces and package-global overrides (#4)
* Drop Bounded* analyzer interfaces and their dead fallback branches BoundedImpactAnalyzer and BoundedFlowTracer were obtained via runtime downcast from the base ImpactAnalyzer/FlowTracer, but the sole concrete types (*impact.Analyzer, *flows.Tracer) and the test mock all implement the bounded method, so the downcast's ok was always true and the else fallback branches never executed. Fold the bounded method into the base interface, call it directly, and delete the two Bounded* interfaces, the downcasts, the dead else branches, and the now-unused mock method. No behavior change: the removed branches were unreachable in production. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Replace test-only package globals with per-instance injection Three package-level mutable var seams existed only so tests could swap behavior, and reassigning a package global in a long-running MCP server is not goroutine-safe: - testBuildBatchReleaseHook and resolveBuildEdges (service) become GraphService fields (onBatchRelease, resolveEdges); edgeResolver() defaults to edgeresolve.ResolveWithOptions when unset. - refreshSearchDocuments (mcp) becomes a Deps field; the handler method defaults to service.RefreshSearchDocuments when unset. Tests now set the field on the instance instead of mutating a global. The production var literally named testBuildBatchReleaseHook is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent bb6d8b5 commit b2df729

8 files changed

Lines changed: 64 additions & 117 deletions

File tree

internal/mcp/deps.go

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,19 @@ type Parser interface {
3333
ParseWithContext(ctx context.Context, filePath string, content []byte) ([]model.Node, []model.Edge, error)
3434
}
3535

36-
// ImpactAnalyzer defines the blast-radius analysis contract for graph nodes.
37-
// @intent Injects an analyzer that calculates the impact radius of node changes into the server handler.
36+
// ImpactAnalyzer defines the bounded blast-radius analysis contract for graph nodes.
37+
// @intent inject a node/depth-capped blast-radius analyzer so a single MCP request cannot
38+
// expand into an unbounded graph walk.
3839
// @see mcp.handlers.getImpactRadius
3940
type ImpactAnalyzer interface {
40-
ImpactRadius(ctx context.Context, nodeID uint, depth int) ([]model.Node, error)
41-
}
42-
43-
// BoundedImpactAnalyzer extends ImpactAnalyzer with node and depth caps to prevent runaway traversal on large graphs.
44-
// @intent expose bounded blast-radius analysis for handlers that must protect shared MCP requests from unbounded graph walks.
45-
type BoundedImpactAnalyzer interface {
4641
ImpactRadiusBounded(ctx context.Context, nodeID uint, depth int, opts impactpkg.RadiusOptions) (*impactpkg.RadiusResult, error)
4742
}
4843

49-
// FlowTracer defines the call-flow tracing contract for graph nodes.
50-
// @intent Connects an analyzer to the server that reconstructs call flows starting from a given node.
44+
// FlowTracer defines the bounded call-flow tracing contract for graph nodes.
45+
// @intent inject a node-capped call-flow tracer so a deep call chain cannot expand into an
46+
// unbounded traversal.
5147
// @see mcp.handlers.traceFlow
5248
type FlowTracer interface {
53-
TraceFlow(ctx context.Context, startNodeID uint) (*model.Flow, error)
54-
}
55-
56-
// BoundedFlowTracer extends FlowTracer with a node cap to prevent runaway traversal on deeply nested call chains.
57-
// @intent let MCP handlers trace deep call chains without letting one request expand into an unbounded traversal.
58-
type BoundedFlowTracer interface {
5949
TraceFlowBounded(ctx context.Context, startNodeID uint, opts flowspkg.TraceOptions) (*flowspkg.TraceResult, error)
6050
}
6151

@@ -193,4 +183,8 @@ type Deps struct {
193183

194184
MaxFileBytes int64
195185
MaxTotalParsedBytes int64
186+
187+
// RefreshSearchDocuments overrides the search-document refresh used after a build; nil uses
188+
// service.RefreshSearchDocuments. Kept as an injectable field so tests need no package globals.
189+
RefreshSearchDocuments func(ctx context.Context, db *gorm.DB) (int, error)
196190
}

internal/mcp/handler_analysis.go

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -161,32 +161,13 @@ func (h *handlers) getImpactRadius(ctx context.Context, request mcp.CallToolRequ
161161
return "", nodeNotFoundErr(qn)
162162
}
163163

164-
var nodes []model.Node
165-
truncated := false
166-
if bounded, ok := h.deps.ImpactAnalyzer.(BoundedImpactAnalyzer); ok {
167-
res, err := bounded.ImpactRadiusBounded(ctx, node.ID, depth, impactpkg.RadiusOptions{MaxDepth: maxDepth, MaxNodes: maxNodes})
168-
if err != nil {
169-
log.ErrorContext(ctx, "impact analysis error", append(obs.TraceLogArgs(ctx), "node_id", node.ID, trace.SlogError(err))...)
170-
return "", trace.Wrap(err, "impact analysis error")
171-
}
172-
nodes = res.Nodes
173-
truncated = res.Truncated
174-
} else {
175-
if maxDepth > 0 && depth > maxDepth {
176-
depth = maxDepth
177-
truncated = true
178-
}
179-
var err error
180-
nodes, err = h.deps.ImpactAnalyzer.ImpactRadius(ctx, node.ID, depth)
181-
if err != nil {
182-
log.ErrorContext(ctx, "impact analysis error", append(obs.TraceLogArgs(ctx), "node_id", node.ID, trace.SlogError(err))...)
183-
return "", trace.Wrap(err, "impact analysis error")
184-
}
185-
if maxNodes > 0 && len(nodes) > maxNodes {
186-
nodes = nodes[:maxNodes]
187-
truncated = true
188-
}
164+
res, err := h.deps.ImpactAnalyzer.ImpactRadiusBounded(ctx, node.ID, depth, impactpkg.RadiusOptions{MaxDepth: maxDepth, MaxNodes: maxNodes})
165+
if err != nil {
166+
log.ErrorContext(ctx, "impact analysis error", append(obs.TraceLogArgs(ctx), "node_id", node.ID, trace.SlogError(err))...)
167+
return "", trace.Wrap(err, "impact analysis error")
189168
}
169+
nodes := res.Nodes
170+
truncated := res.Truncated
190171

191172
log.InfoContext(ctx, "get_impact_radius completed", append(obs.TraceLogArgs(ctx), "qualified_name", qn, "result_count", len(nodes))...)
192173

@@ -249,32 +230,15 @@ func (h *handlers) traceFlow(ctx context.Context, request mcp.CallToolRequest) (
249230
return "", nodeNotFoundErr(qn)
250231
}
251232

252-
var flow *model.Flow
253-
truncated := false
254-
containsFallbackCalls := false
255-
fallbackEdgesCount := 0
256-
if bounded, ok := h.deps.FlowTracer.(BoundedFlowTracer); ok {
257-
res, err := bounded.TraceFlowBounded(ctx, node.ID, flowspkg.TraceOptions{MaxNodes: maxNodes, IncludeFallbackCalls: &includeFallbackCalls})
258-
if err != nil {
259-
log.ErrorContext(ctx, "trace error", append(obs.TraceLogArgs(ctx), "node_id", node.ID, trace.SlogError(err))...)
260-
return "", trace.Wrap(err, "trace error")
261-
}
262-
flow = res.Flow
263-
truncated = res.Truncated
264-
containsFallbackCalls = res.ContainsFallbackCalls
265-
fallbackEdgesCount = res.FallbackEdgesCount
266-
} else {
267-
var err error
268-
flow, err = h.deps.FlowTracer.TraceFlow(ctx, node.ID)
269-
if err != nil {
270-
log.ErrorContext(ctx, "trace error", append(obs.TraceLogArgs(ctx), "node_id", node.ID, trace.SlogError(err))...)
271-
return "", trace.Wrap(err, "trace error")
272-
}
273-
if maxNodes > 0 && len(flow.Members) > maxNodes {
274-
flow.Members = flow.Members[:maxNodes]
275-
truncated = true
276-
}
233+
res, err := h.deps.FlowTracer.TraceFlowBounded(ctx, node.ID, flowspkg.TraceOptions{MaxNodes: maxNodes, IncludeFallbackCalls: &includeFallbackCalls})
234+
if err != nil {
235+
log.ErrorContext(ctx, "trace error", append(obs.TraceLogArgs(ctx), "node_id", node.ID, trace.SlogError(err))...)
236+
return "", trace.Wrap(err, "trace error")
277237
}
238+
flow := res.Flow
239+
truncated := res.Truncated
240+
containsFallbackCalls := res.ContainsFallbackCalls
241+
fallbackEdgesCount := res.FallbackEdgesCount
278242

279243
log.InfoContext(ctx, "trace_flow completed", append(obs.TraceLogArgs(ctx), "qualified_name", qn, "members", len(flow.Members))...)
280244

internal/mcp/handler_parse.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import (
1818
"github.com/tae2089/code-context-graph/internal/service"
1919
)
2020

21-
var refreshSearchDocuments = service.RefreshSearchDocuments
21+
// @intent refresh search documents through the injected override, defaulting to the service impl.
22+
func (h *handlers) refreshSearchDocuments(ctx context.Context) (int, error) {
23+
if h.deps.RefreshSearchDocuments != nil {
24+
return h.deps.RefreshSearchDocuments(ctx, h.deps.DB)
25+
}
26+
return service.RefreshSearchDocuments(ctx, h.deps.DB)
27+
}
2228

2329
// @intent serialize build_or_update_graph results with a fixed JSON schema without changing the wire format.
2430
type buildOrUpdateGraphResponse struct {
@@ -250,7 +256,7 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR
250256
}
251257
// search rebuild
252258
if h.deps.SearchBackend != nil && h.deps.DB != nil {
253-
if _, err := refreshSearchDocuments(ctx, h.deps.DB); err != nil {
259+
if _, err := h.refreshSearchDocuments(ctx); err != nil {
254260
if failClosed {
255261
failClosedErr = err
256262
failedSteps = append(failedSteps, "search_documents")
@@ -274,7 +280,7 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR
274280
skippedSteps = appendUniqueStrings(skippedSteps, "communities", "flows")
275281
// search only rebuild
276282
if h.deps.SearchBackend != nil && h.deps.DB != nil {
277-
if _, err := refreshSearchDocuments(ctx, h.deps.DB); err != nil {
283+
if _, err := h.refreshSearchDocuments(ctx); err != nil {
278284
if failClosed {
279285
failClosedErr = err
280286
failedSteps = append(failedSteps, "search_documents")
@@ -433,7 +439,7 @@ func (h *handlers) runPostprocess(ctx context.Context, request mcp.CallToolReque
433439

434440
if doFTS {
435441
if h.deps.SearchBackend != nil && h.deps.DB != nil {
436-
if _, err := refreshSearchDocuments(ctx, h.deps.DB); err != nil {
442+
if _, err := h.refreshSearchDocuments(ctx); err != nil {
437443
if failClosed {
438444
failClosedErr = err
439445
failedSteps = append(failedSteps, "search_documents")

internal/mcp/handlers_test.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,9 +1404,7 @@ func TestBuildOrUpdateGraph_DegradedOnSearchDocumentRefreshFailure(t *testing.T)
14041404
deps := setupTestDeps(t)
14051405
backend := &failSearchBackend{}
14061406
deps.SearchBackend = backend
1407-
origRefresh := refreshSearchDocuments
1408-
defer func() { refreshSearchDocuments = origRefresh }()
1409-
refreshSearchDocuments = func(ctx context.Context, db *gorm.DB) (int, error) {
1407+
deps.RefreshSearchDocuments = func(ctx context.Context, db *gorm.DB) (int, error) {
14101408
return 0, errors.New("search document refresh boom")
14111409
}
14121410

@@ -1451,9 +1449,7 @@ func TestBuildOrUpdateGraph_MinimalSkipsFTSRebuildOnSearchDocumentRefreshFailure
14511449
deps := setupTestDeps(t)
14521450
backend := &failSearchBackend{}
14531451
deps.SearchBackend = backend
1454-
origRefresh := refreshSearchDocuments
1455-
defer func() { refreshSearchDocuments = origRefresh }()
1456-
refreshSearchDocuments = func(ctx context.Context, db *gorm.DB) (int, error) {
1452+
deps.RefreshSearchDocuments = func(ctx context.Context, db *gorm.DB) (int, error) {
14571453
return 0, errors.New("search document refresh boom")
14581454
}
14591455

internal/mcp/testmocks_test.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -365,23 +365,6 @@ type mockFlowTracer struct {
365365
remainderErr error
366366
}
367367

368-
func (m *mockFlowTracer) TraceFlow(ctx context.Context, startNodeID uint) (*model.Flow, error) {
369-
m.calls++
370-
flow := m.returnFlow
371-
if flow == nil {
372-
flow = &model.Flow{
373-
Namespace: ctxns.FromContext(ctx),
374-
Name: "flow_from_mock",
375-
Members: []model.FlowMembership{{
376-
NodeID: startNodeID,
377-
Ordinal: 0,
378-
Namespace: ctxns.FromContext(ctx),
379-
}},
380-
}
381-
}
382-
return flow, m.traceErr
383-
}
384-
385368
func (m *mockFlowTracer) TraceFlowBounded(ctx context.Context, startNodeID uint, opts flows.TraceOptions) (*flows.TraceResult, error) {
386369
m.calls++
387370
m.opts = append(m.opts, opts)

internal/service/build.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ func (s *GraphService) bindAndReleaseNodeBatch(ctx context.Context, txStore stor
100100

101101
parsed.tsComments = nil
102102
parsed.sourceLines = nil
103-
if testBuildBatchReleaseHook != nil {
104-
testBuildBatchReleaseHook(batches, idx)
103+
if s.onBatchRelease != nil {
104+
s.onBatchRelease(batches, idx)
105105
}
106106
return nil
107107
}
@@ -450,7 +450,7 @@ func (s *GraphService) flushBuildEdges(ctx context.Context, txStore store.GraphS
450450
return err
451451
}
452452
end := min(start+buildEdgeResolveChunkSize, len(implementsEdges))
453-
resolved, err := resolveBuildEdges(ctx, txStore, implementsEdges[start:end], resolveOptions)
453+
resolved, err := s.edgeResolver()(ctx, txStore, implementsEdges[start:end], resolveOptions)
454454
if err != nil {
455455
s.logger().ErrorContext(ctx, "resolve deferred implements edges failed", append(obs.TraceLogArgs(ctx), "start", start, "end", end, "error", err)...)
456456
return trace.Wrap(err, "resolve deferred implements edges")
@@ -474,7 +474,7 @@ func (s *GraphService) flushBuildEdges(ctx context.Context, txStore store.GraphS
474474
end := min(start+buildEdgeResolveChunkSize, len(parsed.edges))
475475
chunk := parsed.edges[start:end]
476476
resolveInput := chunkWithImportWarmup(chunk, importsByPath[parsed.relPath])
477-
resolved, err := resolveBuildEdges(ctx, txStore, resolveInput, resolveOptions)
477+
resolved, err := s.edgeResolver()(ctx, txStore, resolveInput, resolveOptions)
478478
if err != nil {
479479
s.logger().ErrorContext(ctx, "resolve deferred edges failed", append(obs.TraceLogArgs(ctx), "file", parsed.relPath, "error", err)...)
480480
return trace.Wrap(err, "resolve deferred edges for "+parsed.relPath)

internal/service/indexer.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@ import (
1818

1919
type languagePackageInfo = treesitter.PackageInfo
2020

21-
var (
22-
testBuildBatchReleaseHook func([]parsedBuildNodeBatch, int)
23-
resolveBuildEdges resolveBuildEdgesFn = edgeresolve.ResolveWithOptions
24-
)
25-
26-
// @intent abstract build-time edge resolution so tests and build paths can swap resolver behavior without rewiring callers.
21+
// @intent abstract build-time edge resolution so tests can inject resolver behavior per GraphService.
2722
type resolveBuildEdgesFn func(ctx context.Context, lookup edgeresolve.NodeLookup, edges []model.Edge, options edgeresolve.ResolveOptions) ([]model.Edge, error)
2823

2924
const (
@@ -89,6 +84,20 @@ type GraphService struct {
8984
Walkers map[string]*treesitter.Walker
9085
Parsers map[string]Parser
9186
Logger *slog.Logger
87+
88+
// resolveEdges overrides build-time edge resolution; nil uses edgeresolve.ResolveWithOptions.
89+
// onBatchRelease, when set, is notified after each node batch is persisted and its buffers
90+
// are released. Both are test seams kept per-instance so the server has no mutable globals.
91+
resolveEdges resolveBuildEdgesFn
92+
onBatchRelease func([]parsedBuildNodeBatch, int)
93+
}
94+
95+
// @intent resolve build edges through the injected resolver, defaulting to the production resolver.
96+
func (s *GraphService) edgeResolver() resolveBuildEdgesFn {
97+
if s.resolveEdges != nil {
98+
return s.resolveEdges
99+
}
100+
return edgeresolve.ResolveWithOptions
92101
}
93102

94103
// BuildOptions configures one graph build run.

internal/service/indexer_test.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -498,22 +498,20 @@ func TestFlushBuildEdges_ResolvesAndUpsertsBoundedBatches(t *testing.T) {
498498
}
499499

500500
var resolveSizes []int
501-
oldResolve := resolveBuildEdges
502-
resolveBuildEdges = func(ctx context.Context, lookup edgeresolve.NodeLookup, edges []model.Edge, options edgeresolve.ResolveOptions) ([]model.Edge, error) {
501+
resolver := func(ctx context.Context, lookup edgeresolve.NodeLookup, edges []model.Edge, options edgeresolve.ResolveOptions) ([]model.Edge, error) {
503502
resolveSizes = append(resolveSizes, len(edges))
504503
if len(edges) > buildEdgeResolveChunkSize {
505504
t.Fatalf("resolve batch exceeded limit: got %d want <= %d", len(edges), buildEdgeResolveChunkSize)
506505
}
507-
return oldResolve(ctx, lookup, edges, options)
506+
return edgeresolve.ResolveWithOptions(ctx, lookup, edges, options)
508507
}
509-
t.Cleanup(func() { resolveBuildEdges = oldResolve })
510508

511509
batches := []parsedBuildEdgeBatch{
512510
{relPath: "cmd/main.go", edges: []model.Edge{{Kind: model.EdgeKindImportsFrom, FilePath: "cmd/main.go", Line: 1, Fingerprint: "imports_from:cmd/main.go:github.com/example/project/mcp:1"}, {Kind: model.EdgeKindCalls, FilePath: "cmd/main.go", Line: 2, Fingerprint: "calls:cmd/main.go:h.deps.FlowTracer.TraceFlow:2"}}},
513511
{relPath: "flows/tracer.go", edges: []model.Edge{{Kind: model.EdgeKindImplements, FilePath: "flows/tracer.go", Line: 7, Fingerprint: "implements:flows/tracer.go:flows.Tracer:mcp.FlowTracer"}}},
514512
}
515513

516-
svc := &GraphService{}
514+
svc := &GraphService{resolveEdges: resolver}
517515
if err := svc.flushBuildEdges(ctx, st, batches, nil, edgeresolve.ResolveOptions{}); err != nil {
518516
t.Fatalf("flushBuildEdges: %v", err)
519517
}
@@ -543,26 +541,24 @@ func TestFlushBuildEdges_ResolvesImplementsOnlyOnce(t *testing.T) {
543541
st.nodesByFP["flows/tracer.go"] = []model.Node{{ID: 30, QualifiedName: "flows/tracer.go", Name: "flows/tracer.go", Kind: model.NodeKindFile, FilePath: "flows/tracer.go", Language: "go"}, {ID: 3, QualifiedName: "flows.Tracer", Name: "Tracer", Kind: model.NodeKindClass, FilePath: "flows/tracer.go", StartLine: 7, EndLine: 7, Language: "go"}, {ID: 4, QualifiedName: "flows.Tracer.TraceFlow", Name: "TraceFlow", Kind: model.NodeKindFunction, FilePath: "flows/tracer.go", StartLine: 9, EndLine: 11, Language: "go"}}
544542

545543
var implementsSeen []int
546-
oldResolve := resolveBuildEdges
547-
resolveBuildEdges = func(ctx context.Context, lookup edgeresolve.NodeLookup, edges []model.Edge, options edgeresolve.ResolveOptions) ([]model.Edge, error) {
544+
resolver := func(ctx context.Context, lookup edgeresolve.NodeLookup, edges []model.Edge, options edgeresolve.ResolveOptions) ([]model.Edge, error) {
548545
count := 0
549546
for _, edge := range edges {
550547
if edge.Kind == model.EdgeKindImplements {
551548
count++
552549
}
553550
}
554551
implementsSeen = append(implementsSeen, count)
555-
return oldResolve(ctx, lookup, edges, options)
552+
return edgeresolve.ResolveWithOptions(ctx, lookup, edges, options)
556553
}
557-
t.Cleanup(func() { resolveBuildEdges = oldResolve })
558554

559555
batches := []parsedBuildEdgeBatch{
560556
{relPath: "flows/tracer.go", edges: []model.Edge{{Kind: model.EdgeKindImplements, FilePath: "flows/tracer.go", Line: 7, Fingerprint: "implements:flows/tracer.go:flows.Tracer:mcp.FlowTracer"}}},
561557
{relPath: "cmd/main.go", edges: []model.Edge{{Kind: model.EdgeKindImportsFrom, FilePath: "cmd/main.go", Line: 1, Fingerprint: "imports_from:cmd/main.go:github.com/example/project/mcp:1"}, {Kind: model.EdgeKindCalls, FilePath: "cmd/main.go", Line: 2, Fingerprint: "calls:cmd/main.go:h.deps.FlowTracer.TraceFlow:2"}}},
562558
{relPath: "cmd/main.go", edges: []model.Edge{{Kind: model.EdgeKindContains, FilePath: "cmd/main.go", Line: 1, Fingerprint: "contains:cmd/main.go:main.Run"}}},
563559
}
564560

565-
svc := &GraphService{}
561+
svc := &GraphService{resolveEdges: resolver}
566562
if err := svc.flushBuildEdges(ctx, st, batches, nil, edgeresolve.ResolveOptions{}); err != nil {
567563
t.Fatalf("flushBuildEdges: %v", err)
568564
}
@@ -2100,8 +2096,7 @@ func TestBuild_ReleasesBatchCommentStateAfterBinding(t *testing.T) {
21002096
tsCommentsNil bool
21012097
sourceNil bool
21022098
}
2103-
prevHook := testBuildBatchReleaseHook
2104-
testBuildBatchReleaseHook = func(batches []parsedBuildNodeBatch, idx int) {
2099+
recordRelease := func(batches []parsedBuildNodeBatch, idx int) {
21052100
snapshots = append(snapshots, struct {
21062101
batch int
21072102
tsCommentsNil bool
@@ -2112,15 +2107,15 @@ func TestBuild_ReleasesBatchCommentStateAfterBinding(t *testing.T) {
21122107
sourceNil: batches[idx].sourceLines == nil,
21132108
})
21142109
}
2115-
defer func() { testBuildBatchReleaseHook = prevHook }()
21162110

21172111
fakeStore := newRecordingGraphStore(t)
21182112
svc := &GraphService{
21192113
Store: fakeStore,
21202114
Walkers: map[string]*treesitter.Walker{
21212115
".go": treesitter.NewWalker(treesitter.GoSpec),
21222116
},
2123-
Logger: slog.Default(),
2117+
Logger: slog.Default(),
2118+
onBatchRelease: recordRelease,
21242119
}
21252120

21262121
dir := t.TempDir()

0 commit comments

Comments
 (0)