// Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT package plugin_e2e import ( "strings" "testing" "github.com/tidwall/gjson" ) // multipleRestrictPlugin registers TWO distinct plugins that each call // Restrict() with an independently valid Rule. cmdpolicy.Resolve rejects // more than one distinct Restrict-owner regardless of each rule's own // validity (internal/cmdpolicy/resolver.go's distinctOwners check runs // before ValidateRule). const multipleRestrictPlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import "github.com/larksuite/cli/extension/platform" func init() { platform.Register( platform.NewPlugin("restrict-a", "0.1.0"). Restrict(&platform.Rule{ Name: "a-rule", Allow: []string{"docs/**"}, MaxRisk: platform.RiskRead, }). MustBuild()) platform.Register( platform.NewPlugin("restrict-b", "0.1.0"). Restrict(&platform.Rule{ Name: "b-rule", Allow: []string{"im/**"}, MaxRisk: platform.RiskRead, }). MustBuild()) } ` // TestInstallMultipleRestrictPluginsPin pins reason_code=multiple_restrict_plugins. // Observed real output (any command, e.g. "schema" -- the fatal guard walks // every RunE in the tree so it fires regardless of which command runs): // // exit=2 // stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition", // "message":"multiple plugins called Restrict; only one plugin may own the // policy: [restrict-a restrict-b]", // "hint":"plugin policy configuration is broken (reason_code // multiple_restrict_plugins); fix the plugin's Restrict rule or remove the // conflicting plugin"}} func TestInstallMultipleRestrictPluginsPin(t *testing.T) { bin := buildFork(t, "multiple-restrict", multipleRestrictPlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) assertReasonCodeEnvelope(t, res, "multiple_restrict_plugins") } // invalidRulePlugin registers a single plugin whose Restrict Rule carries a // syntactically-invalid MaxRisk value. Neither the Builder nor the staging // Registrar validate Rule *contents* (only nilness) -- semantic validation // happens later, in cmdpolicy.ValidateRule, called from // cmd/platform_bootstrap.go's applyUserPolicyPruning -> cmdpolicy.Resolve. const invalidRulePlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import "github.com/larksuite/cli/extension/platform" func init() { platform.Register( platform.NewPlugin("invalid-rule", "0.1.0"). Restrict(&platform.Rule{ Name: "bad-risk", Allow: []string{"docs/**"}, MaxRisk: platform.Risk("bogus"), }). MustBuild()) } ` // TestInstallInvalidRulePin pins reason_code=invalid_rule. Observed real output // (schema): // // exit=2 // stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition", // "message":"plugin \"invalid-rule\" rule invalid: invalid max_risk \"bogus\": // must be one of read|write|high-risk-write", // "hint":"plugin policy configuration is broken (reason_code invalid_rule); // fix the plugin's Restrict rule or remove the conflicting plugin"}} func TestInstallInvalidRulePin(t *testing.T) { bin := buildFork(t, "invalid-rule", invalidRulePlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) assertReasonCodeEnvelope(t, res, "invalid_rule") } // installFailedPlugin is a hand-written bare platform.Plugin (not // Builder-based -- Install returning a plain error is not expressible // through the Builder's fluent API) whose Install always returns an error. // FailurePolicy=FailClosed makes the host abort rather than warn+skip. const installFailedPlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import ( "errors" "github.com/larksuite/cli/extension/platform" ) type installFailed struct{} func (installFailed) Name() string { return "install-failed" } func (installFailed) Version() string { return "0.1.0" } func (installFailed) Capabilities() platform.Capabilities { return platform.Capabilities{FailurePolicy: platform.FailClosed} } func (installFailed) Install(r platform.Registrar) error { return errors.New("deliberate install failure") } func init() { platform.Register(installFailed{}) } ` // TestInstallFailedPin pins reason_code=install_failed. Observed real output // (schema): // // exit=2 // stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition", // "message":"plugin \"install-failed\" (install_failed): Install returned // error: deliberate install failure", // "hint":"plugin \"install-failed\" failed to install (reason_code // install_failed); fix or remove the plugin before running commands"}} func TestInstallFailedPin(t *testing.T) { bin := buildFork(t, "install-failed", installFailedPlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) assertReasonCodeEnvelope(t, res, "install_failed") } // installPanicPlugin is a hand-written bare Plugin whose Install panics. // safeCallInstall (internal/platform/host.go) recovers and converts the // panic into a typed install_panic error rather than crashing the binary. const installPanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import "github.com/larksuite/cli/extension/platform" type installPanic struct{} func (installPanic) Name() string { return "install-panic" } func (installPanic) Version() string { return "0.1.0" } func (installPanic) Capabilities() platform.Capabilities { return platform.Capabilities{FailurePolicy: platform.FailClosed} } func (installPanic) Install(r platform.Registrar) error { panic("deliberate install panic") } func init() { platform.Register(installPanic{}) } ` // TestInstallPanicPin pins reason_code=install_panic. Observed real output // (schema): // // exit=2 // stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition", // "message":"plugin \"install-panic\" (install_panic): Install panicked: // deliberate install panic", // "hint":"plugin \"install-panic\" failed to install (reason_code // install_panic); fix or remove the plugin before running commands"}} func TestInstallPanicPin(t *testing.T) { bin := buildFork(t, "install-panic", installPanicPlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) assertReasonCodeEnvelope(t, res, "install_panic") } // pluginNamePanicPlugin is a hand-written bare Plugin whose Name() panics. // InstallAll's outer loop calls safeCallName BEFORE it ever reads // Capabilities(), so this aborts unconditionally regardless of what // Capabilities() would have declared (host.go's isUntrustedConfigError // list) -- Capabilities() here is a throwaway zero value, never invoked. const pluginNamePanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import "github.com/larksuite/cli/extension/platform" type pluginNamePanic struct{} func (pluginNamePanic) Name() string { panic("deliberate name panic") } func (pluginNamePanic) Version() string { return "0.1.0" } func (pluginNamePanic) Capabilities() platform.Capabilities { return platform.Capabilities{} } func (pluginNamePanic) Install(r platform.Registrar) error { return nil } func init() { platform.Register(pluginNamePanic{}) } ` // TestInstallPluginNamePanicPin pins reason_code=plugin_name_panic. Observed real // output (schema): // // exit=2 // stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition", // "message":"plugin \"\" (plugin_name_panic): Plugin.Name() // panicked: deliberate name panic", // "hint":"plugin \"\" failed to install (reason_code // plugin_name_panic); fix or remove the plugin before running commands"}} func TestInstallPluginNamePanicPin(t *testing.T) { bin := buildFork(t, "plugin-name-panic", pluginNamePanicPlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) assertReasonCodeEnvelope(t, res, "plugin_name_panic") } // capabilitiesPanicPlugin is a hand-written bare Plugin whose Capabilities() // panics. readFailurePolicy (internal/platform/host.go) re-invokes // Capabilities() to decide FailOpen vs FailClosed, panics again, and its // recover leaves the pre-set FailClosed default in place -- so this aborts // unconditionally too, without the plugin ever declaring a real policy. const capabilitiesPanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import "github.com/larksuite/cli/extension/platform" type capabilitiesPanic struct{} func (capabilitiesPanic) Name() string { return "capabilities-panic" } func (capabilitiesPanic) Version() string { return "0.1.0" } func (capabilitiesPanic) Capabilities() platform.Capabilities { panic("deliberate capabilities panic") } func (capabilitiesPanic) Install(r platform.Registrar) error { return nil } func init() { platform.Register(capabilitiesPanic{}) } ` // TestInstallCapabilitiesPanicPin pins reason_code=capabilities_panic. Observed // real output (schema): // // exit=2 // stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition", // "message":"plugin \"capabilities-panic\" (capabilities_panic): // Plugin.Capabilities() panicked: deliberate capabilities panic", // "hint":"plugin \"capabilities-panic\" failed to install (reason_code // capabilities_panic); fix or remove the plugin before running commands"}} func TestInstallCapabilitiesPanicPin(t *testing.T) { bin := buildFork(t, "capabilities-panic", capabilitiesPanicPlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) assertReasonCodeEnvelope(t, res, "capabilities_panic") } // restrictsMismatchPlugin is a hand-written bare Plugin that declares // Capabilities.Restricts=true (paired with the required FailClosed) but // whose Install never calls r.Restrict. stagingRegistrar.validateSelf // (internal/platform/staging.go) checks this exact declared-vs-actual // consistency after Install returns. const restrictsMismatchPlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import "github.com/larksuite/cli/extension/platform" type restrictsMismatch struct{} func (restrictsMismatch) Name() string { return "restricts-mismatch" } func (restrictsMismatch) Version() string { return "0.1.0" } func (restrictsMismatch) Capabilities() platform.Capabilities { return platform.Capabilities{Restricts: true, FailurePolicy: platform.FailClosed} } func (restrictsMismatch) Install(r platform.Registrar) error { return nil } func init() { platform.Register(restrictsMismatch{}) } ` // TestInstallRestrictsMismatchPin pins reason_code=restricts_mismatch. // Observed real output (schema): // // exit=2 // stderr={"ok":false,"error":{"type":"validation","subtype":"failed_precondition", // "message":"plugin \"restricts-mismatch\" (restricts_mismatch): // Capabilities.Restricts=true but Install did not call r.Restrict", // "hint":"plugin \"restricts-mismatch\" failed to install (reason_code // restricts_mismatch); fix or remove the plugin before running commands"}} func TestInstallRestrictsMismatchPin(t *testing.T) { bin := buildFork(t, "restricts-mismatch", restrictsMismatchPlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) assertReasonCodeEnvelope(t, res, "restricts_mismatch") } // mustBuildPanicPlugin calls MustBuild() on a Builder with an invalid plugin // name ("BadName!!" fails ^[a-z0-9][a-z0-9-]*$). This panics from // plugin.init(), which runs from the blank-import BEFORE main() has a // chance to install any recover-and-envelope guard -- so, unlike every // other case here, this crashes the process outright: no JSON envelope, // non-zero exit, a raw Go panic trace on stderr. const mustBuildPanicPlugin = `// Code generated by plugin_e2e; DO NOT EDIT. package plugin import "github.com/larksuite/cli/extension/platform" func init() { platform.Register(platform.NewPlugin("BadName!!", "0.1.0").MustBuild()) } ` // TestInstallMustBuildInitPanicCrashesBinary pins the MustBuild init-panic crash // shape. This is NOT the plugin_install envelope -- it is a bare Go panic // crash, because it happens in init(), before main()'s recover guard // exists. Observed real output (schema): // // exit=2 // stderr=panic: plugin "BadName!!": invalid plugin name "BadName!!": must // match ^[a-z0-9][a-z0-9-]*$ // // goroutine 1 [running]: // larkcustomer/plugin.init.0(...) // .../plugin/plugin.go:7 // ... func TestInstallMustBuildInitPanicCrashesBinary(t *testing.T) { bin := buildFork(t, "mustbuild-panic", mustBuildPanicPlugin) res := run(t, bin, "schema") t.Logf("exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) if res.exit == 0 { t.Fatalf("expected non-zero exit on init panic; exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr) } if gjson.Valid(res.stderr) { t.Fatalf("expected a raw panic trace, not a JSON envelope; stderr=%s", res.stderr) } if !strings.Contains(res.stderr, "panic:") { t.Fatalf("stderr missing Go panic trace; stderr=%s", res.stderr) } if !strings.Contains(res.stderr, `invalid plugin name "BadName!!"`) { t.Errorf("stderr missing the Builder's invalid-name message; stderr=%s", res.stderr) } }