Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions boatstack/internal/softwaredelivery/durable/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type State struct {
RuntimeFingerprint string `json:"runtime_fingerprint,omitempty"`
RuntimeSource string `json:"runtime_source_revision,omitempty"`
PlanFingerprint string `json:"plan_fingerprint,omitempty"`
ApprovalFingerprint string `json:"approval_fingerprint,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Version the durable state ABI for the new field

Invariant: equal state-schema identities must denote mutually compatible durable wire formats. Head now emits approval_fingerprint while retaining schema version 4; the base runtime's strict decoder rejects that unknown field. Thus after head commits an approval, an exact base candidate cannot even observe the state to perform an otherwise legal runtime downgrade/update, despite both pins declaring schema 4. The new legacy test checks only that the current decoder reads field-absent bytes and does not exercise the real old decoder. A regression test should use the actual base decoder against a head-emitted approved state and require either compatibility or a distinct schema identity with an explicit migration path.

Confidence: 0.96

WorkspaceBranch string `json:"workspace_branch,omitempty"`
WorkspacePath string `json:"workspace_path,omitempty"`
WorkspaceBaseRef string `json:"workspace_base_ref,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion boatstack/internal/softwaredelivery/durable/state_facet.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var stateFieldFacets = map[string]model.StateFacet{
"ConfigFingerprint": model.StateFacetControl, "PlanApprovalPolicy": model.StateFacetControl, "VisualEvidencePolicy": model.StateFacetControl,
"ExternalEffectPolicy": model.StateFacetControl, "IndependentReview": model.StateFacetControl, "EnabledHosts": model.StateFacetControl,
"RuntimeVersion": model.StateFacetInstallation, "RuntimeFingerprint": model.StateFacetInstallation, "RuntimeSource": model.StateFacetInstallation,
"PlanFingerprint": model.StateFacetProduct,
"PlanFingerprint": model.StateFacetProduct, "ApprovalFingerprint": model.StateFacetProduct,
"WorkspaceBranch": model.StateFacetProduct, "WorkspacePath": model.StateFacetProduct, "WorkspaceBaseRef": model.StateFacetProduct,
"WorkspaceSourcePath": model.StateFacetProduct, "WorkspaceSourceID": model.StateFacetProduct, "WorkspaceSourceRef": model.StateFacetProduct,
"PublicationID": model.StateFacetProduct, "PublicationURL": model.StateFacetProduct, "PreviewFingerprint": model.StateFacetProduct,
Expand Down
28 changes: 27 additions & 1 deletion boatstack/internal/softwaredelivery/durable/state_schema_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
package durable

import "testing"
import (
"testing"
"time"

"github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model"
)

func TestStateRejectsPriorObjectiveSchema(t *testing.T) {
state := State{SchemaVersion: StateSchemaVersion - 1}
if err := state.Validate(); err == nil {
t.Fatal("prior objective state schema was accepted")
}
}

func TestStateSchemaPermitsLegacyApprovedStateWithoutApprovalFingerprint(t *testing.T) {
state := State{
SchemaVersion: StateSchemaVersion, RepositoryID: "repo", GitCommonID: "common", WorktreeID: "worktree", Revision: 1,
Phase: model.PhaseActive, Engagement: model.EngagementActive, Delivery: model.DeliveryApproved, Workspace: model.WorkspaceAbsent,
Plan: model.PlanApproved, Configuration: model.ConfigurationUnsupported, Runtime: model.RuntimeAbsent, Publication: model.PublicationNone,
Verification: model.VerificationUnverified, Recovery: model.RecoveryNone, Transaction: model.TransactionNone, Terminal: model.TerminalNonterminal,
Objective: model.Objective{ID: "objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"}, PlanFingerprint: "legacy-plan", UpdatedAt: time.Unix(1, 0).UTC(),
}
raw, err := EncodeState(state)
if err != nil {
t.Fatal(err)
}
decoded, err := DecodeState(raw)
if err != nil {
t.Fatal(err)
}
if decoded.ApprovalFingerprint != "" {
t.Fatalf("legacy approval fingerprint = %q", decoded.ApprovalFingerprint)
}
}
67 changes: 67 additions & 0 deletions boatstack/internal/softwaredelivery/effects/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admissio
}
mutations = append(mutations, approvalMutation)
state.PlanFingerprint = fingerprint
state.ApprovalFingerprint = ""
case "plan.validate":
path := filepath.Join(artifactRoot, "plans", deliveryID+".source")
raw, readErr := os.ReadFile(path)
Expand All @@ -174,12 +175,14 @@ func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admissio
return nil, mutationErr
}
mutations = append(mutations, mutation)
state.ApprovalFingerprint = sha256Bytes(raw)
case "evidence.approval.revoke":
mutation, mutationErr := mutationFor(filepath.Join(artifactRoot, "approvals", deliveryID+".json"), nil, 0o644, false, true)
if mutationErr != nil {
return nil, mutationErr
}
mutations = append(mutations, mutation)
state.ApprovalFingerprint = ""
case "gate.build.record", "gate.test.record", "gate.review.record", "gate.change.record", "gate.journey.record":
revision, _ := admission.Parameters.Get("source_revision")
evidencePath, _ := admission.Parameters.Get("evidence_path")
Expand Down Expand Up @@ -299,6 +302,70 @@ func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admissio
return mutations, nil
}

// prepareWorkspacePlanTransfer carries runtime-owned plan artifacts into a
// newly cut worktree. A run binds the plan bytes before the cut, so the target
// worktree must observe those exact bytes rather than fall back to the inbox
// and accidentally select new intent.
func prepareWorkspacePlanTransfer(repositoryRoot, workspacePath, deliveryID, expectedPlanFingerprint, expectedApprovalFingerprint string) ([]ports.ResourceMutation, error) {
if expectedPlanFingerprint == "" || deliveryID == "" {
return nil, nil
}
if workspacePath == "" || expectedApprovalFingerprint == "" {
return nil, fmt.Errorf("workspace plan transfer requires destination and exact approval for a bound plan")
Comment on lines +313 to +314

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not require approval for every bound plan

Invariant: any workspace.cut prescribed from an admissible snapshot must pass deterministic preparation for that same snapshot. The catalog permits cutting with a draft, valid, or stale plan, but those states have a nonempty plan fingerprint and legitimately no approval fingerprint. Resolution therefore prescribes a targeted workspace.cut, preparation rejects it here, state remains unchanged, and retry repeats the refusal. Legacy approved schema-4 states accepted by this patch provide another witness because their approval fingerprint is empty. The patch introduces this disagreement by treating every bound plan as approved. A regression test should resolve and apply workspace.cut from a draft plan, and separately verify that a legacy/stale plan is refused during resolution or has a defined transfer path.

Confidence: 0.99

}
deliveryID, err := safeSegment(deliveryID, "delivery identity")
if err != nil {
return nil, err
}
sourceRoot := filepath.Join(repositoryRoot, ".boatstack")
destinationRoot := filepath.Join(workspacePath, ".boatstack")
planPath := filepath.Join(sourceRoot, "plans", deliveryID+".source")
planRaw, err := readRegularWorkspacePlanArtifact(planPath)
if err != nil {
return nil, err
}
if actual := sha256Bytes(planRaw); actual != expectedPlanFingerprint {
return nil, fmt.Errorf("workspace plan artifact fingerprint changed: got %s", actual)
}
approvalPath := filepath.Join(sourceRoot, "approvals", deliveryID+".json")
approvalRaw, err := readRegularWorkspacePlanArtifact(approvalPath)
if err != nil {
return nil, err
}
if actual := sha256Bytes(approvalRaw); actual != expectedApprovalFingerprint {
return nil, fmt.Errorf("workspace approval artifact fingerprint changed: got %s", actual)
}
var approval approvalArtifact
if err := decodeStrictArtifact(approvalRaw, &approval); err != nil || approval.SchemaVersion != 1 || approval.DeliveryID != deliveryID ||
approval.PlanFingerprint != expectedPlanFingerprint || approval.Actor == "" || approval.AdmissionID == "" || approval.ApprovedAt.IsZero() {
return nil, fmt.Errorf("workspace approval artifact does not bind the admitted plan")
}
planMutation, err := mutationFor(filepath.Join(destinationRoot, "plans", deliveryID+".source"), planRaw, 0o644, false, false)
if err != nil {
return nil, err
}
approvalMutation, err := mutationFor(filepath.Join(destinationRoot, "approvals", deliveryID+".json"), approvalRaw, 0o644, false, false)
Comment on lines +343 to +347

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Bind destination artifacts only after establishing their actual paths

Invariant: staged mutations must bind the actual prior resource and remain inside the admitted workspace. Prepare calls this while the destination does not yet exist, so mutationFor always records PriorExists=false. Then git worktree add can check out either a tracked artifact or a .boatstack/plans/approvals symlink before installation. The former is overwritten but receipted as a create and deleted on rollback; the latter redirects the write outside the workspace while verification still succeeds through the symlink. This is introduced by the new destination writes and can corrupt external files or produce false receipts/recovery data. A regression test should cut from a base ref containing both a tracked target and a symlinked artifact directory, asserting refusal, no external write, and exact prior facts.

Confidence: 0.99

if err != nil {
return nil, err
}
return []ports.ResourceMutation{planMutation, approvalMutation}, nil
}

func readRegularWorkspacePlanArtifact(path string) ([]byte, error) {
info, err := os.Lstat(path)
if err != nil {
return nil, fmt.Errorf("inspect workspace plan artifact %s: %w", path, err)
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("workspace plan artifact is not a regular file: %s", path)
}
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read workspace plan artifact %s: %w", path, err)
}
return raw, nil
}

func transitionUsesDeliveryArtifacts(id catalog.TransitionID) bool {
switch id {
case "plan.create", "plan.amend", "plan.validate", "plan.approve", "plan.approve-amendment",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func (b NativeBoundary) Execute(ctx context.Context, admission protocol.Admissio
if output, err := b.runner.CombinedOutput(ctx, layout.RepositoryRoot, "git", "push", "--set-upstream", "origin", preview.HeadRef); err != nil {
return ports.EffectResult{Settlement: ports.EffectUnknown, Detail: strings.TrimSpace(string(output))}, nil
}
if output, err := b.runner.CombinedOutput(ctx, layout.RepositoryRoot, "gh", "pr", "create", "--base", preview.BaseRef, "--head", preview.HeadRef, "--body-file", preview.BodyPath); err != nil {
if output, err := b.runner.CombinedOutput(ctx, layout.RepositoryRoot, "gh", "pr", "create", "--base", preview.BaseRef, "--head", preview.HeadRef, "--fill-first", "--body-file", preview.BodyPath); err != nil {
return ports.EffectResult{Settlement: ports.EffectUnknown, Detail: strings.TrimSpace(string(output))}, nil
}
case "publication.correct":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,54 @@ func TestPublicationPreviewRejectsFieldTamperingUnderAnOldFingerprint(t *testing
}
}

func TestPublicationExecutionUsesBoundBodyAndNoninteractiveTitle(t *testing.T) {
runner := &boundaryRunner{}
boundary, err := NewNativeBoundaryWithRunner(runner)
if err != nil {
t.Fatal(err)
}
transition, _ := testprogram.StandardRegistry().Lookup("publication.execute")
layout := writeBoundaryConfig(t, "go test ./...")
bodyPath := filepath.Join(layout.RepositoryRoot, "body.md")
body := []byte("reviewed body")
if err := os.WriteFile(bodyPath, body, 0o600); err != nil {
t.Fatal(err)
}
preview := publicationPreview{SchemaVersion: 1, DeliveryID: "delivery", BaseRef: "main", HeadRef: "feature", BodyPath: bodyPath, BodySHA256: sha256Bytes(body), CreatedAt: time.Unix(10, 0).UTC()}
identity := preview
identity.CreatedAt = time.Time{}
raw, err := json.Marshal(identity)
if err != nil {
t.Fatal(err)
}
preview.Fingerprint = sha256Bytes(raw)
previewPath := filepath.Join(layout.RepositoryRoot, ".boatstack", "publication", "delivery.preview.json")
if err := os.MkdirAll(filepath.Dir(previewPath), 0o700); err != nil {
t.Fatal(err)
}
encoded, err := encodeJSON(preview)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(previewPath, encoded, 0o600); err != nil {
t.Fatal(err)
}
admission := protocol.Admission{
Invocation: model.InvocationContext{Ref: "refs/heads/feature"},
Objective: model.Objective{DeliveryID: "delivery"},
Parameters: protocol.Parameters{{Name: "preview_fingerprint", Value: preview.Fingerprint}},
}
admission.RequiredCapabilities = catalog.RequiredCapabilities(transition)
admission.EffectiveCapabilities = admission.RequiredCapabilities
if _, err := boundary.Execute(context.Background(), admission, transition, layout, durable.State{}); err != nil {
t.Fatal(err)
}
want := []string{"pr", "create", "--base", "main", "--head", "feature", "--fill-first", "--body-file", bodyPath}
if runner.name != "gh" || strings.Join(runner.arguments, "\x00") != strings.Join(want, "\x00") {
t.Fatalf("publication command = %s %q, want gh %q", runner.name, runner.arguments, want)
}
}

func TestPublicationCorrectionRejectsBodyDriftBeforeProviderCall(t *testing.T) {
runner := &boundaryRunner{}
boundary, _ := NewNativeBoundaryWithRunner(runner)
Expand Down
7 changes: 7 additions & 0 deletions boatstack/internal/softwaredelivery/effects/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ func (d Driver) Prepare(ctx context.Context, admission protocol.Admission, trans
if err != nil {
return nil, err
}
if transition.ID == "workspace.cut" {
transferMutations, transferErr := prepareWorkspacePlanTransfer(layout.RepositoryRoot, next.WorkspacePath, admission.Objective.DeliveryID, next.PlanFingerprint, next.ApprovalFingerprint)
if transferErr != nil {
return nil, transferErr
}
mutations = append(mutations, transferMutations...)
}
if transitionSetsRuntimePin(transition.ID) || transition.ID == "catalog.reconcile" {
pinMutation, pinErr := prepareRuntimePinMutation(layout.RepositoryRoot, next)
if pinErr != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,87 @@ func TestPlanEffectRejectsSourceChangedAfterEntryBinding(t *testing.T) {
t.Fatalf("replaced plan created a managed effect: %v", statErr)
}
}

func TestWorkspacePlanTransferCopiesOnlyRegularRuntimeOwnedArtifacts(t *testing.T) {
repository := t.TempDir()
workspace := t.TempDir()
plan := "# Bound plan\n"
for path, contents := range map[string]string{
filepath.Join(repository, ".boatstack", "plans", "delivery-one.source"): plan,
filepath.Join(repository, ".boatstack", "approvals", "delivery-one.json"): `{"schema_version":1,"delivery_id":"delivery-one","plan_fingerprint":"` + sha256Bytes([]byte(plan)) + `","actor":"reviewer","admission_id":"adm-1","approved_at":"2026-01-01T00:00:00Z"}`,
} {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
approvalRaw, err := os.ReadFile(filepath.Join(repository, ".boatstack", "approvals", "delivery-one.json"))
if err != nil {
t.Fatal(err)
}
mutations, err := prepareWorkspacePlanTransfer(repository, workspace, "delivery-one", sha256Bytes([]byte(plan)), sha256Bytes(approvalRaw))
if err != nil {
t.Fatal(err)
}
if len(mutations) != 2 {
t.Fatalf("transfer mutations = %#v", mutations)
}
for _, mutation := range mutations {
if !strings.HasPrefix(mutation.Path, filepath.Join(workspace, ".boatstack")+string(filepath.Separator)) || !mutation.PriorExists && len(mutation.Target) == 0 {
t.Fatalf("invalid transfer mutation: %#v", mutation)
}
}
}

func TestWorkspacePlanTransferRejectsStaleOrMissingBoundArtifacts(t *testing.T) {
repository := t.TempDir()
workspace := t.TempDir()
planPath := filepath.Join(repository, ".boatstack", "plans", "delivery-one.source")
approvalPath := filepath.Join(repository, ".boatstack", "approvals", "delivery-one.json")
if err := os.MkdirAll(filepath.Dir(planPath), 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(approvalPath), 0o700); err != nil {
t.Fatal(err)
}
bound := []byte("# Bound plan\n")
if err := os.WriteFile(planPath, bound, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(approvalPath, []byte(`{"schema_version":1,"delivery_id":"delivery-one","plan_fingerprint":"`+sha256Bytes(bound)+`","actor":"reviewer","admission_id":"adm-1","approved_at":"2026-01-01T00:00:00Z"}`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(planPath, []byte("# Substituted plan\n"), 0o600); err != nil {
t.Fatal(err)
}
approvalRaw, err := os.ReadFile(approvalPath)
if err != nil {
t.Fatal(err)
}
mutations, err := prepareWorkspacePlanTransfer(repository, workspace, "delivery-one", sha256Bytes(bound), sha256Bytes(approvalRaw))
if err == nil || len(mutations) != 0 {
t.Fatalf("stale plan transfer = %#v, %v", mutations, err)
}
if _, err := os.Stat(filepath.Join(workspace, ".boatstack", "plans", "delivery-one.source")); !os.IsNotExist(err) {
t.Fatalf("stale plan created destination artifact: %v", err)
}
if err := os.WriteFile(planPath, bound, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(approvalPath, []byte(`{"schema_version":1,"delivery_id":"delivery-one","plan_fingerprint":"`+sha256Bytes(bound)+`","actor":"substitute","admission_id":"adm-1","approved_at":"2026-01-01T00:00:00Z"}`), 0o600); err != nil {
t.Fatal(err)
}
mutations, err = prepareWorkspacePlanTransfer(repository, workspace, "delivery-one", sha256Bytes(bound), sha256Bytes(approvalRaw))
if err == nil || len(mutations) != 0 {
t.Fatalf("substituted approval transfer = %#v, %v", mutations, err)
}
if err := os.Remove(approvalPath); err != nil {
t.Fatal(err)
}
mutations, err = prepareWorkspacePlanTransfer(repository, workspace, "delivery-one", sha256Bytes(bound), sha256Bytes(approvalRaw))
if err == nil || len(mutations) != 0 {
t.Fatalf("missing approval transfer = %#v, %v", mutations, err)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ func resetDeliveryState(state *durable.State) {
state.Delivery, state.Plan = model.DeliveryUninitialized, model.PlanAbsent
state.Workspace = model.WorkspaceAbsent
state.Publication, state.Verification = model.PublicationNone, model.VerificationUnverified
state.PlanFingerprint, state.PublicationID, state.PublicationURL, state.PreviewFingerprint = "", "", "", ""
state.PlanFingerprint, state.ApprovalFingerprint, state.PublicationID, state.PublicationURL, state.PreviewFingerprint = "", "", "", "", ""
state.WorkspaceBranch, state.WorkspacePath, state.WorkspaceBaseRef = "", "", ""
state.WorkspaceSourcePath, state.WorkspaceSourceID, state.WorkspaceSourceRef = "", "", ""
state.Gates = nil
Expand Down
8 changes: 4 additions & 4 deletions boatstack/internal/softwaredelivery/plant/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,19 +520,19 @@ func observeRepositoryArtifacts(layout ports.ControllerLayout, state durable.Sta
}
if state.Plan == model.PlanApproved || state.Plan == model.PlanLocked {
path := filepath.Join(layout.RepositoryRoot, ".boatstack", "approvals", deliveryID+".json")
evidence, _, exists, err := fileEvidence(path, "approval", now)
evidence, fingerprint, exists, err := fileEvidence(path, "approval", now)
if err != nil {
return plan, verification, terminal, nil, nil, err
}
planEvidence = append(planEvidence, evidence)
valid := exists
if exists {
valid := exists && state.ApprovalFingerprint != "" && fingerprint == state.ApprovalFingerprint
if valid {
raw, readErr := os.ReadFile(path)
if readErr != nil {
return plan, verification, terminal, nil, nil, readErr
}
var approval observedApproval
valid = decodeStrictJSON(raw, &approval) == nil && approval.SchemaVersion == 1 &&
valid = valid && decodeStrictJSON(raw, &approval) == nil && approval.SchemaVersion == 1 &&
approval.DeliveryID == deliveryID && approval.PlanFingerprint == state.PlanFingerprint &&
approval.Actor != "" && approval.AdmissionID != "" && !approval.ApprovedAt.IsZero()
}
Expand Down
45 changes: 45 additions & 0 deletions boatstack/internal/softwaredelivery/plant/observer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,51 @@ func TestDoubleStarMatchesRootAndNestedPaths(t *testing.T) {
}
}

func TestObserverMarksApprovalByteSubstitutionStale(t *testing.T) {
// control-law: an approval remains authoritative only while its exact admitted bytes remain present.
repository := t.TempDir()
planPath := filepath.Join(repository, ".boatstack", "plans", "delivery.source")
approvalPath := filepath.Join(repository, ".boatstack", "approvals", "delivery.json")
if err := os.MkdirAll(filepath.Dir(planPath), 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(approvalPath), 0o700); err != nil {
t.Fatal(err)
}
planRaw := []byte("# Approved plan\n")
approvalRaw := []byte(`{"schema_version":1,"delivery_id":"delivery","plan_fingerprint":"pending","actor":"reviewer","admission_id":"adm-1","approved_at":"2026-01-01T00:00:00Z"}`)
if err := os.WriteFile(planPath, planRaw, 0o600); err != nil {
t.Fatal(err)
}
_, planFingerprint, _, err := fileEvidence(planPath, "plan", time.Unix(1, 0).UTC())
if err != nil {
t.Fatal(err)
}
approvalRaw = []byte(`{"schema_version":1,"delivery_id":"delivery","plan_fingerprint":"` + planFingerprint + `","actor":"reviewer","admission_id":"adm-1","approved_at":"2026-01-01T00:00:00Z"}`)
if err := os.WriteFile(approvalPath, approvalRaw, 0o600); err != nil {
t.Fatal(err)
}
_, approvalFingerprint, _, err := fileEvidence(approvalPath, "approval", time.Unix(1, 0).UTC())
if err != nil {
t.Fatal(err)
}
state := durable.State{
Plan: model.PlanApproved, Verification: model.VerificationCurrent, Terminal: model.TerminalNonterminal,
Objective: model.Objective{ID: "objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"},
PlanFingerprint: planFingerprint, ApprovalFingerprint: approvalFingerprint,
}
if err := os.WriteFile(approvalPath, []byte(`{"schema_version":1,"delivery_id":"delivery","plan_fingerprint":"`+planFingerprint+`","actor":"substitute","admission_id":"adm-1","approved_at":"2026-01-01T00:00:00Z"}`), 0o600); err != nil {
t.Fatal(err)
}
plan, _, terminal, _, _, err := observeRepositoryArtifacts(ports.ControllerLayout{RepositoryRoot: repository}, state, time.Unix(2, 0).UTC())
if err != nil {
t.Fatal(err)
}
if plan != model.PlanStale || terminal != model.TerminalStale {
t.Fatalf("substituted approval observed as plan=%s terminal=%s", plan, terminal)
}
}

func TestObserverDerivesHighRiskChangeFromCommittedAndWorkingTreePaths(t *testing.T) {
repository := t.TempDir()
runGit(t, repository, "init", "-q")
Expand Down
Loading
Loading