@@ -13,6 +13,7 @@ import (
1313 runnablev0 "github.com/codefly-dev/core/generated/go/codefly/runnable/v0"
1414 "github.com/codefly-dev/core/resources"
1515 corerunnable "github.com/codefly-dev/core/runnable"
16+ "github.com/codefly-dev/core/standards"
1617 "github.com/stretchr/testify/require"
1718)
1819
@@ -263,3 +264,132 @@ func TestGenerateRunnableBindingsPreparesASolutionsReads(t *testing.T) {
263264 require .Equal (t , basev0 .RunnableExecution_RECOVERY_RECOMPUTE , prepared .GetPolicy ().GetRecovery ())
264265 }
265266}
267+
268+ // committedArtifact is testdata/solution-runnables/api.swagger.json: the
269+ // releases backend's interface artifact exactly as solution-runtime-go's
270+ // Server.InterfaceArtifact renders it (codefly-dev/solution-runtime-go#77) —
271+ // every operation core-solutions' releases solution serves, each with the
272+ // policy it declares on the authority runtime, and its MCP server — with no
273+ // edit. It is a snapshot of that shape; the runtime's own tests hold
274+ // InterfaceArtifact to deriving in core.
275+ func committedArtifact (t * testing.T ) []byte {
276+ t .Helper ()
277+ return mustRead (t , filepath .Join ("testdata" , "solution-runnables" , "api.swagger.json" ))
278+ }
279+
280+ // saveCommittedSolution places the artifact where a solution backend commits
281+ // it (openapi/api.swagger.json beside its service.codefly.yaml), so the whole
282+ // chain runs on it: generate contracts publishes it as the http endpoint's
283+ // contract, extensions and all, and generate runnables reads what that wrote.
284+ func saveCommittedSolution (t * testing.T , ctx context.Context , artifact []byte ) (root , moduleDir string ) {
285+ t .Helper ()
286+ root , moduleDir = saveFixtureWorkspace (t , ctx , "releases" ,
287+ & resources.ModuleInterface {Endpoints : []* resources.InterfaceEndpoint {
288+ {Service : "backend" , Endpoint : "http" , Visibility : resources .VisibilityPublic },
289+ }},
290+ & resources.Service {
291+ Name : "backend" ,
292+ Version : "0.3.0" ,
293+ Agent : & resources.Agent {Kind : resources .ServiceAgent , Name : "go" , Version : "0.0.70" , Publisher : "codefly.dev" },
294+ Endpoints : []* resources.Endpoint {
295+ {Name : "http" , API : "http" , Visibility : resources .VisibilityPublic , Exposure : resources .ExposureNone },
296+ },
297+ },
298+ )
299+ writeFixtureFile (t , filepath .Join (moduleDir , "services" , "backend" , filepath .FromSlash (standards .OpenAPIPath )), artifact )
300+ writeFixtureFile (t , filepath .Join (moduleDir , composition .PackageManifestFileName ), []byte (`schema: codefly/module-package/v2
301+ kind: module-package
302+ id: obin/releases
303+ version: 0.1.0
304+ minimum-codefly-version: ">=0.0.0"
305+ artifact-roots:
306+ - contracts
307+ - services
308+ contracts:
309+ composition: ">=2.0.0 <3.0.0"
310+ services:
311+ - name: backend
312+ endpoints: [http]
313+ ` ))
314+ t .Chdir (root )
315+ resetContractsFlags (t )
316+ resetRunnablesFlags (t )
317+ resetRunnableBindingsFlags (t )
318+ if out , err := captureStdout (t , func () error { return ContractsCmd .RunE (ContractsCmd , []string {"releases" }) }); err != nil {
319+ t .Fatalf ("generate contracts: %v\n %s" , err , out )
320+ }
321+ return root , moduleDir
322+ }
323+
324+ // TestTheCommittedArtifactBecomesTools gates the seam end to end rather than
325+ // inferring it: the runtime's own artifact survives generate contracts with its
326+ // vendor extensions, and from what that published, generate runnables derives
327+ // exactly the two reads and generate runnable-bindings prepares exactly two GET
328+ // calls. The writes, the any-checked-caller routes, the datasource read
329+ // (datasource.sources:invoke, not read) and the MCP route become nothing.
330+ func TestTheCommittedArtifactBecomesTools (t * testing.T ) {
331+ ctx := context .Background ()
332+ artifact := committedArtifact (t )
333+ root , moduleDir := saveCommittedSolution (t , ctx , artifact )
334+
335+ published := mustRead (t , filepath .Join (moduleDir , "contracts" , "api" , "backend" , "http" , "openapi.json" ))
336+ require .Equal (t , artifact , published , "generate contracts publishes the artifact byte for byte" )
337+
338+ require .NoError (t , RunnablesCmd .RunE (RunnablesCmd , []string {"releases" }))
339+ index := readIndex (t , moduleDir )
340+ methods := make ([]string , 0 , len (index .Operations ))
341+ for _ , entry := range index .Operations {
342+ methods = append (methods , entry .Method )
343+ }
344+ require .Equal (t , []string {"GET /v1/releases/history" , "GET /v1/releases/recorded" }, methods )
345+
346+ require .NoError (t , RunnableBindingsCmd .RunE (RunnableBindingsCmd , nil ))
347+ raw := mustRead (t , filepath .Join (root , "configurations" , "local" , RunnableBindingsGroup + ".env" ))
348+ var keys []string
349+ for _ , line := range strings .Split (strings .TrimSpace (string (raw )), "\n " ) {
350+ key , value , found := strings .Cut (line , "=" )
351+ if ! found || strings .HasPrefix (line , "#" ) {
352+ continue
353+ }
354+ keys = append (keys , key )
355+ prepared , err := corerunnable .DecodePrepared ([]byte (value ))
356+ require .NoError (t , err )
357+ require .Equal (t , "GET" , prepared .GetCall ().GetRest ().GetVerb ())
358+ tool , err := corerunnable .ToolFromPrepared (prepared )
359+ require .NoError (t , err )
360+ require .Equal (t , runnablev0 .ToolExposure_EFFECT_READ_ONLY , tool .GetEffect ())
361+ }
362+ require .Equal (t , []string {
363+ "RELEASES__BACKEND_HTTP_RELEASES_GET_V1_RELEASES_HISTORY" ,
364+ "RELEASES__BACKEND_HTTP_RELEASES_GET_V1_RELEASES_RECORDED" ,
365+ }, keys )
366+ }
367+
368+ // TestAnArtifactThatMarksNothingNamesItsReads is the InterfaceDocument trap: an
369+ // artifact whose reads declare their policies but carry no marker — rendered by
370+ // InterfaceDocument rather than Server.InterfaceArtifact, or by a runtime from
371+ // before the marker — derives nothing, and the run says so read by read rather
372+ // than writing an empty index in silence. It is a notice, not a refusal: every
373+ // solution still on such a runtime is in exactly this state.
374+ func TestAnArtifactThatMarksNothingNamesItsReads (t * testing.T ) {
375+ ctx := context .Background ()
376+ var doc map [string ]any
377+ require .NoError (t , json .Unmarshal (committedArtifact (t ), & doc ))
378+ for _ , methods := range doc ["paths" ].(map [string ]any ) {
379+ for _ , operation := range methods .(map [string ]any ) {
380+ delete (operation .(map [string ]any ), corerunnable .OpenAPIOperationMarker )
381+ }
382+ }
383+ unmarked , err := json .Marshal (doc )
384+ require .NoError (t , err )
385+ _ , moduleDir := saveCommittedSolution (t , ctx , unmarked )
386+
387+ out , err := captureStdout (t , func () error { return RunnablesCmd .RunE (RunnablesCmd , []string {"releases" }) })
388+ require .NoError (t , err )
389+ require .Empty (t , readIndex (t , moduleDir ).Operations )
390+ for _ , read := range []string {"GET /v1/releases/history" , "GET /v1/releases/recorded" } {
391+ require .Contains (t , out , read + ": backend/http requires only \" read\" but its document marks no route x-codefly-operation" )
392+ }
393+ require .Contains (t , out , "Server.InterfaceArtifact" )
394+ require .NotContains (t , out , "POST /v1/releases/record:" , "a write is not a read the trap lost" )
395+ }
0 commit comments