package controller import ( "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) // P2: deliverable judgment must read structured artifact_type / file signals, // not just the legacy title/summary/uri string heuristics. func TestRuntimeArtifactsAreSummaryOnly_StructuredFields(t *testing.T) { cases := []struct { name string artifacts []gin.H want bool }{ { name: "empty list is not summary-only", artifacts: nil, want: false, }, { // The regression the review flagged: Runtime now emits // artifact_type=document under runtime://.../artifacts/, // which the old "/artifacts/summary" uri heuristic missed. name: "document type with new uri scheme and no files is summary-only", artifacts: []gin.H{{ "artifact_id": "art_1", "artifact_type": "document", "title": "Backend delivery", "summary": "下面是方案总结", "uri": "runtime://swm_x/artifacts/backend_1", }}, want: true, }, { name: "code_patch is a real deliverable", artifacts: []gin.H{{ "artifact_type": "code_patch", "uri": "runtime://swm_x/artifacts/backend_1", }}, want: false, }, { name: "deployment_manifest is a real deliverable", artifacts: []gin.H{{"artifact_type": "deployment_manifest"}}, want: false, }, { name: "document type but with file-change signal is a deliverable", artifacts: []gin.H{{ "artifact_type": "document", "metadata": map[string]any{"files_modified": float64(2)}, }}, want: false, }, { name: "mixed set with one real deliverable is not summary-only", artifacts: []gin.H{ {"artifact_type": "document"}, {"artifact_type": "code_patch"}, }, want: false, }, { name: "legacy runtime execution summary marker still detected", artifacts: []gin.H{{ "artifact_type": "other", "title": "Runtime execution summary", }}, want: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.want, runtimeArtifactsAreSummaryOnly(tc.artifacts)) }) } } // P6a: Resource CRUD must reject a secret_ref that is not an azkv:// reference. func TestNormalizeResourcePayload_SecretRefMustBeAzkv(t *testing.T) { base := func(secretRef string) resourcePayload { return resourcePayload{Name: "repo", ResourceType: "git", SecretRef: secretRef} } _, err := normalizeResourcePayload(base("https://example.com/token")) require.Error(t, err) require.Contains(t, err.Error(), "azkv://") _, err = normalizeResourcePayload(base("sk-live-plaintext-leak")) require.Error(t, err) got, err := normalizeResourcePayload(base("azkv://heicode-kv.vault.azure.net/secrets/repo")) require.NoError(t, err) require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo", got.SecretRef) // Empty secret_ref stays allowed (secret can be set later via UpsertResourceSecret). _, err = normalizeResourcePayload(base("")) require.NoError(t, err) } // P6b: plaintext-secret detection must scan string VALUES, not only key names. func TestValueLooksLikeSecretAndPlaintextScan(t *testing.T) { positives := []string{ "sk-abcdefghij1234567890", "sk-live-abcdefghijklmnopqrst", "ghp_abcdefghijklmnopqrstuvwxyz0123", "AKIAIOSFODNN7EXAMPLE", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.s5h6Qk0c5Qx2hQ", "-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END", } for _, s := range positives { require.Truef(t, valueLooksLikeSecret(s), "expected secret-like: %q", s) } negatives := []string{ "", "main", "https://github.com/org/repo.git", "azkv://heicode-kv.vault.azure.net/secrets/repo", "a normal sentence with sk in it", } for _, s := range negatives { require.Falsef(t, valueLooksLikeSecret(s), "expected NOT secret-like: %q", s) } // Value hidden under an innocuous key must now be caught. require.True(t, containsPlaintextSecret(map[string]any{"note": "sk-abcdefghij1234567890"})) require.False(t, containsPlaintextSecret(map[string]any{"repo_url": "https://github.com/o/r.git"})) } // P5: a single source of truth for the default model; no placeholder fallback. func TestDefaultAgentModelID_SingleSourceNoPlaceholder(t *testing.T) { def := defaultAgentModelID() require.Equal(t, "gpt-5.4", def) // Draft builder must use the single default, never agent-model-. plan := buildAgentDraftAgentPlan("backend", "", nil) require.Equal(t, def, plan.DefaultModelID) require.NotContains(t, plan.DefaultModelID, "agent-model-") // Explicit client model is still honored. plan = buildAgentDraftAgentPlan("backend", "gpt-5.4-mini", nil) require.Equal(t, "gpt-5.4-mini", plan.DefaultModelID) // Every role template resolves to the single default, no claude-* hardcoding. for _, tpl := range agentRoleTemplates() { require.Equal(t, def, tpl.DefaultModel, "role %s", tpl.Key) } }