redact: route OPF progress to stderr instead of /dev/tty · Entire

redact: route OPF progress to stderr instead of /dev/tty

626a034→main·Soph·4w ago·4 files·+31 added/-52 removed

OPF progress UX (→ scanning / ✓ done / × unavailable) defaulted to /dev/tty so the messages survived the post-commit hook's 2>/dev/null redirect. OPF no longer runs at post-commit — it lives solely in the pre-push rewrite path, whose hook is installed without a stderr redirect (post-commit condensation calls the 7-layer functions directly via RedactBlobBytes(..., usePrivacyFilter=false)). So the /dev/tty routing is obsolete: plain os.Stderr is visible during git push and, in tests, is captured by go test instead of bleeding straight to the terminal.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

8cc824aa79c0View transcript

Changes

4

2 unmodified lines

3
4
5
6
6
7
8
9
11
12
13
10
11
12
13
14
15
16
17
18
19
20
18
19
20
134 unmodified lines

155
156
157
161
162
163
158
159
160
94 unmodified lines

255
256
257
264
265
266
258
259
260

2 unmodified lines

import (
    "context"
    "errors"
    "io"
    "strings"
    "testing"
)

// configureFakeOPF wires up the runtime, redirects opfStderr, and registers
// cleanup so each test starts and ends with a clean config. Returns the fake
// so individual tests can assert call counts.
// configureFakeOPF wires up the runtime and registers cleanup so each
// test starts and ends with a clean config. Returns the fake so
// individual tests can assert call counts. (opfStderr is silenced
// process-wide in TestMain; see global_test.go.)
func configureFakeOPF(t *testing.T, fake *fakeRuntime, cats map[string]bool) {
    t.Helper()
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })
    ConfigurePrivacyFilterWithRuntime(OPFConfig{
        Enabled:    true,
        Categories: cats,
134 unmodified lines

rt := &recordingRuntime{spans: []Span{{Start: 0, End: 5, Label: "private_person"}}}
    resetOPFConfig()
    ...
    ConfigurePrivacyFilterWithRuntime(OPFConfig{
        Enabled:    true,
        Categories: map[string]bool{"private_person": true},
94 unmodified lines

fake := &shortReturnBatchRuntime{}
    resetOPFConfig()
    ...
}

Mredact/batch_test.go+4/-13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

package redact

import (
    "io"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    // OPF progress UX (→ scanning / ✓ done / × unavailable) defaults to
    // os.Stderr in production. Silence it process-wide for tests so it
    // neither bleeds into `go test -v` output nor needs a per-test
    // override. No test captures opfStderr to assert on its content; the
    // strategy package suppresses its own pre-push progress writer the
    // same way (see strategy/global_test.go).
    opfStderr = io.Discard
    os.Exit(m.Run())
}

Aredact/global_test.go+18

180 unmodified lines

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
184
185
186
187
188
189
190
191
192
193
194
195

180 unmodified lines

return out
}

// opfStderr is where progress and failure UX is written. Defaults to
// /dev/tty so messages survive the post-commit hook's stderr redirect
// (the hook runs `entire hooks git post-commit 2>/dev/null` to suppress
// unrelated logging). Tests override this directly.
var opfStderr = openTTYOrDiscard()

func openTTYOrDiscard() io.Writer {
    f, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0)
    if err != nil {
        return io.Discard
    }
    return f
}
// opfStderr is where progress and failure UX is written. OPF only runs
// in the pre-push rewrite path (strategy/manual_commit_opf_rewrite.go),
// whose hook is installed without an `2>/dev/null` redirect, so plain
// stderr reaches the user's terminal during `git push`. Post-commit
// condensation never invokes OPF (it calls the 7-layer functions
directly via RedactBlobBytes(..., usePrivacyFilter=false)), so the
// historical `/dev/tty` routing that survived the post-commit hook's
// stderr redirect is no longer needed. Tests override this directly.
var opfStderr io.Writer = os.Stderr

// detectOPF runs OPF on a single text and returns tagged regions for any
// spans whose category is enabled in cfg. Returns nil when OPF is disabled,

Mredact/opf.go+9/-13

2 unmodified lines

3
4
5
6
6
7
8
120 unmodified lines

129
130
131
133
134
135
136
132
133
134
23 unmodified lines

158
159
160
166
167
168
161
162
163
46 unmodified lines

210
211
212
221
222
223
213
214
215
45 unmodified lines

261
262
263
275
276
277
264
265
266
263 unmodified lines

530
531
532
547
548
549
533
534
535
10 unmodified lines

546
547
548
566
567
568
549
550
551
17 unmodified lines

569
570
571
592
593
594
572
573
574
40 unmodified lines

615
616
617
641
642
643
618
619
620

2 unmodified lines

import (
    "context"
    "errors"
    "io"
    "os"
    "os/exec"
    "path/filepath"
120 unmodified lines

resetOPFConfig()
    t.Cleanup(resetOPFConfig)

origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

fake := &fakeRuntime{
        spans: []Span{
            {Start: 0, End: 5, Label: "private_person"},
23 unmodified lines

func TestDetectOPF_SkipsCategoriesNotEnabled(t *testing.T) {
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

fake := &fakeRuntime{
        spans: []Span{
46 unmodified lines

func TestDetectOPF_CircuitBreaker(t *testing.T) {
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

fake := &fakeRuntime{err: errors.New("simulated opf failure")}
    ConfigurePrivacyFilterWithRuntime(OPFConfig{
45 unmodified lines

func TestDetectOPF_SingleInputShortReturnTripsBreaker(t *testing.T) {
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

ConfigurePrivacyFilterWithRuntime(OPFConfig{
        Enabled:    true,
263 unmodified lines

func TestStringWithPrivacyFilter_AugmentsRegexLayers(t *testing.T) {
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

fake := &fakeRuntime{spans: []Span{{Start: 0, End: 5, Label: "private_person"}}}
    ConfigurePrivacyFilterWithRuntime(OPFConfig{
10 unmodified lines

func TestJSONLContentWithPrivacyFilter_BatchesSingleCall(t *testing.T) {
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

fake := &fakeRuntime{spans: []Span{{Start: 0, End: 5, Label: "private_person"}}}
    ConfigurePrivacyFilterWithRuntime(OPFConfig{
17 unmodified lines

func TestJSONLContentWithPrivacyFilter_FallsBackOnBatchError(t *testing.T) {
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

fake := &fakeRuntime{err: errors.New("simulated opf failure")}
    ConfigurePrivacyFilterWithRuntime(OPFConfig{
40 unmodified lines

func TestJSONLContentWithPrivacyFilter_ShortReturnTripsBreaker(t *testing.T) {
    resetOPFConfig()
    t.Cleanup(resetOPFConfig)
    origStderr := opfStderr
    opfStderr = io.Discard
    t.Cleanup(func() { opfStderr = origStderr })

ConfigurePrivacyFilterWithRuntime(OPFConfig{
        Enabled:    true,