@@ -714,3 +714,79 @@ func TestInitializer_GenerateTwBrief(t *testing.T) {
714714 t .Error ("Skill name 'tw-brief' doesn't match required pattern" )
715715 }
716716}
717+
718+ // TestInitializer_GenerateOpenCodePlugin tests that OpenCode plugin is generated correctly
719+ // with proper hook mappings and ctx.$ Bun shell API usage.
720+ func TestInitializer_GenerateOpenCodePlugin (t * testing.T ) {
721+ tmpDir := t .TempDir ()
722+ init := NewInitializer (tmpDir )
723+
724+ // Run initialization with opencode
725+ err := init .Run (false , []string {"opencode" })
726+ if err != nil {
727+ t .Fatalf ("Run failed: %v" , err )
728+ }
729+
730+ // Verify plugin file exists
731+ pluginPath := filepath .Join (tmpDir , ".opencode" , "plugins" , "taskwing-hooks.js" )
732+ content , err := os .ReadFile (pluginPath )
733+ if err != nil {
734+ t .Fatalf ("Failed to read taskwing-hooks.js: %v" , err )
735+ }
736+
737+ contentStr := string (content )
738+
739+ // Verify exports default as async function
740+ if ! strings .Contains (contentStr , "export default async" ) {
741+ t .Error ("Plugin missing 'export default async' export" )
742+ }
743+
744+ // Verify ctx parameter is used
745+ if ! strings .Contains (contentStr , "(ctx)" ) {
746+ t .Error ("Plugin missing ctx parameter in default export" )
747+ }
748+
749+ // Verify session.created hook exists
750+ if ! strings .Contains (contentStr , `"session.created"` ) {
751+ t .Error ("Plugin missing session.created hook handler" )
752+ }
753+
754+ // Verify session.idle hook exists
755+ if ! strings .Contains (contentStr , `"session.idle"` ) {
756+ t .Error ("Plugin missing session.idle hook handler" )
757+ }
758+
759+ // Verify ctx.$ calls to taskwing hook commands (Bun shell API)
760+ if ! strings .Contains (contentStr , "ctx.$`taskwing hook session-init`" ) {
761+ t .Error ("Plugin missing ctx.$`taskwing hook session-init` call" )
762+ }
763+ if ! strings .Contains (contentStr , "ctx.$`taskwing hook continue-check" ) {
764+ t .Error ("Plugin missing ctx.$`taskwing hook continue-check` call" )
765+ }
766+
767+ // Verify no inline secrets (basic check)
768+ secretPatterns := []string {
769+ "api_key" ,
770+ "apikey" ,
771+ "secret" ,
772+ "password" ,
773+ "token" ,
774+ "credential" ,
775+ }
776+ contentLower := strings .ToLower (contentStr )
777+ for _ , pattern := range secretPatterns {
778+ // Skip if it's just a reference (like error.message)
779+ if strings .Contains (contentLower , pattern ) && ! strings .Contains (contentLower , "error.message" ) {
780+ // Allow "token" in comments explaining what the plugin does
781+ if pattern == "token" && strings .Contains (contentStr , "// " ) {
782+ continue
783+ }
784+ t .Errorf ("Plugin may contain sensitive data (found pattern: %s)" , pattern )
785+ }
786+ }
787+
788+ // Verify managed marker exists (for update detection)
789+ if ! strings .Contains (contentStr , "TASKWING_MANAGED_PLUGIN" ) {
790+ t .Error ("Plugin missing TASKWING_MANAGED_PLUGIN marker" )
791+ }
792+ }
0 commit comments