The tickets array was never broken. Every ticket in the reported batch targeted a client repo, and the I1 gate refuses a confidential capture through a us-nexus origin at any length — reproduced live: length 1 and 20 both fine for an internal repo, length 1 refused for a client one. What made it look like an array bug is that the refusal named neither the offending target nor that a policy had said no, so with 20 tickets in flight "the array is broken" was a reasonable conclusion. The refusal now names the targets carrying the effective classification, and tells a caller who declared confidential themselves exactly that. Regression tests at length 1 and 20 pin the path that was already correct; a third pins per-item partial success. Closes #91 part 1. Co-authored-by: Mathias <mthbqv@gmail.com>
541 lines
18 KiB
Go
541 lines
18 KiB
Go
package capture
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/classification"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- fakes ---
|
|
|
|
type fakeBrain struct {
|
|
writes []Note
|
|
updates []Note
|
|
gets []string
|
|
failOn func(Note) error // nil = always succeed
|
|
hashSeq int
|
|
}
|
|
|
|
func (f *fakeBrain) ref(prefix string, n Note, superseded bool) Ref {
|
|
f.hashSeq++
|
|
path := "wiki/" + n.Wing + "/" + n.Hall + "/" + n.Filename + ".md"
|
|
return Ref{ID: path, Path: path, ContentHash: prefix + string(rune('0'+f.hashSeq)), Superseded: superseded}
|
|
}
|
|
|
|
func (f *fakeBrain) Write(_ context.Context, n Note) (Ref, error) {
|
|
if f.failOn != nil {
|
|
if err := f.failOn(n); err != nil {
|
|
return Ref{}, err
|
|
}
|
|
}
|
|
f.writes = append(f.writes, n)
|
|
return f.ref("w", n, false), nil
|
|
}
|
|
|
|
func (f *fakeBrain) Update(_ context.Context, slug string, n Note) (Ref, error) {
|
|
if f.failOn != nil {
|
|
if err := f.failOn(n); err != nil {
|
|
return Ref{}, err
|
|
}
|
|
}
|
|
n.Filename = slug
|
|
f.updates = append(f.updates, n)
|
|
return f.ref("u", n, true), nil
|
|
}
|
|
|
|
func (f *fakeBrain) Get(_ context.Context, id string) (StoredNote, error) {
|
|
f.gets = append(f.gets, id)
|
|
return StoredNote{ID: id, Path: id}, nil
|
|
}
|
|
|
|
type fakeTracker struct {
|
|
created []string
|
|
closed []int
|
|
comments []int
|
|
err error
|
|
// failOnTitle makes CreateIssue fail for one specific title, so a
|
|
// partial-failure receipt can be exercised without failing every item.
|
|
failOnTitle string
|
|
}
|
|
|
|
func (f *fakeTracker) CreateIssue(_ context.Context, repo, title, _ string) (IssueRef, error) {
|
|
if f.err != nil {
|
|
return IssueRef{}, f.err
|
|
}
|
|
if f.failOnTitle != "" && title == f.failOnTitle {
|
|
return IssueRef{}, errors.New("tracker refused this one")
|
|
}
|
|
f.created = append(f.created, repo+":"+title)
|
|
return IssueRef{Repo: repo, Number: 100 + len(f.created), URL: "https://git/" + repo + "/issues/x"}, nil
|
|
}
|
|
|
|
func (f *fakeTracker) CloseIssue(_ context.Context, repo string, number int, _ string) (IssueRef, error) {
|
|
if f.err != nil {
|
|
return IssueRef{}, f.err
|
|
}
|
|
f.closed = append(f.closed, number)
|
|
return IssueRef{Repo: repo, Number: number}, nil
|
|
}
|
|
|
|
func (f *fakeTracker) CommentIssue(_ context.Context, repo string, number int, _ string) (IssueRef, error) {
|
|
if f.err != nil {
|
|
return IssueRef{}, f.err
|
|
}
|
|
f.comments = append(f.comments, number)
|
|
return IssueRef{Repo: repo, Number: number}, nil
|
|
}
|
|
|
|
// fakePolicy derives from an explicit map; default Internal so tests pin
|
|
// behaviour without depending on the real defaulting.
|
|
type fakePolicy struct {
|
|
tags map[string]classification.Level
|
|
}
|
|
|
|
func (p fakePolicy) Derive(t classification.Target) classification.Level {
|
|
if lvl, ok := p.tags[t.Name]; ok {
|
|
return lvl
|
|
}
|
|
return classification.Internal
|
|
}
|
|
|
|
type fakeAudit struct {
|
|
entries []AuditEntry
|
|
err error // Record error
|
|
reserveErr error // Reserve error (refuse before write)
|
|
reserveMode AuditOutcome
|
|
}
|
|
|
|
func (f *fakeAudit) Reserve(_ context.Context, _ classification.Level) (AuditOutcome, error) {
|
|
if f.reserveErr != nil {
|
|
return 0, f.reserveErr
|
|
}
|
|
return f.reserveMode, nil
|
|
}
|
|
|
|
func (f *fakeAudit) Record(_ context.Context, e AuditEntry, _ AuditOutcome) error {
|
|
if f.err != nil {
|
|
return f.err
|
|
}
|
|
f.entries = append(f.entries, e)
|
|
return nil
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func newSvc(b BrainStore, tr IssueTracker, p ClassificationPolicy, a AuditSink) *Service {
|
|
s := NewService(b, tr, p, a)
|
|
s.now = func() time.Time { return time.Date(2026, 6, 22, 12, 0, 0, 0, time.UTC) }
|
|
return s
|
|
}
|
|
|
|
func baseCtx() CaptureContext {
|
|
return CaptureContext{Harness: "claude-code", Actor: "mathias", Principal: "mathias", Classification: "internal"}
|
|
}
|
|
|
|
// --- scenarios ---
|
|
|
|
func TestCaptureHappyPath(t *testing.T) {
|
|
b := &fakeBrain{}
|
|
tr := &fakeTracker{}
|
|
au := &fakeAudit{}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Insights: []Insight{
|
|
{Text: "a", Wing: "hyperguild", Hall: "decisions", SupersedeSlug: ""},
|
|
{Text: "b", Wing: "hyperguild", Hall: "facts"},
|
|
},
|
|
Tickets: []Ticket{{Repo: "hyperguild", Action: "create", Title: "do x", Body: "y"}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rec.Insights, 2)
|
|
for _, r := range rec.Insights {
|
|
assert.True(t, r.OK)
|
|
assert.NotEmpty(t, r.ContentHash, "read-after-write hash returned")
|
|
}
|
|
require.Len(t, rec.Tickets, 1)
|
|
assert.True(t, rec.Tickets[0].OK)
|
|
assert.Equal(t, 2, len(b.writes))
|
|
assert.Empty(t, rec.Errors)
|
|
// Audit emitted naming principal/harness + items that landed.
|
|
require.Len(t, au.entries, 1)
|
|
assert.Equal(t, "mathias", au.entries[0].Principal)
|
|
assert.Equal(t, "claude-code", au.entries[0].Harness)
|
|
assert.Len(t, au.entries[0].Items, 3)
|
|
}
|
|
|
|
func TestCaptureSupersedeNotDuplicate(t *testing.T) {
|
|
b := &fakeBrain{}
|
|
svc := newSvc(b, &fakeTracker{}, fakePolicy{}, &fakeAudit{})
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Insights: []Insight{{Text: "revised", Wing: "hyperguild", Hall: "facts", SupersedeSlug: "prior-note"}},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, b.writes, "supersede must not create")
|
|
require.Len(t, b.updates, 1)
|
|
assert.Equal(t, "prior-note", b.updates[0].Filename)
|
|
assert.True(t, rec.Insights[0].Superseded)
|
|
}
|
|
|
|
func TestCaptureValidationFailClosed(t *testing.T) {
|
|
b := &fakeBrain{}
|
|
tr := &fakeTracker{}
|
|
au := &fakeAudit{}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
_, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Insights: []Insight{
|
|
{Text: "ok", Wing: "hyperguild", Hall: "facts"},
|
|
{Text: "bad", Wing: "hyperguild", Hall: "garbage-hall"}, // invalid hall
|
|
},
|
|
Tickets: []Ticket{{Repo: "hyperguild", Action: "create", Title: "t"}},
|
|
})
|
|
require.Error(t, err)
|
|
// Nothing written anywhere.
|
|
assert.Empty(t, b.writes)
|
|
assert.Empty(t, b.updates)
|
|
assert.Empty(t, tr.created)
|
|
assert.Empty(t, au.entries)
|
|
}
|
|
|
|
func TestCaptureValidationRejectsBadTicket(t *testing.T) {
|
|
svc := newSvc(&fakeBrain{}, &fakeTracker{}, fakePolicy{}, &fakeAudit{})
|
|
_, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Tickets: []Ticket{{Repo: "hyperguild", Action: "frobnicate"}}, // bad action
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, err = svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Tickets: []Ticket{{Repo: "hyperguild", Action: "close"}}, // close needs number
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCapturePartialFailureBestEffort(t *testing.T) {
|
|
b := &fakeBrain{failOn: func(n Note) error {
|
|
if strings.Contains(n.Content, "FAIL") {
|
|
return errors.New("disk full")
|
|
}
|
|
return nil
|
|
}}
|
|
tr := &fakeTracker{}
|
|
au := &fakeAudit{}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Insights: []Insight{
|
|
{Text: "good one", Wing: "hyperguild", Hall: "facts"},
|
|
{Text: "FAIL here", Wing: "hyperguild", Hall: "facts"},
|
|
},
|
|
Tickets: []Ticket{{Repo: "hyperguild", Action: "create", Title: "t"}},
|
|
})
|
|
require.NoError(t, err, "partial failure is not a request-level error")
|
|
assert.True(t, rec.Insights[0].OK)
|
|
assert.False(t, rec.Insights[1].OK)
|
|
assert.True(t, rec.Tickets[0].OK, "ticket still persisted; no rollback")
|
|
require.Len(t, rec.Errors, 1)
|
|
assert.Equal(t, "insight[1]", rec.Errors[0].Item)
|
|
// Audit reflects exactly what landed: 1 insight + 1 ticket.
|
|
require.Len(t, au.entries, 1)
|
|
assert.Len(t, au.entries[0].Items, 2)
|
|
}
|
|
|
|
func TestCaptureDryRunWritesNothing(t *testing.T) {
|
|
b := &fakeBrain{}
|
|
tr := &fakeTracker{}
|
|
au := &fakeAudit{}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
DryRun: true,
|
|
Insights: []Insight{{Text: "a", Wing: "hyperguild", Hall: "facts"}},
|
|
Tickets: []Ticket{{Repo: "hyperguild", Action: "create", Title: "t"}},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, rec.DryRun)
|
|
assert.Len(t, rec.Insights, 1)
|
|
assert.True(t, rec.Insights[0].OK, "would-be receipt marks planned items ok")
|
|
// Nothing written anywhere, including audit.
|
|
assert.Empty(t, b.writes)
|
|
assert.Empty(t, tr.created)
|
|
assert.Empty(t, au.entries)
|
|
}
|
|
|
|
func TestCaptureStricterClassificationWins(t *testing.T) {
|
|
// Caller declares internal; target wing tagged confidential → effective confidential + security event.
|
|
b := &fakeBrain{}
|
|
au := &fakeAudit{}
|
|
pol := fakePolicy{tags: map[string]classification.Level{"client-seb": classification.Confidential}}
|
|
svc := newSvc(b, &fakeTracker{}, pol, au)
|
|
|
|
ctx := baseCtx()
|
|
ctx.Classification = "internal"
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: ctx,
|
|
Insights: []Insight{{Text: "x", Wing: "client-seb", Hall: "facts"}},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "confidential", rec.EffectiveClassification)
|
|
require.Len(t, au.entries, 1)
|
|
assert.NotEmpty(t, au.entries[0].SecurityEvents, "under-declaration logged as security event")
|
|
assert.Equal(t, "confidential", au.entries[0].EffectiveClassification)
|
|
}
|
|
|
|
func TestCaptureCallerRaisingSensitivityHonoured(t *testing.T) {
|
|
// Caller declares confidential; target internal → effective confidential, NOT a security event.
|
|
au := &fakeAudit{}
|
|
pol := fakePolicy{tags: map[string]classification.Level{"hyperguild": classification.Internal}}
|
|
svc := newSvc(&fakeBrain{}, &fakeTracker{}, pol, au)
|
|
|
|
ctx := baseCtx()
|
|
ctx.Classification = "confidential"
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: ctx,
|
|
Insights: []Insight{{Text: "x", Wing: "hyperguild", Hall: "facts"}},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "confidential", rec.EffectiveClassification)
|
|
assert.Empty(t, au.entries[0].SecurityEvents, "raising sensitivity is honoured, not flagged")
|
|
}
|
|
|
|
// --- I1 sovereignty gate (#53) ---
|
|
|
|
func TestCaptureRefusesConfidentialViaUSNexus(t *testing.T) {
|
|
b := &fakeBrain{}
|
|
tr := &fakeTracker{}
|
|
au := &fakeAudit{}
|
|
pol := fakePolicy{tags: map[string]classification.Level{"client-seb": classification.Confidential}}
|
|
svc := newSvc(b, tr, pol, au)
|
|
|
|
ctx := baseCtx()
|
|
ctx.Classification = "confidential"
|
|
ctx.Origin = ZoneUSNexus
|
|
_, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: ctx,
|
|
Insights: []Insight{{Text: "x", Wing: "client-seb", Hall: "facts"}},
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrSovereigntyRefused)
|
|
// Refused before any write.
|
|
assert.Empty(t, b.writes)
|
|
assert.Empty(t, tr.created)
|
|
// Refusal is audited.
|
|
require.Len(t, au.entries, 1)
|
|
assert.Empty(t, au.entries[0].Items, "no items landed on refusal")
|
|
}
|
|
|
|
func TestCaptureAllowsConfidentialViaSovereign(t *testing.T) {
|
|
b := &fakeBrain{}
|
|
pol := fakePolicy{tags: map[string]classification.Level{"client-seb": classification.Confidential}}
|
|
svc := newSvc(b, &fakeTracker{}, pol, &fakeAudit{})
|
|
|
|
ctx := baseCtx()
|
|
ctx.Classification = "confidential"
|
|
ctx.Origin = ZoneSovereign
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: ctx,
|
|
Insights: []Insight{{Text: "x", Wing: "client-seb", Hall: "facts"}},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, rec.Insights[0].OK)
|
|
assert.Len(t, b.writes, 1)
|
|
}
|
|
|
|
func TestCaptureAssertedLabelIgnoredAndLogged(t *testing.T) {
|
|
// Caller asserts harness "sovereign-soil" but principal resolves to
|
|
// us-nexus; confidential ⇒ refused, and the discrepancy is a security event.
|
|
au := &fakeAudit{}
|
|
pol := fakePolicy{tags: map[string]classification.Level{"client-seb": classification.Confidential}}
|
|
svc := newSvc(&fakeBrain{}, &fakeTracker{}, pol, au)
|
|
|
|
ctx := baseCtx()
|
|
ctx.Harness = "sovereign-soil" // asserted
|
|
ctx.Origin = ZoneUSNexus // server-derived
|
|
ctx.Classification = "confidential"
|
|
_, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: ctx,
|
|
Insights: []Insight{{Text: "x", Wing: "client-seb", Hall: "facts"}},
|
|
})
|
|
require.ErrorIs(t, err, ErrSovereigntyRefused)
|
|
require.Len(t, au.entries, 1)
|
|
joined := strings.Join(au.entries[0].SecurityEvents, " | ")
|
|
assert.Contains(t, joined, "asserted-vs-derived origin mismatch")
|
|
assert.Contains(t, joined, "I1 refusal")
|
|
}
|
|
|
|
func TestCaptureInternalViaUSNexusAllowed(t *testing.T) {
|
|
// us-nexus origin is fine for non-confidential data.
|
|
b := &fakeBrain{}
|
|
svc := newSvc(b, &fakeTracker{}, fakePolicy{}, &fakeAudit{})
|
|
ctx := baseCtx()
|
|
ctx.Origin = ZoneUSNexus // internal classification, so gate doesn't fire
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: ctx,
|
|
Insights: []Insight{{Text: "x", Wing: "hyperguild", Hall: "facts"}},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, rec.Insights[0].OK)
|
|
}
|
|
|
|
// --- I5 audit gate (#54) ---
|
|
|
|
func TestCaptureRefusesWhenAuditReserveFails(t *testing.T) {
|
|
// Reserve refusing (e.g. confidential + central sink down, or the floor)
|
|
// aborts the capture before any write.
|
|
b := &fakeBrain{}
|
|
tr := &fakeTracker{}
|
|
au := &fakeAudit{reserveErr: errors.New("central sink unreachable")}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
_, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Insights: []Insight{{Text: "x", Wing: "hyperguild", Hall: "facts"}},
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrAuditUnavailable)
|
|
assert.Empty(t, b.writes, "nothing written when audit unavailable")
|
|
assert.Empty(t, tr.created)
|
|
}
|
|
|
|
func TestCaptureFlagsLocallyBufferedAudit(t *testing.T) {
|
|
// Reserve returns AuditBuffered (internal/public, central down) → capture
|
|
// proceeds and the receipt flags the degraded audit state.
|
|
b := &fakeBrain{}
|
|
au := &fakeAudit{reserveMode: AuditBuffered}
|
|
svc := newSvc(b, &fakeTracker{}, fakePolicy{}, au)
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Insights: []Insight{{Text: "x", Wing: "hyperguild", Hall: "facts"}},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, rec.Insights[0].OK, "capture proceeds on degraded audit")
|
|
assert.True(t, rec.AuditBuffered, "receipt flags locally-buffered audit")
|
|
require.Len(t, au.entries, 1)
|
|
}
|
|
|
|
func TestCaptureDryRunSkipsAuditGate(t *testing.T) {
|
|
// dry_run must not even probe the audit sink (writes nothing anywhere).
|
|
au := &fakeAudit{reserveErr: errors.New("would refuse")}
|
|
svc := newSvc(&fakeBrain{}, &fakeTracker{}, fakePolicy{}, au)
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
DryRun: true,
|
|
Insights: []Insight{{Text: "x", Wing: "hyperguild", Hall: "facts"}},
|
|
})
|
|
require.NoError(t, err, "dry-run does not hit the audit gate")
|
|
assert.True(t, rec.DryRun)
|
|
assert.Empty(t, au.entries)
|
|
}
|
|
|
|
// ---- hyperguild#91 part 1 -------------------------------------------------
|
|
//
|
|
// The reported symptom was "capture with a tickets array fails" (infra#265:
|
|
// 20 tickets errored, 5 errored, caller fell back to 20 individual
|
|
// issue_create calls). The array was never the failing dimension — every
|
|
// ticket targeted a client repo, and the I1 gate refuses that through a
|
|
// us-nexus origin at ANY length. The refusal did not say which target
|
|
// escalated the classification, so it read as a malfunction.
|
|
|
|
func TestCapture_TicketsArrayWorksAtLengthOne(t *testing.T) {
|
|
b, tr, au := &fakeBrain{}, &fakeTracker{}, &fakeAudit{}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Tickets: []Ticket{{Repo: "hyperguild", Action: "create", Title: "one", Body: "b"}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rec.Tickets, 1)
|
|
assert.True(t, rec.Tickets[0].OK)
|
|
assert.Len(t, tr.created, 1)
|
|
assert.Empty(t, rec.Errors)
|
|
}
|
|
|
|
func TestCapture_TicketsArrayWorksAtLengthMany(t *testing.T) {
|
|
b, tr, au := &fakeBrain{}, &fakeTracker{}, &fakeAudit{}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
in := CaptureInput{Context: baseCtx()}
|
|
for i := range 20 {
|
|
in.Tickets = append(in.Tickets, Ticket{
|
|
Repo: "hyperguild", Action: "create", Title: fmt.Sprintf("t%02d", i), Body: "b",
|
|
})
|
|
}
|
|
|
|
rec, err := svc.Capture(context.Background(), in)
|
|
require.NoError(t, err)
|
|
require.Len(t, rec.Tickets, 20, "one receipt entry per ticket")
|
|
for i, r := range rec.Tickets {
|
|
assert.True(t, r.OK, "ticket %d failed: %+v", i, rec.Errors)
|
|
}
|
|
assert.Len(t, tr.created, 20)
|
|
assert.Empty(t, rec.Errors)
|
|
}
|
|
|
|
func TestCapture_OneFailingTicketDoesNotSinkTheRest(t *testing.T) {
|
|
// infra#265 asked explicitly: "if ticket 3 of 5 fails, what happened to
|
|
// 1 and 2?" They land, and the receipt says so per item.
|
|
b, au := &fakeBrain{}, &fakeAudit{}
|
|
tr := &fakeTracker{failOnTitle: "boom"}
|
|
svc := newSvc(b, tr, fakePolicy{}, au)
|
|
|
|
rec, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: baseCtx(),
|
|
Tickets: []Ticket{
|
|
{Repo: "hyperguild", Action: "create", Title: "ok-1", Body: "b"},
|
|
{Repo: "hyperguild", Action: "create", Title: "boom", Body: "b"},
|
|
{Repo: "hyperguild", Action: "create", Title: "ok-3", Body: "b"},
|
|
},
|
|
})
|
|
require.NoError(t, err, "a per-item failure is a partial receipt, not a call error")
|
|
require.Len(t, rec.Tickets, 3)
|
|
assert.True(t, rec.Tickets[0].OK)
|
|
assert.False(t, rec.Tickets[1].OK)
|
|
assert.True(t, rec.Tickets[2].OK)
|
|
require.Len(t, rec.Errors, 1)
|
|
assert.Contains(t, rec.Errors[0].Item, "ticket[1]")
|
|
}
|
|
|
|
func TestCapture_I1RefusalNamesWhatEscalatedTheClassification(t *testing.T) {
|
|
// The actual defect behind infra#265. Without the offending target in
|
|
// the message, a caller with 20 tickets cannot tell which repo tripped
|
|
// the gate — and reasonably concludes the array itself is broken.
|
|
b, tr, au := &fakeBrain{}, &fakeTracker{}, &fakeAudit{}
|
|
pol := fakePolicy{tags: map[string]classification.Level{"trucker-ecommerce": classification.Confidential}}
|
|
svc := newSvc(b, tr, pol, au)
|
|
|
|
ctx := baseCtx()
|
|
ctx.Origin = ZoneUSNexus
|
|
_, err := svc.Capture(context.Background(), CaptureInput{
|
|
Context: ctx,
|
|
Tickets: []Ticket{
|
|
{Repo: "hyperguild", Action: "create", Title: "fine", Body: "b"},
|
|
{Repo: "trucker-ecommerce", Action: "create", Title: "client work", Body: "b"},
|
|
},
|
|
})
|
|
|
|
require.ErrorIs(t, err, ErrSovereigntyRefused)
|
|
assert.Contains(t, err.Error(), "trucker-ecommerce",
|
|
"the refusal must name the target that escalated the classification")
|
|
assert.NotContains(t, err.Error(), "hyperguild",
|
|
"only the offending target, or the message is noise at 20 tickets")
|
|
assert.Empty(t, tr.created, "refused before any write")
|
|
}
|