diff --git a/internal/guard/wasm_labels_success_test.go b/internal/guard/wasm_labels_success_test.go new file mode 100644 index 000000000..51a5afa8b --- /dev/null +++ b/internal/guard/wasm_labels_success_test.go @@ -0,0 +1,205 @@ +package guard + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/github/gh-aw-mcpg/internal/difc" +) + +// labelAgentSuccessWasm exports "label_agent" + "memory". It writes the following +// JSON to the output buffer and returns its length, exercising the full success +// path of LabelAgent (including the final logging + return statement). +// +// {"difc_mode": "strict"} +// +// Compiled by hand: the function body stores each byte of the JSON into linear +// memory at outPtr (param 2) via a sequence of i32.const/i32.store8 pairs, then +// returns i32.const . +var labelAgentSuccessWasm = []byte{ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x01, 0x60, 0x04, 0x7f, 0x7f, 0x7f, + 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x18, 0x02, 0x0b, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x06, 0x6d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0a, 0xb9, 0x01, 0x01, 0xb6, 0x01, 0x00, 0x20, 0x02, 0x41, + 0xfb, 0x00, 0x3a, 0x00, 0x00, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x01, 0x20, 0x02, 0x41, 0xe4, + 0x00, 0x3a, 0x00, 0x02, 0x20, 0x02, 0x41, 0xe9, 0x00, 0x3a, 0x00, 0x03, 0x20, 0x02, 0x41, 0xe6, + 0x00, 0x3a, 0x00, 0x04, 0x20, 0x02, 0x41, 0xe3, 0x00, 0x3a, 0x00, 0x05, 0x20, 0x02, 0x41, 0xdf, + 0x00, 0x3a, 0x00, 0x06, 0x20, 0x02, 0x41, 0xed, 0x00, 0x3a, 0x00, 0x07, 0x20, 0x02, 0x41, 0xef, + 0x00, 0x3a, 0x00, 0x08, 0x20, 0x02, 0x41, 0xe4, 0x00, 0x3a, 0x00, 0x09, 0x20, 0x02, 0x41, 0xe5, + 0x00, 0x3a, 0x00, 0x0a, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x0b, 0x20, 0x02, 0x41, 0x3a, 0x3a, + 0x00, 0x0c, 0x20, 0x02, 0x41, 0x20, 0x3a, 0x00, 0x0d, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x0e, + 0x20, 0x02, 0x41, 0xf3, 0x00, 0x3a, 0x00, 0x0f, 0x20, 0x02, 0x41, 0xf4, 0x00, 0x3a, 0x00, 0x10, + 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x11, 0x20, 0x02, 0x41, 0xe9, 0x00, 0x3a, 0x00, 0x12, + 0x20, 0x02, 0x41, 0xe3, 0x00, 0x3a, 0x00, 0x13, 0x20, 0x02, 0x41, 0xf4, 0x00, 0x3a, 0x00, 0x14, + 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x15, 0x20, 0x02, 0x41, 0xfd, 0x00, 0x3a, 0x00, 0x16, 0x41, + 0x17, 0x0b, +} + +// labelResourceRespondsToCapabilitiesWasm exports "label_resource" + "memory". +// It is input-sensitive: the response it writes depends on the size of the input +// JSON, so the tests below fail if LabelResource stops injecting the +// "capabilities" key (or starts injecting it unconditionally). +// +// If inLen > 60 it writes (54 bytes): +// +// {"resource":{"description":"caps"},"operation":"read"} +// +// otherwise it writes (58 bytes): +// +// {"resource":{"description":"no-caps"},"operation":"write"} +// +// Input size reference (json.Marshal key-sorted output, toolName="some_tool", +// args={"arg":"value"}): +// +// baseline (caps == nil) 53 B – {"tool_args":…,"tool_name":…} +// + capabilities ({}) 71 B +// threshold 60 B +// +// Compiled from: +// +// (module +// (memory (export "memory") 1) +// (func (export "label_resource") (param i32 i32 i32 i32) (result i32) +// ;; params: inPtr inLen outPtr outLen +// local.get 1 i32.const 60 i32.gt_u +// if +// ;; store the "caps"/"read" JSON byte-by-byte at outPtr +// i32.const 54 +// return +// end +// ;; store the "no-caps"/"write" JSON byte-by-byte at outPtr +// i32.const 58)) +var labelResourceRespondsToCapabilitiesWasm = []byte{ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x01, 0x60, 0x04, 0x7f, 0x7f, 0x7f, + 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x1b, 0x02, 0x06, + 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x5f, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x00, 0x00, 0x0a, 0xf5, 0x06, 0x01, 0xf2, 0x06, 0x00, + 0x20, 0x01, 0x41, 0x3c, 0x4b, 0x04, 0x40, 0x20, 0x02, 0x41, 0xfb, 0x00, 0x3a, 0x00, 0x00, 0x20, + 0x02, 0x41, 0x22, 0x3a, 0x00, 0x01, 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x02, 0x20, 0x02, + 0x41, 0xe5, 0x00, 0x3a, 0x00, 0x03, 0x20, 0x02, 0x41, 0xf3, 0x00, 0x3a, 0x00, 0x04, 0x20, 0x02, + 0x41, 0xef, 0x00, 0x3a, 0x00, 0x05, 0x20, 0x02, 0x41, 0xf5, 0x00, 0x3a, 0x00, 0x06, 0x20, 0x02, + 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x07, 0x20, 0x02, 0x41, 0xe3, 0x00, 0x3a, 0x00, 0x08, 0x20, 0x02, + 0x41, 0xe5, 0x00, 0x3a, 0x00, 0x09, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x0a, 0x20, 0x02, 0x41, + 0x3a, 0x3a, 0x00, 0x0b, 0x20, 0x02, 0x41, 0xfb, 0x00, 0x3a, 0x00, 0x0c, 0x20, 0x02, 0x41, 0x22, + 0x3a, 0x00, 0x0d, 0x20, 0x02, 0x41, 0xe4, 0x00, 0x3a, 0x00, 0x0e, 0x20, 0x02, 0x41, 0xe5, 0x00, + 0x3a, 0x00, 0x0f, 0x20, 0x02, 0x41, 0xf3, 0x00, 0x3a, 0x00, 0x10, 0x20, 0x02, 0x41, 0xe3, 0x00, + 0x3a, 0x00, 0x11, 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x12, 0x20, 0x02, 0x41, 0xe9, 0x00, + 0x3a, 0x00, 0x13, 0x20, 0x02, 0x41, 0xf0, 0x00, 0x3a, 0x00, 0x14, 0x20, 0x02, 0x41, 0xf4, 0x00, + 0x3a, 0x00, 0x15, 0x20, 0x02, 0x41, 0xe9, 0x00, 0x3a, 0x00, 0x16, 0x20, 0x02, 0x41, 0xef, 0x00, + 0x3a, 0x00, 0x17, 0x20, 0x02, 0x41, 0xee, 0x00, 0x3a, 0x00, 0x18, 0x20, 0x02, 0x41, 0x22, 0x3a, + 0x00, 0x19, 0x20, 0x02, 0x41, 0x3a, 0x3a, 0x00, 0x1a, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x1b, + 0x20, 0x02, 0x41, 0xe3, 0x00, 0x3a, 0x00, 0x1c, 0x20, 0x02, 0x41, 0xe1, 0x00, 0x3a, 0x00, 0x1d, + 0x20, 0x02, 0x41, 0xf0, 0x00, 0x3a, 0x00, 0x1e, 0x20, 0x02, 0x41, 0xf3, 0x00, 0x3a, 0x00, 0x1f, + 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x20, 0x20, 0x02, 0x41, 0xfd, 0x00, 0x3a, 0x00, 0x21, 0x20, + 0x02, 0x41, 0x2c, 0x3a, 0x00, 0x22, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x23, 0x20, 0x02, 0x41, + 0xef, 0x00, 0x3a, 0x00, 0x24, 0x20, 0x02, 0x41, 0xf0, 0x00, 0x3a, 0x00, 0x25, 0x20, 0x02, 0x41, + 0xe5, 0x00, 0x3a, 0x00, 0x26, 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x27, 0x20, 0x02, 0x41, + 0xe1, 0x00, 0x3a, 0x00, 0x28, 0x20, 0x02, 0x41, 0xf4, 0x00, 0x3a, 0x00, 0x29, 0x20, 0x02, 0x41, + 0xe9, 0x00, 0x3a, 0x00, 0x2a, 0x20, 0x02, 0x41, 0xef, 0x00, 0x3a, 0x00, 0x2b, 0x20, 0x02, 0x41, + 0xee, 0x00, 0x3a, 0x00, 0x2c, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x2d, 0x20, 0x02, 0x41, 0x3a, + 0x3a, 0x00, 0x2e, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x2f, 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, + 0x00, 0x30, 0x20, 0x02, 0x41, 0xe5, 0x00, 0x3a, 0x00, 0x31, 0x20, 0x02, 0x41, 0xe1, 0x00, 0x3a, + 0x00, 0x32, 0x20, 0x02, 0x41, 0xe4, 0x00, 0x3a, 0x00, 0x33, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, + 0x34, 0x20, 0x02, 0x41, 0xfd, 0x00, 0x3a, 0x00, 0x35, 0x41, 0x36, 0x0f, 0x0b, 0x20, 0x02, 0x41, + 0xfb, 0x00, 0x3a, 0x00, 0x00, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x01, 0x20, 0x02, 0x41, 0xf2, + 0x00, 0x3a, 0x00, 0x02, 0x20, 0x02, 0x41, 0xe5, 0x00, 0x3a, 0x00, 0x03, 0x20, 0x02, 0x41, 0xf3, + 0x00, 0x3a, 0x00, 0x04, 0x20, 0x02, 0x41, 0xef, 0x00, 0x3a, 0x00, 0x05, 0x20, 0x02, 0x41, 0xf5, + 0x00, 0x3a, 0x00, 0x06, 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x07, 0x20, 0x02, 0x41, 0xe3, + 0x00, 0x3a, 0x00, 0x08, 0x20, 0x02, 0x41, 0xe5, 0x00, 0x3a, 0x00, 0x09, 0x20, 0x02, 0x41, 0x22, + 0x3a, 0x00, 0x0a, 0x20, 0x02, 0x41, 0x3a, 0x3a, 0x00, 0x0b, 0x20, 0x02, 0x41, 0xfb, 0x00, 0x3a, + 0x00, 0x0c, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x0d, 0x20, 0x02, 0x41, 0xe4, 0x00, 0x3a, 0x00, + 0x0e, 0x20, 0x02, 0x41, 0xe5, 0x00, 0x3a, 0x00, 0x0f, 0x20, 0x02, 0x41, 0xf3, 0x00, 0x3a, 0x00, + 0x10, 0x20, 0x02, 0x41, 0xe3, 0x00, 0x3a, 0x00, 0x11, 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, + 0x12, 0x20, 0x02, 0x41, 0xe9, 0x00, 0x3a, 0x00, 0x13, 0x20, 0x02, 0x41, 0xf0, 0x00, 0x3a, 0x00, + 0x14, 0x20, 0x02, 0x41, 0xf4, 0x00, 0x3a, 0x00, 0x15, 0x20, 0x02, 0x41, 0xe9, 0x00, 0x3a, 0x00, + 0x16, 0x20, 0x02, 0x41, 0xef, 0x00, 0x3a, 0x00, 0x17, 0x20, 0x02, 0x41, 0xee, 0x00, 0x3a, 0x00, + 0x18, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x19, 0x20, 0x02, 0x41, 0x3a, 0x3a, 0x00, 0x1a, 0x20, + 0x02, 0x41, 0x22, 0x3a, 0x00, 0x1b, 0x20, 0x02, 0x41, 0xee, 0x00, 0x3a, 0x00, 0x1c, 0x20, 0x02, + 0x41, 0xef, 0x00, 0x3a, 0x00, 0x1d, 0x20, 0x02, 0x41, 0x2d, 0x3a, 0x00, 0x1e, 0x20, 0x02, 0x41, + 0xe3, 0x00, 0x3a, 0x00, 0x1f, 0x20, 0x02, 0x41, 0xe1, 0x00, 0x3a, 0x00, 0x20, 0x20, 0x02, 0x41, + 0xf0, 0x00, 0x3a, 0x00, 0x21, 0x20, 0x02, 0x41, 0xf3, 0x00, 0x3a, 0x00, 0x22, 0x20, 0x02, 0x41, + 0x22, 0x3a, 0x00, 0x23, 0x20, 0x02, 0x41, 0xfd, 0x00, 0x3a, 0x00, 0x24, 0x20, 0x02, 0x41, 0x2c, + 0x3a, 0x00, 0x25, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x26, 0x20, 0x02, 0x41, 0xef, 0x00, 0x3a, + 0x00, 0x27, 0x20, 0x02, 0x41, 0xf0, 0x00, 0x3a, 0x00, 0x28, 0x20, 0x02, 0x41, 0xe5, 0x00, 0x3a, + 0x00, 0x29, 0x20, 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x2a, 0x20, 0x02, 0x41, 0xe1, 0x00, 0x3a, + 0x00, 0x2b, 0x20, 0x02, 0x41, 0xf4, 0x00, 0x3a, 0x00, 0x2c, 0x20, 0x02, 0x41, 0xe9, 0x00, 0x3a, + 0x00, 0x2d, 0x20, 0x02, 0x41, 0xef, 0x00, 0x3a, 0x00, 0x2e, 0x20, 0x02, 0x41, 0xee, 0x00, 0x3a, + 0x00, 0x2f, 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x30, 0x20, 0x02, 0x41, 0x3a, 0x3a, 0x00, 0x31, + 0x20, 0x02, 0x41, 0x22, 0x3a, 0x00, 0x32, 0x20, 0x02, 0x41, 0xf7, 0x00, 0x3a, 0x00, 0x33, 0x20, + 0x02, 0x41, 0xf2, 0x00, 0x3a, 0x00, 0x34, 0x20, 0x02, 0x41, 0xe9, 0x00, 0x3a, 0x00, 0x35, 0x20, + 0x02, 0x41, 0xf4, 0x00, 0x3a, 0x00, 0x36, 0x20, 0x02, 0x41, 0xe5, 0x00, 0x3a, 0x00, 0x37, 0x20, + 0x02, 0x41, 0x22, 0x3a, 0x00, 0x38, 0x20, 0x02, 0x41, 0xfd, 0x00, 0x3a, 0x00, 0x39, 0x41, 0x3a, + 0x0b, +} + +// TestLabelAgent_SuccessPath exercises the full happy path of LabelAgent, including +// the final response-parsing, marshaled-debug-logging, and return statements that +// were previously uncovered by existing error-path-only tests. +func TestLabelAgent_SuccessPath(t *testing.T) { + g, cleanup := setupRawWasmModule(t, labelAgentSuccessWasm, "label-agent-success") + defer cleanup() + + validPolicy := map[string]any{ + "allow-only": map[string]any{ + "repos": "public", + "min-integrity": "none", + }, + } + result, err := g.LabelAgent(context.Background(), validPolicy, &mockBackendCaller{}, nil) + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "strict", result.DIFCMode) +} + +// TestLabelResource_SuccessPath exercises the full happy path of LabelResource, +// including the caps != nil branch and a successful parseResourceResponse call +// returning a populated resource and a non-default ("read") operation. +// +// labelResourceRespondsToCapabilitiesWasm only returns the "caps"/"read" response +// when the input JSON exceeds 60 bytes, which happens only once the "capabilities" +// key is added to the 53-byte baseline input. The assertions therefore fail if +// LabelResource stops injecting capabilities. +func TestLabelResource_SuccessPath(t *testing.T) { + g, cleanup := setupRawWasmModule(t, labelResourceRespondsToCapabilitiesWasm, "label-resource-success") + defer cleanup() + + caps := difc.NewCapabilities() + resource, operation, err := g.LabelResource( + context.Background(), + "some_tool", + map[string]any{"arg": "value"}, + &mockBackendCaller{}, + caps, + ) + + require.NoError(t, err) + require.NotNil(t, resource) + assert.Equal(t, "caps", resource.Description, "capabilities must be included in the WASM input (inLen should exceed the 60-byte threshold)") + assert.Equal(t, difc.OperationRead, operation) +} + +// TestLabelResource_SuccessPath_NilCapabilities verifies that LabelResource omits +// the "capabilities" key when caps is nil, covering the complementary branch. The +// input stays at the 53-byte baseline (below the fixture's 60-byte threshold), so +// the guard returns the distinct "no-caps"/"write" response. +func TestLabelResource_SuccessPath_NilCapabilities(t *testing.T) { + g, cleanup := setupRawWasmModule(t, labelResourceRespondsToCapabilitiesWasm, "label-resource-success-nil-caps") + defer cleanup() + + resource, operation, err := g.LabelResource( + context.Background(), + "some_tool", + map[string]any{"arg": "value"}, + &mockBackendCaller{}, + nil, + ) + + require.NoError(t, err) + require.NotNil(t, resource) + assert.Equal(t, "no-caps", resource.Description, "nil capabilities must not add the capabilities key to the WASM input") + assert.Equal(t, difc.OperationWrite, operation) +}