Skip to content
Open
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
91717c9
docs(executors): specify native worker capabilities for executor plugins
nova28 Oct 27, 2026
584659c
docs(executors): specify medium-term plugin needs M1-M8 with capabili…
nova28 Oct 28, 2026
3771ee9
docs(executors): close plugin skeleton findings in the executor plugi…
nova28 Oct 28, 2026
ba065eb
docs(executors): close spec review round 1 findings for executor plug…
nova28 Oct 28, 2026
abf3623
docs(executors): close spec review round 2 findings for executor plug…
nova28 Oct 28, 2026
eda239d
docs(executors): close spec review round 3 findings for executor plug…
nova28 Oct 28, 2026
c111806
docs(executors): index the placement requirements file
nova28 Oct 28, 2026
801c8eb
docs(executors): fold round 3 shadow paths into the executor plugin s…
nova28 Oct 28, 2026
9d400a1
docs(executors): close spec review round 4 findings for executor plug…
nova28 Oct 28, 2026
ef5e9fc
docs(executors): cut shared environments from the executor plugin cyc…
nova28 Oct 28, 2026
aa28a2d
docs(executors): cut profile migration and close spec review round 6 …
nova28 Oct 28, 2026
dd60466
docs(executors): close spec review round 7, cut admission classes to …
nova28 Oct 28, 2026
4880a25
docs(executors): freeze slice 1, close round 8 capability gate and re…
nova28 Oct 28, 2026
9aeb716
docs(executors): record native worker capability decisions D1 D2 D3 D6
nova28 Oct 28, 2026
f327e04
feat(plugins): add host capability registry and manifest capability s…
nova28 Oct 28, 2026
979a99e
feat(plugins): negotiate host capabilities through the profile valida…
nova28 Oct 28, 2026
2768734
feat(plugins): gate allocation on capability agreement and bound exec…
nova28 Oct 28, 2026
6c04562
feat(plugins): fence state readability between activation and launch …
nova28 Oct 28, 2026
2962b6d
feat(plugins): add profile schema keywords, typed values and secret r…
nova28 Oct 28, 2026
94cb5d1
feat(plugins): record a validated, immutable workspace descriptor per…
nova28 Oct 28, 2026
f934ec7
feat(plugins): accept claim-scoped destroy and recover checkpoints wi…
nova28 Oct 28, 2026
d656aa4
feat(plugins): seal and drain the agent runtime before destroying a p…
nova28 Oct 28, 2026
8df3348
feat(plugins): publish a recovered environment ready under the recove…
nova28 Oct 28, 2026
6e7ed27
feat(plugins): resume a durable bootstrap after a backend restart und…
nova28 Oct 28, 2026
21e1bd4
docs(plans): record WO 04 and 05 in the native workers build record
nova28 Oct 28, 2026
baee393
feat(plugins): probe API reachability for provider-forwarded runtime …
nova28 Oct 28, 2026
985d36a
docs(plans): record WO 06 API forwarding half in the native workers b…
nova28 Oct 28, 2026
0132226
feat(plugins): carry agentctl traffic over a host-opened provider tunnel
nova28 Oct 28, 2026
5a52f4d
docs(plans): record WO 06 tunnel half in the native workers build record
nova28 Oct 28, 2026
f480d6e
feat(plugins): hand repository intent to provider-prepared workspaces…
nova28 Oct 28, 2026
050b08b
docs(plans): record WO 07 in the native workers build record
nova28 Oct 28, 2026
cd5ee1e
feat(plugins): add e2e-only crash barriers and publish pending ready …
nova28 Oct 28, 2026
56fe7e1
docs(plans): record WO 08 in the native workers build record
nova28 Oct 28, 2026
cb10f88
test(plugins): seed the readability lock test with the PostgreSQL tas…
nova28 Oct 28, 2026
2d2359a
test(plugins): give the readability lock test a checker and isolated …
nova28 Oct 28, 2026
a7a3719
test(web): cover profile schema default hint and length limits in e2e
nova28 Oct 28, 2026
f2022bf
docs(plans): record the PostgreSQL run for slice 1 in the native work…
nova28 Oct 28, 2026
7961a96
fix(plugins): gate typed envelopes, order envelope locks, and re-drai…
nova28 Oct 28, 2026
3335b41
fix(plugins): keep the auth token secret ref and record cleanup reque…
nova28 Oct 28, 2026
35a99d2
test(plugins): pin tunnel close before the provider destroy
nova28 Oct 28, 2026
76372e2
test(plugins): cover the capability gate through Install and upgrade
nova28 Oct 28, 2026
37c41bc
test(plugins): cover changed-intent conflict and a partial-stop shutdown
nova28 Oct 28, 2026
38c943e
fix(plugins): keep a recorded cleanup request across later checkpoints
nova28 Oct 28, 2026
3995c12
test(plugins): cover recovery adopting a claim a crashed holder left
nova28 Oct 28, 2026
a500f3d
fix(agentctl): require a control credential for the host API probe
nova28 Oct 28, 2026
5d87678
docs(plans): record Build round 1 results for native workers
nova28 Oct 28, 2026
2f2b7e2
fix(plugins): lock the task row before the execution row when recordi…
nova28 Oct 28, 2026
5dd50ee
fix(plugins): import the relocated plugin SDK module path
nova28 Oct 28, 2026
3aabc4f
fix(plugins): carry the recorded profile after provision and accept e…
nova28 Oct 29, 2026
87d1079
fix(plugins): drain on archive, retry cleanup from the stored record …
nova28 Oct 29, 2026
421bc6c
fix(plugins): leave archive drain to the cleanup job, defer it during…
nova28 Oct 29, 2026
0371b9d
docs(specs): record cleanup-job archive ownership and required recove…
nova28 Oct 29, 2026
b209646
feat(executors): accept loopback and file remotes for plugin executor…
nova28 Oct 29, 2026
fbec208
fix(plugins): destroy a provisioned resource the launch cannot record…
nova28 Oct 29, 2026
b42b46f
fix(plugins): destroy an unrecordable provisioned resource whether th…
nova28 Oct 29, 2026
d43da40
fix(plugins): report a provisioned resource that was neither removed …
nova28 Oct 29, 2026
cd984cb
docs(specs): state exactly what the host guarantees about revealed pr…
nova28 Oct 29, 2026
0428eef
fix(plugins): hand an unrecordable launch resource to the cleanup ins…
nova28 Oct 29, 2026
2ad8c40
test(gitcredentials): use a placeholder host for the non-loopback e2e…
nova28 Oct 29, 2026
04210f1
fix(repoclone): clone e2e-local loopback and file remotes without cre…
nova28 Oct 29, 2026
986be23
fix(plugins): record a provisioned-but-unrecorded executor resource s…
nova28 Oct 29, 2026
deaa437
fix(executor): accept a loopback http Git credential broker for e2e-l…
nova28 Oct 29, 2026
25d98f3
fix(plugins): destroy a handed-off unrecorded resource under the laun…
nova28 Oct 29, 2026
16d69f7
fix(plugins): settle every plugin resource cleanup path and fence cla…
nova28 Oct 29, 2026
05ef462
fix(plugins): log every destroy and let e2e-local repositories skip c…
nova28 Oct 29, 2026
f084d26
fix(plugins): accept e2e-local file locators in agentctl and log clai…
nova28 Oct 29, 2026
8962b8b
fix(plugins): accept claim checkpoints for handed-off resources and l…
nova28 Oct 29, 2026
8273a0e
fix(lifecycle): bound the graceful agentctl stop of plugin_remote run…
nova28 Oct 30, 2026
c100d78
fix(lifecycle): do not wait on busy agentctl leases when stopping plu…
nova28 Oct 30, 2026
f257db1
fix(lifecycle): keep the ErrCancelEscalated doc comment on its var
nova28 Oct 30, 2026
e1c4f65
docs(agents): specify node launch wrapper
nova28 Oct 30, 2026
2e9d23f
feat(agentctl): apply an operator-owned node launch wrapper to every …
nova28 Oct 30, 2026
24f303c
docs(agents): drop the sticky exception and fail the backend passthro…
nova28 Oct 30, 2026
0e39652
feat(agentctl): drop the sticky ancestor exception and fail the backe…
nova28 Oct 30, 2026
114a85b
fix(lifecycle): read the peer's own delivery stream when recovering a…
nova28 Oct 30, 2026
f7860bc
fix(lifecycle): give plugin_remote agentctl a durable delivery journa…
nova28 Oct 30, 2026
4431ee1
feat(plugins): probe the worker-side API URL of a forwarded launch
nova28 Oct 30, 2026
f70b186
fix(plugins): persist the drain outcome of a failed launch from any s…
nova28 Oct 31, 2026
dd5007f
feat(agentctl): let a node launch wrapper reference an operator-suppl…
nova28 Oct 31, 2026
9f3c1ce
docs(agents): the node wrapper allocation directory must be private t…
nova28 Oct 31, 2026
14853d2
test(agentctl): use a scheduler-neutral decoy variable in the allocat…
nova28 Oct 31, 2026
06755de
test(agentctl): pin the pre-launch surface and the token-reference or…
nova28 Oct 31, 2026
c7e889b
feat(plugins): register drain_receipt version 2 and specify its meaning
nova28 Oct 31, 2026
b815d79
feat(lifecycle): send not_attempted without a handshake under drain_r…
nova28 Oct 31, 2026
f425b5a
feat(lifecycle): record whether the host ever held an agentctl creden…
nova28 Oct 31, 2026
9653ec9
test(lifecycle): pin where credential_recorded starts and that a prov…
nova28 Oct 31, 2026
981fdea
fix(lifecycle): let archive cleanup reach the plugin destroy after a …
nova28 Oct 31, 2026
2cbc514
Merge remote-tracking branch 'origin/main' into fix/pr4934-merge-main
nova28 Oct 31, 2026
5b7632a
docs(executors): complete work order references for the documentation…
nova28 Oct 31, 2026
f274c85
test(agentctl): allow the pure allocation-directory checks in the sta…
nova28 Oct 31, 2026
8f1fcb7
feat(agent): launch managed npm agents from a preinstalled command
nova28 Oct 31, 2026
6ca6b15
docs(executors): specify the preinstalled agent command override
nova28 Oct 31, 2026
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
2 changes: 1 addition & 1 deletion apps/backend/cmd/agentctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func run(cfg *config.Config, log *logger.Logger) {
}
// Shutdown all instances first so any final ACP frames they emit during
// teardown are still captured by the (still-live) debug writers.
if err := instMgr.Shutdown(ctx); err != nil {
if err := controlServer.ShutdownInstances(ctx); err != nil {
log.Error("error shutting down instance manager", zap.Error(err))
}
// Then flush + close all debug-log writers as the final logging step.
Expand Down
85 changes: 84 additions & 1 deletion apps/backend/cmd/plugin-fixture/executor_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ func (p *fixturePlugin) DestroyExecutorEnvironment(ctx context.Context, req *plu
if err := ctx.Err(); err != nil {
return nil, err
}
if refusal := fixtureDrainRefusal(req); refusal != nil {
return &pluginsdk.DestroyExecutorEnvironmentResponse{Error: refusal}, nil
}
if refusal := p.checkpointFixtureCleanup(ctx, req); refusal != nil {
return &pluginsdk.DestroyExecutorEnvironmentResponse{Error: refusal}, nil
}
if consumeFixtureFailureBarrier(p.dataDir, req.GetContext().GetOperationId(), fixtureBarrierUnconfirmedCleanupOnce) {
return &pluginsdk.DestroyExecutorEnvironmentResponse{Error: fixtureProviderError("cleanup_unconfirmed", "provider.fixture.cleanupUnconfirmed")}, nil
}
Expand Down Expand Up @@ -232,7 +238,7 @@ func (p *fixturePlugin) allocateFixtureExecutor(request *pluginsdk.ExecutorProvi
}
if existing, ok := p.executorState.Environments[request.GetOperationId()]; ok {
if existing.EnvironmentID != request.GetEnvironmentId() || existing.InputDigest != request.GetInputDigest() {
return fixtureExecutorResource{}, errors.New("plugin-fixture: operation identity changed after allocation")
return fixtureExecutorResource{}, pluginsdk.CategorizeActionError(pluginsdk.ActionErrorConflict, errors.New("plugin-fixture: operation identity changed after allocation"))
}
return existing, nil
}
Expand Down Expand Up @@ -558,3 +564,80 @@ func consumeFixtureFailureBarrier(dataDir, operationID, barrier string) bool {
_ = file.Close()
return true
}

// checkpointFixtureCleanup records cleanup_progress through the host before the
// destroy mutates anything. A destroy under a cleanup claim whose checkpoint the
// host refuses fails closed, as a real provider's does.
func (p *fixturePlugin) checkpointFixtureCleanup(ctx context.Context, req *pluginsdk.DestroyExecutorEnvironmentRequest) *pluginsdk.ExecutorProviderError {
if req.GetCleanupClaim() == "" || !fixtureDeclaresCapability(fixtureCapabilityDestroyCheckpoints) {
return nil
}
host, ok := p.Host().(pluginsdk.ExecutorProviderHost)
if !ok {
return fixtureProviderError("cleanup_checkpoint_refused", "provider.fixture.cleanupCheckpointRefused")
}
if _, err := host.ExecutorProvider().CheckpointExecutorResource(ctx, &pluginsdk.CheckpointExecutorResourceRequest{
Context: req.GetContext(), Resource: req.GetResource(), Phase: "cleanup_progress", Sequence: 1,
Claim: req.GetCleanupClaim(),
}); err != nil {
return fixtureProviderError("cleanup_checkpoint_refused", "provider.fixture.cleanupCheckpointRefused")
}
return nil
}

const (
fixtureCapabilitiesEnv = "KANDEV_PLUGIN_FIXTURE_CAPABILITIES"
fixtureCapabilityDestroyCheckpoints = "destroy_checkpoints"
fixtureCapabilityDrainReceipt = "drain_receipt"
fixtureLaunchFailedCleanupReason = "launch_failed"
fixtureDrainOutcomeDrained = "drained"
fixtureDrainOutcomeNotAttempted = "not_attempted"
fixtureDefaultDeclaredCapabilityList = fixtureCapabilityDestroyCheckpoints + "," + fixtureCapabilityDrainReceipt
)

// fixtureDeclaresCapability reports whether the fixture acts as a provider that
// requires the named host capability. The set is configuration, so a test can
// run the fixture as a provider that declares none.
func fixtureDeclaresCapability(name string) bool {
list, ok := os.LookupEnv(fixtureCapabilitiesEnv)
if !ok {
list = fixtureDefaultDeclaredCapabilityList
}
for _, declared := range strings.Split(list, ",") {
if strings.TrimSpace(declared) == name {
return true
}
}
return false
}

// fixtureDrainRefusal applies the destroy-time drain rules of a provider that
// requires drain_receipt: not_attempted proves "no runtime" only under the
// launch cleanup reason, and a drained outcome carries a receipt bound to the
// cleanup claim of this destroy.
func fixtureDrainRefusal(req *pluginsdk.DestroyExecutorEnvironmentRequest) *pluginsdk.ExecutorProviderError {
if !fixtureDeclaresCapability(fixtureCapabilityDrainReceipt) {
return nil
}
refuse := func() *pluginsdk.ExecutorProviderError {
return fixtureProviderError("drain_evidence_refused", "provider.fixture.drainEvidenceRefused")
}
switch req.GetDrainOutcome() {
case fixtureDrainOutcomeDrained:
receipt := req.GetDrainReceipt()
if receipt == nil || receipt.GetReceiptId() == "" || receipt.GetCleanupClaim() != req.GetCleanupClaim() {
return refuse()
}
case fixtureDrainOutcomeNotAttempted:
if req.GetCleanupReason() != fixtureLaunchFailedCleanupReason || req.GetDrainReceipt() != nil {
return refuse()
}
case "unreachable", "uncertain":
if req.GetDrainReceipt() != nil {
return refuse()
}
default:
return refuse()
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"context"
"errors"
"testing"

"github.com/kdlbs/kandev/pluginsdk"
"github.com/stretchr/testify/require"
)

// checkpointingHost is a Host whose executor-provider callbacks record the
// checkpoints a provider issues, refusing them when asked.
type checkpointingHost struct {
pluginsdk.Host
checkpoints []*pluginsdk.CheckpointExecutorResourceRequest
refuse error
onCheckpoint func()
}

func (h *checkpointingHost) ExecutorProvider() pluginsdk.ExecutorProviderHostAPI { return h }

func (h *checkpointingHost) CheckpointExecutorResource(_ context.Context, req *pluginsdk.CheckpointExecutorResourceRequest) (*pluginsdk.CheckpointExecutorResourceResponse, error) {
h.checkpoints = append(h.checkpoints, req)
if h.onCheckpoint != nil {
h.onCheckpoint()
}
if h.refuse != nil {
return nil, h.refuse
}
return &pluginsdk.CheckpointExecutorResourceResponse{Accepted: true}, nil
}

func (h *checkpointingHost) ReportExecutorProgress(context.Context, *pluginsdk.ReportExecutorProgressRequest) (*pluginsdk.ReportExecutorProgressResponse, error) {
return &pluginsdk.ReportExecutorProgressResponse{}, nil
}

func (h *checkpointingHost) ReadExecutorRuntimeArtifact(context.Context, *pluginsdk.ReadExecutorRuntimeArtifactRequest, func(*pluginsdk.ExecutorRuntimeArtifactChunk) error) error {
return nil
}

func provisionForCleanup(t *testing.T, plugin *fixturePlugin) (*pluginsdk.ExecutorProviderRequestContext, *pluginsdk.ExecutorResourceDescriptor) {
t.Helper()
profile := &pluginsdk.ExecutorProfileSnapshot{ProfileId: "profile-1", Config: map[string]string{"region": "eu-west-1"}}
ctx := &pluginsdk.ExecutorProviderRequestContext{
PluginId: "kandev-plugin-e2e", InstallationId: "installation-1", ProviderKey: executorProviderKey,
ContractVersion: 1, EnvironmentId: "environment-1", OperationId: "operation-1", InputDigest: "digest-1",
}
provisioned, err := plugin.ProvisionExecutorEnvironment(context.Background(), &pluginsdk.ProvisionExecutorEnvironmentRequest{Context: ctx, Profile: profile})
require.NoError(t, err)
return ctx, provisioned.GetResource()
}

// A destroy under a cleanup claim checkpoints cleanup_progress through the host
// before it mutates anything, as a real provider must.
func TestFixtureDestroyCheckpointsCleanupProgressBeforeMutating(t *testing.T) {
plugin := newFixturePluginAt(t.TempDir())
t.Cleanup(func() { require.NoError(t, plugin.Close()) })
ctx, resource := provisionForCleanup(t, plugin)
host := &checkpointingHost{}
plugin.SetHost(host)
host.onCheckpoint = func() {
_, found, err := plugin.fixtureResourceByHandle(resource.GetResourceHandle())
require.NoError(t, err)
require.True(t, found, "the resource was destroyed before the checkpoint")
}

destroyed, err := plugin.DestroyExecutorEnvironment(context.Background(), &pluginsdk.DestroyExecutorEnvironmentRequest{
Context: ctx, Resource: resource, CleanupClaim: "operation-1:cleanup",
CleanupReason: "launch_failed", DrainOutcome: "not_attempted",
})
require.NoError(t, err)
require.True(t, destroyed.GetConfirmedAbsent())
require.Len(t, host.checkpoints, 1)
require.Equal(t, "cleanup_progress", host.checkpoints[0].GetPhase())
require.EqualValues(t, 1, host.checkpoints[0].GetSequence())
require.Equal(t, resource.GetResourceHandle(), host.checkpoints[0].GetResource().GetResourceHandle())
_, found, err := plugin.fixtureResourceByHandle(resource.GetResourceHandle())
require.NoError(t, err)
require.False(t, found)
}

// A refused checkpoint fails the destroy closed: nothing is mutated and the
// resource is not reported absent.
func TestFixtureDestroyFailsClosedWhenTheHostRefusesTheCheckpoint(t *testing.T) {
plugin := newFixturePluginAt(t.TempDir())
t.Cleanup(func() { require.NoError(t, plugin.Close()) })
ctx, resource := provisionForCleanup(t, plugin)
plugin.SetHost(&checkpointingHost{refuse: errors.New("cleanup claim not found")})

destroyed, err := plugin.DestroyExecutorEnvironment(context.Background(), &pluginsdk.DestroyExecutorEnvironmentRequest{
Context: ctx, Resource: resource, CleanupClaim: "operation-1:cleanup",
CleanupReason: "launch_failed", DrainOutcome: "not_attempted",
})
require.NoError(t, err)
require.False(t, destroyed.GetConfirmedAbsent())
require.Equal(t, "cleanup_checkpoint_refused", destroyed.GetError().GetCode())
_, found, err := plugin.fixtureResourceByHandle(resource.GetResourceHandle())
require.NoError(t, err)
require.True(t, found, "the resource was mutated without an accepted checkpoint")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"context"
"testing"

"github.com/kdlbs/kandev/pluginsdk"
"github.com/stretchr/testify/require"
)

func destroyWithEvidence(t *testing.T, mutate func(*pluginsdk.DestroyExecutorEnvironmentRequest)) *pluginsdk.DestroyExecutorEnvironmentResponse {
t.Helper()
plugin := newFixturePluginAt(t.TempDir())
t.Cleanup(func() { require.NoError(t, plugin.Close()) })
ctx, resource := provisionForCleanup(t, plugin)
plugin.SetHost(&checkpointingHost{})
request := &pluginsdk.DestroyExecutorEnvironmentRequest{Context: ctx, Resource: resource, CleanupClaim: "claim-1", CleanupReason: "task_cleanup"}
mutate(request)
response, err := plugin.DestroyExecutorEnvironment(context.Background(), request)
require.NoError(t, err)
return response
}

// A provider that requires drain_receipt accepts not_attempted only as the
// no-runtime proof of a failed launch, and a receipt only for its own claim.
func TestFixtureDestroyAppliesTheDrainEvidenceRules(t *testing.T) {
receipt := func(claim string) *pluginsdk.ExecutorDrainReceipt {
return &pluginsdk.ExecutorDrainReceipt{ReceiptId: "receipt-1", CleanupClaim: claim, RuntimeIdentity: "runtime-1"}
}
cases := []struct {
name string
mutate func(*pluginsdk.DestroyExecutorEnvironmentRequest)
refused bool
}{
{"no outcome", func(*pluginsdk.DestroyExecutorEnvironmentRequest) {}, true},
{"not attempted under task cleanup", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) { r.DrainOutcome = "not_attempted" }, true},
{"not attempted under launch failure", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) {
r.DrainOutcome, r.CleanupReason = "not_attempted", "launch_failed"
}, false},
{"drained without a receipt", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) { r.DrainOutcome = "drained" }, true},
{"drained with another claim's receipt", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) {
r.DrainOutcome, r.DrainReceipt = "drained", receipt("claim-0")
}, true},
{"drained with this claim's receipt", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) {
r.DrainOutcome, r.DrainReceipt = "drained", receipt("claim-1")
}, false},
{"uncertain", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) { r.DrainOutcome = "uncertain" }, false},
{"unreachable", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) { r.DrainOutcome = "unreachable" }, false},
{"uncertain with a receipt", func(r *pluginsdk.DestroyExecutorEnvironmentRequest) {
r.DrainOutcome, r.DrainReceipt = "uncertain", receipt("claim-1")
}, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
response := destroyWithEvidence(t, tc.mutate)
if tc.refused {
require.Equal(t, "drain_evidence_refused", response.GetError().GetCode())
require.False(t, response.GetConfirmedAbsent())
return
}
require.Nil(t, response.GetError())
require.True(t, response.GetConfirmedAbsent())
})
}
}

// A provider configured without the capabilities neither checkpoints nor
// demands drain evidence.
func TestFixtureWithoutDeclaredCapabilitiesAcceptsABareDestroy(t *testing.T) {
t.Setenv(fixtureCapabilitiesEnv, "")
response := destroyWithEvidence(t, func(*pluginsdk.DestroyExecutorEnvironmentRequest) {})
require.Nil(t, response.GetError())
require.True(t, response.GetConfirmedAbsent())
}
26 changes: 26 additions & 0 deletions apps/backend/cmd/plugin-fixture/executor_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func TestPluginExecutorFixtureContract(t *testing.T) {

destroyed, err := plugin.DestroyExecutorEnvironment(context.Background(), &pluginsdk.DestroyExecutorEnvironmentRequest{
Context: ctx, Resource: provisioned.GetResource(),
CleanupReason: "launch_failed", DrainOutcome: "not_attempted",
})
require.NoError(t, err)
require.True(t, destroyed.GetConfirmedAbsent())
Expand Down Expand Up @@ -179,13 +180,15 @@ func TestPluginExecutorFixtureFailureBarriersAreRetryable(t *testing.T) {
t.Setenv(fixtureExecutorBarrierEnv, fixtureBarrierUnconfirmedCleanupOnce)
delayed, err := plugin.DestroyExecutorEnvironment(context.Background(), &pluginsdk.DestroyExecutorEnvironmentRequest{
Context: ctx, Resource: provisioned.GetResource(),
CleanupReason: "launch_failed", DrainOutcome: "not_attempted",
})
require.NoError(t, err)
require.False(t, delayed.GetConfirmedAbsent())
require.Equal(t, "cleanup_unconfirmed", delayed.GetError().GetCode())

cleaned, err := plugin.DestroyExecutorEnvironment(context.Background(), &pluginsdk.DestroyExecutorEnvironmentRequest{
Context: ctx, Resource: provisioned.GetResource(),
CleanupReason: "launch_failed", DrainOutcome: "not_attempted",
})
require.NoError(t, err)
require.True(t, cleaned.GetConfirmedAbsent())
Expand Down Expand Up @@ -216,3 +219,26 @@ func fixtureHTTPSRequest(t *testing.T, plugin *fixturePlugin, lease *pluginsdk.E
require.NoError(t, err)
return string(body)
}

func TestPluginExecutorFixtureSameOperationSameInputReusesAndChangedInputConflicts(t *testing.T) {
plugin := newFixturePluginAt(t.TempDir())
t.Cleanup(func() { require.NoError(t, plugin.Close()) })
profile := &pluginsdk.ExecutorProfileSnapshot{ProfileId: "profile-1", Config: map[string]string{"region": "eu-west-1"}}
request := func(digest string) *pluginsdk.ProvisionExecutorEnvironmentRequest {
return &pluginsdk.ProvisionExecutorEnvironmentRequest{Profile: profile, Context: &pluginsdk.ExecutorProviderRequestContext{
PluginId: "kandev-plugin-e2e", InstallationId: "installation-1", ProviderKey: executorProviderKey,
ContractVersion: 1, EnvironmentId: "environment-1", OperationId: "operation-1", InputDigest: digest,
}}
}
_, err := plugin.ProvisionExecutorEnvironment(context.Background(), request("digest-intent-a"))
require.NoError(t, err)
_, err = plugin.ProvisionExecutorEnvironment(context.Background(), request("digest-intent-a"))
require.NoError(t, err)
require.Equal(t, 1, plugin.executorAllocationCount(), "a retry with the same intent must not allocate again")

_, err = plugin.ProvisionExecutorEnvironment(context.Background(), request("digest-intent-b"))
var actionErr *pluginsdk.ActionError
require.ErrorAs(t, err, &actionErr)
require.Equal(t, pluginsdk.ActionErrorConflict, actionErr.Code)
require.Equal(t, 1, plugin.executorAllocationCount())
}
3 changes: 3 additions & 0 deletions apps/backend/cmd/plugin-fixture/fixture-package/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ executor_providers:
image:
type: string
title: Image
default: fixture-agent:latest
minLength: 3
maxLength: 32
workspace_label:
type: string
title: Environment label with additional context for long names
Expand Down
Loading
Loading