277677f330
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
156 lines
6.5 KiB
Go
156 lines
6.5 KiB
Go
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"go.uber.org/zap/zaptest/observer"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/plugins/manifest"
|
|
taskmodels "github.com/kandev/kandev/internal/task/models"
|
|
taskservice "github.com/kandev/kandev/internal/task/service"
|
|
"github.com/kandev/kandev/pkg/pluginsdk"
|
|
)
|
|
|
|
// newObservedLogger wires a *logger.Logger backed by a zaptest/observer
|
|
// core, so a test can assert on the fields of a specific log record.
|
|
func newObservedLogger(t *testing.T) (*logger.Logger, *observer.ObservedLogs) {
|
|
t.Helper()
|
|
core, logs := observer.New(zapcore.DebugLevel)
|
|
log, err := logger.NewFromZap(zap.New(core))
|
|
require.NoError(t, err)
|
|
return log, logs
|
|
}
|
|
|
|
// TestAttachDependencies_LogsWhenTaskDataSourceIsNil proves the
|
|
// AC-PLUGINS-TASK-DEPS-002.4 diagnostic fires for the "derivation source
|
|
// absent" failure class, carrying plugin, instance, resource type,
|
|
// operation, and result code — no task title, id, or edge content.
|
|
func TestAttachDependencies_LogsWhenTaskDataSourceIsNil(t *testing.T) {
|
|
log, logs := newObservedLogger(t)
|
|
host := &pluginHost{
|
|
pluginID: "plugin-1",
|
|
capabilities: manifest.Capabilities{APIRead: []string{"tasks"}},
|
|
log: log,
|
|
}
|
|
tasks := []pluginsdk.Task{{ID: "task-1"}}
|
|
|
|
err := host.attachDependencies(context.Background(), tasks, []*taskmodels.Task{{ID: "task-1"}}, false, "GetTask")
|
|
require.NoError(t, err)
|
|
|
|
entries := logs.All()
|
|
require.Len(t, entries, 1, "exactly one record for this derivation attempt")
|
|
fields := entries[0].ContextMap()
|
|
require.Equal(t, "plugin-1", fields["plugin_id"])
|
|
require.Equal(t, "plugin-1", fields["instance_id"], "a gRPC host has no separate instance id, so it falls back to plugin id")
|
|
require.Equal(t, "task", fields["resource_type"])
|
|
require.Equal(t, "GetTask", fields["operation"])
|
|
require.Equal(t, "source_unavailable", fields["result_code"])
|
|
}
|
|
|
|
// TestAttachDependencies_UsesCanvasInstanceIDWhenSet proves a canvas-scoped
|
|
// host logs its real instance id rather than falling back to plugin id.
|
|
func TestAttachDependencies_UsesCanvasInstanceIDWhenSet(t *testing.T) {
|
|
log, logs := newObservedLogger(t)
|
|
host := &pluginHost{
|
|
pluginID: "plugin-1",
|
|
instanceID: "instance-9",
|
|
capabilities: manifest.Capabilities{APIRead: []string{"tasks"}},
|
|
log: log,
|
|
}
|
|
tasks := []pluginsdk.Task{{ID: "task-1"}}
|
|
|
|
err := host.attachDependencies(context.Background(), tasks, []*taskmodels.Task{{ID: "task-1"}}, false, "GetWebAppTask")
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, logs.All(), 1)
|
|
require.Equal(t, "instance-9", logs.All()[0].ContextMap()["instance_id"])
|
|
}
|
|
|
|
// TestAttachDependencies_LogsOnceWhenDerivationWithheldAcrossManyTasks
|
|
// proves the log unit is the derivation attempt, not the withheld task: a
|
|
// batch-wide failure (every task's BlockedReason coming back "unknown")
|
|
// still produces exactly one record.
|
|
func TestAttachDependencies_LogsOnceWhenDerivationWithheldAcrossManyTasks(t *testing.T) {
|
|
log, logs := newObservedLogger(t)
|
|
d := newTestDataHost(manifest.Capabilities{APIRead: []string{"tasks"}})
|
|
d.host.log = log
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-1": {Blocked: true, BlockedReason: taskservice.BlockedReasonUnknown},
|
|
"task-2": {Blocked: true, BlockedReason: taskservice.BlockedReasonUnknown},
|
|
"task-3": {Blocked: true, BlockedReason: taskservice.BlockedReasonUnknown},
|
|
}
|
|
tasks := []pluginsdk.Task{{ID: "task-1"}, {ID: "task-2"}, {ID: "task-3"}}
|
|
models := []*taskmodels.Task{{ID: "task-1"}, {ID: "task-2"}, {ID: "task-3"}}
|
|
|
|
err := d.host.attachDependencies(context.Background(), tasks, models, true, "ListTasks")
|
|
require.NoError(t, err)
|
|
|
|
entries := logs.All()
|
|
require.Len(t, entries, 1, "one record per derivation attempt, not one per withheld task")
|
|
require.Equal(t, "derivation_withheld", entries[0].ContextMap()["result_code"])
|
|
}
|
|
|
|
// TestAttachDependencies_NoLogOnSuccessfulDerivation proves an ordinary
|
|
// non-withheld read logs nothing.
|
|
func TestAttachDependencies_NoLogOnSuccessfulDerivation(t *testing.T) {
|
|
log, logs := newObservedLogger(t)
|
|
d := newTestDataHost(manifest.Capabilities{APIRead: []string{"tasks"}})
|
|
d.host.log = log
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-1": {Blocked: true, BlockedReason: taskservice.BlockedReasonPending},
|
|
}
|
|
tasks := []pluginsdk.Task{{ID: "task-1"}}
|
|
|
|
err := d.host.attachDependencies(context.Background(), tasks, []*taskmodels.Task{{ID: "task-1"}}, false, "GetTask")
|
|
require.NoError(t, err)
|
|
require.Empty(t, logs.All())
|
|
}
|
|
|
|
// TestAttachDependencies_NoLogWhenCapabilityDenied proves a caller lacking
|
|
// api_read:tasks is not logged as a failed derivation attempt: per the
|
|
// design, withholding for lack of capability never reaches the derivation
|
|
// at all, so there is nothing to report as having failed.
|
|
func TestAttachDependencies_NoLogWhenCapabilityDenied(t *testing.T) {
|
|
log, logs := newObservedLogger(t)
|
|
d := newTestDataHost(manifest.Capabilities{})
|
|
d.host.log = log
|
|
tasks := []pluginsdk.Task{{ID: "task-1"}}
|
|
|
|
err := d.host.attachDependencies(context.Background(), tasks, []*taskmodels.Task{{ID: "task-1"}}, false, "GetTask")
|
|
require.NoError(t, err)
|
|
require.Empty(t, logs.All())
|
|
}
|
|
|
|
// TestAttachDependencies_NoLogOnFanOutRefusal proves the bounded fan-out
|
|
// refusal is not logged by this diagnostic: it is a distinct sentinel
|
|
// (ResourceExhausted before any task is serialized), never the withheld
|
|
// verdict AC-PLUGINS-TASK-DEPS-002.4 covers.
|
|
func TestAttachDependencies_NoLogOnFanOutRefusal(t *testing.T) {
|
|
log, logs := newObservedLogger(t)
|
|
d := newTestDataHost(manifest.Capabilities{APIRead: []string{"tasks"}})
|
|
d.host.log = log
|
|
d.tasks.dependencyViewsErr = taskservice.ErrDependencyFanOutExceeded
|
|
tasks := []pluginsdk.Task{{ID: "task-1"}}
|
|
|
|
err := d.host.attachDependencies(context.Background(), tasks, []*taskmodels.Task{{ID: "task-1"}}, true, "ListTasks")
|
|
require.Error(t, err)
|
|
require.Empty(t, logs.All())
|
|
}
|
|
|
|
// TestAttachDependencies_NilLoggerNeverPanics proves a bare test/production
|
|
// host with no logger wired (log is nil) degrades to no-op logging instead
|
|
// of panicking, on both the source-absent and derivation-withheld paths.
|
|
func TestAttachDependencies_NilLoggerNeverPanics(t *testing.T) {
|
|
host := &pluginHost{capabilities: manifest.Capabilities{APIRead: []string{"tasks"}}}
|
|
tasks := []pluginsdk.Task{{ID: "task-1"}}
|
|
|
|
require.NotPanics(t, func() {
|
|
_ = host.attachDependencies(context.Background(), tasks, []*taskmodels.Task{{ID: "task-1"}}, false, "GetTask")
|
|
})
|
|
}
|