refactor(output): always warn on stderr when jq may drop a content-safety alert

When --jq is applied, the jq expression can filter the _content_safety_alert
field out of stdout, hiding the warning. Whether a stderr fallback warning was
written used to depend on a caller-set EmitOptions.JQSafetyWarning flag, so the
raw api/service paths warned but shortcut commands did not — the safety alert
was silently lost. Remove the flag and always write the stderr warning when jq
is applied and an alert exists.
This commit is contained in:
shanglei
2026-07-22 14:37:11 +08:00
parent e0e034c170
commit 9f22d89112
5 changed files with 53 additions and 24 deletions

View File

@@ -39,17 +39,13 @@ type EmitterConfig struct {
// envelope encoding and jq's complex-value encoding. Format is a canonical
// typed value — boundaries reject unknown formats via ParseFormatStrict, so the
// Emitter never sees one and never falls back.
//
// JQSafetyWarning preserves the legacy difference between RuntimeContext.emit
// (false) and WriteSuccessEnvelope (true) until their callers are migrated.
type EmitOptions struct {
Raw bool
Meta *Meta
Format Format
JQ string
DryRun bool
Pretty PrettyRenderer
JQSafetyWarning bool
Raw bool
Meta *Meta
Format Format
JQ string
DryRun bool
Pretty PrettyRenderer
}
// StreamOptions describes one streamed page's wire representation. Streaming
@@ -191,7 +187,7 @@ func (e *Emitter) emitEnvelope(data interface{}, ok bool, opts EmitOptions) erro
}
if opts.JQ != "" {
if scanResult.Alert != nil && opts.JQSafetyWarning {
if scanResult.Alert != nil {
if err := WriteAlertWarning(e.errOut, scanResult.Alert); err != nil {
return wrapOutputError("write", err)
}

View File

@@ -456,6 +456,41 @@ func TestEmitterRawJSONPropagatesWriteError(t *testing.T) {
}
}
func TestEmitterJQSafetyAlertAlwaysWritesStderrWarning(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONTENT_SAFETY_MODE", "warn")
extcs.Register(&contractSafetyProvider{alert: &extcs.Alert{
Provider: "emitter-contract",
MatchedRules: []string{"fixture-rule"},
}})
t.Cleanup(func() { extcs.Register(nil) })
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
emitter := output.NewEmitter(output.EmitterConfig{
Out: stdout,
ErrOut: stderr,
CommandPath: "lark-cli fixture +emit",
})
err := emitter.Success(map[string]interface{}{"id": "1"}, output.EmitOptions{
Format: output.FormatJSON,
JQ: ".data",
})
if err != nil {
t.Fatalf("Emitter.Success() error = %v", err)
}
wantStdout := "{\n \"id\": \"1\"\n}\n"
if stdout.String() != wantStdout {
t.Fatalf("Emitter.Success() stdout = %q, want %q", stdout.String(), wantStdout)
}
if strings.Contains(stdout.String(), "_content_safety_alert") {
t.Fatalf("Emitter.Success() stdout contains filtered safety alert: %q", stdout.String())
}
wantWarning := "warning: content safety alert from emitter-contract (rules: fixture-rule)\n"
if !strings.Contains(stderr.String(), wantWarning) {
t.Fatalf("Emitter.Success() stderr = %q, want warning containing %q", stderr.String(), wantWarning)
}
}
func TestEmitterInvalidJQReturnsErrorWithoutStderr(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONTENT_SAFETY_MODE", "off")
stderr := &bytes.Buffer{}

View File

@@ -236,7 +236,7 @@ func TestEmitterMatchesRuntimeContextLegacyOracle(t *testing.T) {
useFormat: true,
},
{
name: "jq_safety_alert_without_stderr_warning",
name: "jq_safety_alert_writes_stderr_warning",
data: func() interface{} {
return map[string]interface{}{"id": "1"}
},
@@ -539,11 +539,10 @@ func TestEmitterMatchesWriteSuccessEnvelopeLegacyOracle(t *testing.T) {
Identity: "bot",
NoticeProvider: func() map[string]interface{} { return notice },
}, true, output.EmitOptions{
Format: output.FormatJSON,
Raw: false,
JQ: tc.jq,
DryRun: tc.dryRun,
JQSafetyWarning: true,
Format: output.FormatJSON,
Raw: false,
JQ: tc.jq,
DryRun: tc.dryRun,
})
assertEmitterGolden(t, want, current)

View File

@@ -41,10 +41,9 @@ func WriteSuccessEnvelope(data interface{}, opts SuccessEnvelopeOptions) error {
Identity: opts.Identity,
NoticeProvider: GetNotice,
}).Success(data, EmitOptions{
Format: FormatJSON,
Raw: false,
JQ: opts.JqExpr,
DryRun: opts.DryRun,
JQSafetyWarning: true,
Format: FormatJSON,
Raw: false,
JQ: opts.JqExpr,
DryRun: opts.DryRun,
})
}

View File

@@ -22,9 +22,9 @@
"exit_code": 2
}
},
"jq_safety_alert_without_stderr_warning": {
"jq_safety_alert_writes_stderr_warning": {
"stdout": "1\n",
"stderr": ""
"stderr": "warning: content safety alert from emitter-oracle (rules: fixture-rule)\n"
},
"jq_scalar": {
"stdout": "Alice\n",